Tuesday, December 27, 2011

Rails authentication using Devise and devise_ldap_authenticatable

About a week ago I posted about my attempts to integrate Rails applications with Active Directory.  I have successfully rolled my own authentication using net/ldap, but I want to use a gem so that there will be more than just me using it and fixing it up.

Last week I was successful getting Devise and Omniauth-ldap to work together to integrate with Active Directory, but I decided that the solution was not enterprise-ready because there is currently no way to override the login page and make it look like the rest of my application.

Now I think I have the solution, and it was a little bit easier too.  Devise + devise_ldap_authenticatable.  Here is the walkthrough for what worked for me.

Adding your Gems
For this project I am going to add the following lines to my gemfile:
gem "nifty-generators", :git => 'https://github.com/ryanb/nifty-generators.git'
gem 'devise'
gem 'devise_ldap_authenticatable', :git => 'git://github.com/cschiewek/devise_ldap_authenticatable.git'
gem 'annotate', :git => 'git://github.com/ctran/annotate_models.git'

The annotate gem and the nifty-generators gem aren't necessary, but I like to use them. Nifty-generators will make the flash notices that you'll need later on in the project so you might consider using it if you're just following along. Now run bundle to get everything set up.

Setting up nifty-generators
There are a couple steps that I'm going to go through to set up nifty-generators which are optional if you decide that you don't mind having your rails project look ugly.

rails generate nifty:config
rails generate nifty:layout
rails generate nifty:scaffold Change title:string description:string
cp public/stylesheets/application.css app/assets/stylesheets/nifty.css
rm public/index.html
cat app/views/layouts/application.html.erb | sed \
's/javascript_include_tag :defaults/javascript_include_tag "application"/' \
> /tmp/what.txt
 cp /tmp/what.txt app/views/layouts/application.html.erb
rake db:migrate
Also, make sure you edit config/routes.rb and add a path to root:
root :to => 'changes#index'
At this point you should have a working application that will allow you to create, edit, and delete changes.  It should look nice and neat.  Make sure that you have the ability to delete objects, and if you do not check the line in app/views/layouts/application.html.erb to make sure that javascript_include_tag is including "application" not :defaults.  A tell-tale sign that javascript is the source of your delete problem is that when you try to delete a change you're not even given the "are you sure" prompt.


Configuring devise for user management
 Now we need to create a model for our users, and we'll call that model User.  We will use devise because then we'll have all of the helpers created for things like checking if the current user is authenticated.  This part is amazingly simple.
rails generate devise:install
rails generate devise User 
rake db:migrate 
Bam!  That's it.  Now you have a user model with all kinds of neat options.  If you didn't want to perform Active Directory authentication you would be almost done now with just some work to customize the views.

Setting up devise_ldap_authenticatable
First we need to generate the devise_ldap_authenticatable installation.
rails generate devise_ldap_authenticatable:install
There are options that you can set, but I don't need them.  The gem assumes that your user model is called User which mine is.

Now we need to set the LDAP parameters used to connect to Active Directory.  This is kept config/ldap.yml.

development:
  host: domaincontroller.domain.com 
  port: 389 
  attribute: sAMAccountName 
  base: dc=domain,dc=com
  admin_user: cn=admin,dc=test,dc=com
  admin_password: admin_password
  ssl: false
  # <<: *AUTHORIZATIONS

The most important thing here is that you set the attribute to sAMAccountName and that you have the full DN and password of your binding user.  There are options here to force certain group membership or make sure that certain attributes are set, but I haven't played with that yet.  Right now, anybody that is a member of the domain is able to log into my app.

The last thing we need to do is make some edits to config/initializers/devise.rb which has some new options that were added by the devise_ldap_authenticatable installer.  This is why my configuration looks like:

  # ==> LDAP Configuration 
  config.ldap_logger = true
  config.ldap_create_user = true 
  # config.ldap_update_password = true
  config.ldap_config = "#{Rails.root}/config/ldap.yml"
  config.ldap_check_group_membership = false
  config.ldap_check_attributes = false
  config.ldap_use_admin_to_bind = true 
  # config.ldap_ad_group_check = false

Look a little further in the document to find these lines and change them to match mine:

config.authentication_keys = [ :username ]
config.case_insensitive_keys = [ :email, :username ]

Changes to the user model
We want to keep track of some Active Directory attributes, so we need to make a few changes to our user model.  Let's add a firstname, lastname, username, and displayname field.

rails generate migration add_fields_to_users firstname:string lastname:string username:string displayname:string
rake db:migrate

This is when I like to use annotate so that I can keep track of what I've done to my models.  Anyway, lets take a look at the app/models/user.rb file.  We need to update our attributes accessible, and add some methods to pull in Active Directory attributes and save them with our user.

attr_accessible :username, :email, :password, :password_confirmation, :remember_me, :firstname, :lastname, :displayname

  before_save :get_ldap_lastname, :get_ldap_firstname, :get_ldap_displayname, :get_ldap_email

  def get_ldap_lastname
      Rails::logger.info("### Getting the users last name")
      tempname = Devise::LdapAdapter.get_ldap_param(self.username,"sn")
      puts "\tLDAP returned lastname of " + tempname
      self.lastname = tempname
  end 

  def get_ldap_firstname
      Rails::logger.info("### Getting the users first name")
      tempname = Devise::LdapAdapter.get_ldap_param(self.username,"givenname")
      puts "\tLDAP returned firstname of " + tempname
      self.firstname = tempname
  end 

  def get_ldap_displayname
      Rails::logger.info("### Getting the users display name")
      tempname = Devise::LdapAdapter.get_ldap_param(self.username,"displayname")
      self.displayname = tempname
  end 

  def get_ldap_email
      Rails::logger.info("### Getting the users email address")
      tempmail = Devise::LdapAdapter.get_ldap_param(self.username,"mail")
      self.email = tempmail
  end


Changes to the views
The final change is to make sure that our view is going to reflect whether or not a user is logged in and provide some basic information about the logged in user.  I added this right under the title in app/views/changes/index.html.erb

<% if user_signed_in? %>
    Welcome <%= current_user.firstname %>, you last logged on <%= current_user.last_sign_in_at %> from <%= current_user.last_sign_in_ip %>


<% else %>
    <%= link_to "login", new_user_session_path %>
<% end %>

We also need to make a critical change to the login form.  Right now it is still asking for an email address and it wont let you get away with typing a username into the email field.  So we need to have devise show us our view so that we can edit it.
rails generate devise:views
Now edit app/views/devise/sessions/new.html.erb.  Change the email label to say username and change the email_field to text_field.

Also, if you want to have the user redirect to your root path, then you need to add something like this to config/routes.rb
match '/changes' => 'changes#index', :as => 'user_root'

2 comments:

Anonymous said...

Thanks

Dean C. said...

thanks for sharing ur AD integration experience. much appreciated. how long did all of this take to implement? and is there any cost required to buy additional AD servers?