Ruby on Rails
WindowsDomainAuthentication

If your company runs Windows as its main platform, then most likely users are authenticated by logging onto a Windows domain. If you’re building a company intranet app, you most likely want to integrate with this user database, rather than creating your own. Does that mean Rails is out of the question? Fear not, here are a couple ways to make your Rails app play friendly with the rest of your Windows network!

The Easy Way Out: Running on IIS

If you have the luxury of running your Rails app on IIS, then you essentially gain Windows integrated authentication for free. First, your IIS website needs to have “Integrated Authentication” set for the authentication mode. Then, IIS will automatically authenticate website users (invisibly with IE, with a pop-up for FireFox) and pass you a server variable with the user’s identity. You can access it like this:


request.env["AUTH_USER"]

EDIT: Until FastCGI support for IIS is solidified (see this rails_core thread ), the only way to run a Rails app “under IIS” is to use a proxy, like ISAPI_Rewrite. See Brian Hogan’s documentation on how to do this. Also, the httpd.conf configuration file for ISAPI_Rewrite will need to be modified to pass authentication parameters by adding “,A,D” to the RewriteProxy flags. See the ISAPI_Rewrite documentation for all the details. Finally, using the above configuration I was able to pull the AUTH_USER data from Rails using:


request.env["HTTP_X_ISRW_PROXY_AUTH_USER"]

The Less Traveled Path

The method above is an all-or-nothing approach. Either your entire Rails app is protected, or none of it is. It also requires IIS, and acts a little funky with FireFox. If you long for a traditional, HTML-based login, with more control over what is and isn’t protected, that also doesn’t require IIS, look no further! We can leverage the power of the Win32 API to accomplish what IIS does, however there are a few caveats:

The relevant code to determine if a domain, username, password is valid is:


require 'dl/win32'

LOGON32_LOGON_NETWORK = 3
LOGON32_PROVIDER_DEFAULT = 0
BOOL_SUCCESS = 1
AdvApi32 = DL.dlopen("advapi32")
Kernel32 = DL.dlopen("kernel32")

# Load the DLL functions
logon_user = AdvApi32['LogonUser', 'ISSSIIp']
close_handle = Kernel32['CloseHandle', 'IL']

# Authenticate user
ptoken = "\0" * 4
r,rs = logon_user.call(username, domain, password, LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, ptoken)
success = (r == BOOL_SUCCESS)
		
# Close impersonation token
token = ptoken.unpack('L')[0]
close_handle.call(token)

Alright, now it’s time to see what a full-blown LoginController, that you can simply drop into your Rails app would look like. This controller assumes that the views login.rhtml and logout.rhtml exist. You can check whether a user is logged in by checking session[:username], which is how you can also get the user’s identity. Note that the username expected is of the form domain\username, although you could have a separate textbox for domain if you wish (or simply set it to your default corporate domain, if you only have a single one).


class LoginController < ApplicationController
  before_filter :authorize, :except => [:login, :logout]
  
  def login
    if request.get?
      session[:username] = nil
    else
      # Split username on domain and alias
      split_username = params[:user][:name].split('\\', 2)
      if split_username.length != 2
        flash[:notice] = "You did not enter a properly formatted domain and alias"
        redirect_to :action => 'login'
      else
        # Authenticate username and password
        domain = split_username[0]
        username = split_username[1]
        password = params[:user][:password]
        if authenticate domain, username, password
          # User has been authenticated
          session[:username] = username
        else
          flash[:notice] = "Incorrect username or password"
          redirect_to :action => 'login'
        end
      end
    end
  end

  def logout
    session[:username] = nil
  end
  require 'dl/win32'
  LOGON32_LOGON_NETWORK = 3
  LOGON32_PROVIDER_DEFAULT = 0
  BOOL_SUCCESS = 1
  AdvApi32 = DL.dlopen("advapi32")
  Kernel32 = DL.dlopen("kernel32")

  def authorize
    redirect_to( :controller => 'login', :action => 'login' ) unless logged_in?
  end

  def logged_in?
    session[:username] != nil
  end

  def authenticate(domain, username, password)
    # Load the DLL functions
    logon_user = AdvApi32['LogonUser', 'ISSSIIp']
    close_handle = Kernel32['CloseHandle', 'IL']

    # Normalize username and domain
    username = username.strip.downcase
    domain = domain.strip.downcase
		
    # Authenticate user
    ptoken = "\0" * 4
    r,rs = logon_user.call(username, domain, password, LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, ptoken)
    success = (r == BOOL_SUCCESS)
		
    # Close impersonation token
    token = ptoken.unpack('L')[0]
    close_handle.call(token)

    session[:username] = username
    return success
  end
end

Voila! Integrated domain authentication for your Rails app!


Question:
h3. mod_auth_kerb?

How about using Apache + mod_auth_kerb? Then use request.env["REMOTE_USER"]

Answer:
No Answer


Question:
How would you determine if the user belongs to a group?

Answer:
No Answer