Active Directory Authentication for Ruby on Rails

June 4, 2007 - 5 minute read -
ruby-on-rails active-directory

Ruby on Rails can be used to build many kinds of web applications including public internet applications as well as private intranet ones. As an intranet application it is often very interesting to be able to do Single Sign-On using an existing Active Directory setup. Rails does not support NTLM authentication out of the box which is what is required.

IIS for NTLM

If you are talking about Active Directory authentication then chances are good that you already have a Windows infrastructure. IIS, of course, supports NTLM so that's the first thing I looked into. To use this you have to run Rails under something like FastCGI. To make a long story short, I could not get FastCGI to work with my Rails installation. This looks like a promising path for people who have a mostly Microsoft infrastructure already. This is an area that I hope to explore further, but I gave up on it for now.

If you want to try this route, check out RoR IIS which has a lot of instructions as well as an installer that can do a lot for you. (Again, I tried this first and it didn't work, so your mileage my vary.)

Apache with NTLM

Authentication using Active Directory can be done with Apache on Windows as well using the mod_auth_sspi authentication module, so this seemed like another promising path as Apache and Rails is a more common combination as opposed to IIS and Rails.

Running Rails under Apache can be a bit tricky. There are a lot of options to choose from: mod_ruby, fast_cgi, proxying and scgi. All of these options with no real breakdown that I could find of why use one over another? Proxying with multiple mongrel instances is a very common combination, but I did not want to have mongrel running because I don't want a way to end-run around the authentication mechanism. So, I again tried FastCGI, this time under Apache. Me and FastCGI don't seem to get along though and it again failed to work.

InstantRails uses SCGI though and as a reference implementation for Rails and Apache on Windows, I figured that was a promising path.. SCGI is supposed to be a simpler form of CGI with all of the performance advantages of FastCGI. SCGI is a two part solution. There is an Apache module and there is an SCGI server that runs the rails application. Using this combination, I was able to get Rails running under Apache on Windows.

Under Apache the setup is fairly simple:

LoadModule scgi_module modules/mod_scgi.so
SCGIMount / 127.0.0.1:9999

Rails SCGI is well documented information including configuring Apache. So, I won't repeat everything.

Configure Apache for NTLM Authentication

To use mod_auth_sspi, you have to load the module and then configure your application to use the domain to authenticate users. Once that is in place, your users will be authenticating using Active Directory. If you use a browser like Firefox or Safari, the user will see a Login Prompt, but if you are using Internet Explorer it will automatically pass the user's current Active Directory credentials to Apache and mod_auth_sspi will do the authentication transparently.

LoadModule sspi_auth_module   modules/mod_auth_sspi.so</p>
<p><Directory "D:/work/rails_app/public/">
    AuthType SSPI
    SSPIAuth On
    SSPIAuthoritative On
    SSPIDomain DOMAIN
    SSPIOfferBasic On
    SSPIOmitDomain Off
    Require valid-user
</Directory>

Tying it Together In Rails

At this point, anyone who gets access to your application has been authenticated. If your application does not need to know who the user is, congratulations! You're done. But if your application needs to actually know they who is using the app and not just that it's a valid user then you have a little bit more work to do.

The most common way to check that a user is authenticated in Rails is to use a before_filter in your controllers to check that the session is properly setup and to send the user to a Login controller if they are not authenticated. You'll need do the same thing in this case:

class ApplicationController < ActionController::Base
  protected
  def authenticate
    unless session[:user]
      redirect_to :controller => "login"
      return false
    else
      # TODO: Check that the current user is the same as the session user
      # TODO: Check user against active directory every time?
      # request.env["REMOTE_USER"]
    end
  end
end

Most LoginContollers will show the user a form that allows them to authenticate. A User object is then used to authenticate that the username and password are correct. This implementation will be slightly different though. When Authentication is done by Apache it will set an HTTP variable request.env["REMOTE_USER"] that will be available in your Controllers to identify who the authenticated User is. You can use this information to learn about the user instead of showing them a login form. Remember, the user has already been authenticated by the Active Directory domain, so the user's credentials have been checked.

class LoginController < ApplicationController
  def index
    success = false
    user = User.find_by_name(request.env["REMOTE_USER"]);
    if user
      session[:user] = user
      success = true
    end</p>
    redirect_to request.env["HTTP_REFERER"] ? :back : '/' if success
    redirect_to :action => :error if ! success
  end</p>
  def logout
     session[:user] = nil
  end</p>
  def error
  end
end

In the login controller you can get the REMOTE_USER information and then do whatever you need to do with that information. Most likely you'll want to call into Active Directory using something like ruby-ldap to check on things like groups or role membership and to get extra User information.

Conclusion

There are a lot more implementation pieces that need to be completed. I would like to be able to pull extra information about a user from Active Directory the first time they authenticate and store it in the session so I could know their full name for example.

Hopefully this information will help someone else out because it definitely is not an obvious thing to do (at least it wasn't to me).