LDAP Authentication in an Apache-Fronted Rails App
If you manage anything beyond the simplest of setups, you've probably got an LDAP server providing directory services to your network. If you don't, this one probably isn't for you.
#### Authenticate using LDAP
The first step is getting Apache to authenticate all requests before they reach your Rails application. This is fiddly work, and Apache already has a rather lovely module -- `mod_authnz_ldap` -- that handles the heavy lifting.
```
# I've used port 443 above because I'm dealing with passwords.
# [...snip...]
AuthType Basic
AuthName "Foo Application Control Panel"
AuthBasicAuthoritative off
AuthBasicProvider ldap
AuthLDAPUrl ldap://ldap.example.com/ou=people,dc=example,dc=com?userid?one
Require valid-user
# [...snip...]
# Your normal Rails HTTP configuration goes here
```
#### Look up the user in Rails
At this point, any request hitting your application has already been authenticated against your LDAP directory. Now you need Rails to identify the user. For this I wrote a mixin called `Xeriom::Acts::ProtectedSystem`:
```ruby
module Xeriom # :nodoc:
module Acts # :nodoc:
module ProtectedSystem # :nodoc:
def self.included(base)
base.send(:extend, ClassMethods)
end
module ClassMethods
def acts_as_protected_system
include InstanceMethods
send(:before_filter, :ensure_user_is_logged_in)
send(:helper_method, :current_user)
send(:helper_method, :logged_in?)
end
end
module InstanceMethods
def ensure_user_is_logged_in
if !logged_in?
authenticate_user
end
end
def logged_in?
!current_user.blank?
end
def current_user
@current_user ||= User.find_by_id(session[:user_id])
end
def current_user=(user)
@current_user = user
session[:user_id] = user.blank? ? nil : user.id
end
def authenticate_user
authenticate_or_request_with_http_basic("Protected Area") do |username, password|
# Lock your application servers down to listen to only
# the web tier or this will kick your ass.
send(:current_user=, User.find_by_username(username))
end
end
end
end
end
end
ActionController::Base.send(:include, Xeriom::Acts::ProtectedSystem)
```
To use it, drop the code in your `lib/` directory, then call `acts_as_protected_system` in your `ApplicationController`:
```ruby
class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
protect_from_forgery # because CSRF sucks!
acts_as_protected_system # lock the door
end
```
The key insight here is that Apache does the hard work of validating credentials against LDAP. Rails simply trusts the authenticated username and looks up the corresponding user record. Just make sure your application servers are locked down to only accept requests from the web tier -- otherwise anyone could pass through a forged username.
Questions or thoughts? Get in touch.