Today, I pushed a branch to my fork of authlogic_example
: with-activeldap
.
This branch shows a way of implementing pass-through authentication to an LDAP server using ActiveLdap and Authlogic, with just some small changes to the User
and UserSession
models.
First, we’ll need to bring in the net-ldap
and activeldap
gems. We edit config/environment.rb
to include the following two lines:
config.gem "net-ldap", :lib => false, :version => '>=0.0.5' config.gem "activeldap", :lib => "active_ldap", :version => '1.0.9' |
If you use sudo gem install net-ldap
, you’ll get 0.0.4. Here, I’ve built the 0.0.5 gem from the GitHub repo. This is because there are some bug fixes in the GitHub master that aren’t in the RubyForge gem. In the with-activeldap
branch, the two gems are vendored.
Now, we create config/ldap.yml to configure ActiveLdap’s connection to our LDAP server. Here’s mine:
development: host: 127.0.0.1 base: dc=dev,dc=Asuka,dc=local bind_dn: cn=Manager,dc=dev,dc=Asuka,dc=local password: secret test: host: 127.0.0.1 base: dc=test,dc=Asuka,dc=local bind_dn: cn=Manager,dc=Asuka,dc=local password: secret production: host: 127.0.0.1 base: dc=prod,dc=Asuka,dc=local bind_dn: cn=Manager,dc=Asuka,dc=local password: secret |
This tells ActiveLdap the server/port to connect to, what the base entry for our LDAP objects is, and what user to bind as for operations on the server. Now, we create a LdapUser
class to represent user entries in the LDAP:
class LdapUser < ActiveLdap::Base ldap_mapping :dn_attribute => "uid", :scope => :sub, :prefix => "o=users" end |
This defines an LDAP user as being an entry from the o=users
organization, where all of the entries are distinguished by the uid
attribute. Now, we should be able to use the console to list all of our LDAP users like so:
>> LdapUser.all |
Now, we’ll add some methods to the User
model that allow us to look up users by login in the LDAP and create entries in the database if they don’t already exist. We’ll also need a method for forwarding the credentials provided on the login form to the LDAP and see if they are valid:
class User < ActiveRecord::Base acts_as_authentic do |c| # Don't validate password, since that will be held in the LDAP c.validate_password_field = false end def ldap_entry LdapUser.find(self.login) end # Tries to find a User first by looking into the database and then by # creating a User if there's an LDAP entry for the given login def self.find_or_create_from_ldap(login) find_by_login(login) || create_from_ldap_if_valid(login) end # Creates a User record in the database if there is an entry in the LDAP # with the given login def self.create_from_ldap_if_valid(login) begin User.create(:login => login) if LdapUser.find(login) rescue ActiveLdap::EntryNotFound nil # Don't do anything since we can't find an entry end end protected # Authenticates the user against the LDAP. def valid_ldap_credentials?(password_plaintext) ldap_entry.bind(password_plaintext) ldap_entry.remove_connection true rescue ActiveLdap::AuthenticationError, ActiveLdap::LdapError::UnwillingToPerform false end end |
With this, there is no longer a need for the crypted_password
and password_salt
columns in the users
table, so if those columns exist, you’ll have to write a migration to remove them (or at least allow NULL values for them). Now, we modify the UserSession
to use our custom methods for looking up users and validating their credentials:
class UserSession < Authlogic::Session::Base find_by_login_method :find_or_create_from_ldap verify_password_method :valid_ldap_credentials? end |
With that, we should now be able to log in as a user by providing her uid
as the login and the password. LDAP pass-through authentication achieved! There is a downside though: ActiveLdap is not particularly efficient with its queries but this can be mitigated by storing the user’s LDAP entry (the LdapUser
object) in the User
instance when it is first looked up.
def ldap_entry @ldap_entry ||= LdapUser.find(self.login) end |
In a future post, I will extend this further by modeling LDAP groups and bringing in declarative_authorization to implement role-based access control based on LDAP group membership.
Excellent article, very useful. I’ll be waiting for your post on declarative_authorization.
— nachokb
To put:
config.gem “net-ldap”, :lib => false, :version => ‘>=0.0.5’
fails, but when y try to do:
Net::LDAP.new it works.
And when i try to
sudo gem install net-ldap for the 0.0.4 version it said:
ERROR: could not find gem net-ldap locally or in a repository
Only i try to declare in my config.gem that is needed net-ldap 0.0.5 version.
@maxjgon: Yeah, the 0.0.5
net-ldap
gem needs to be built from the GitHub repository. If you want to try using 0.0.4 (which might work depending on your particular schema), you actually want to use the command below.sudo gem install ruby-net-ldap
And change your
config.gem
line inconfig/environment.rb
accordingly. Turns out the RubyForge-released gem is calledruby-net-ldap
while the GitHub-built one is callednet-ldap
. This threw me for a bit of a loop.If you need more help with this, fire me an e-mail and I’ll try and help you out as much as I can.
Hi,
I am having a problem with getting any return value from “LdapUser.all”. I am using OpenDS and intend to authenticate against users in a group in it. Any help with OpenDS would really help.
Hi! I’m need two diferents forms to authenticate. Firts without ldap, second within ldap. Any idea?
Best Rails + Authlogic + LDAP solution I’ve found. Thanks for the post. I did find that I needed to use ActiveLdap version 1.0.9, because in 1.1.0+ they added gettext_activerecord which didn’t work for me. This may be because of my use of formtastic.
Thanks for the post.
@Ademola: I have little experience with OpenDS and I’m not sure what the nature of your errors are based on your description. If you still need help with this, send me an e-mail and I’ll see what I can do.
@Javier: I don’t think that’s possible with the method that I’m using because I’m overriding the mechanism by which Authlogic validates the username and password as opposed to providing an entirely different (parallel) method of authentication like Authlogic plugins do.
@Adam: Yeah, at the time I wrote the post, 1.0.9 was the latest version. Now I’m using 1.2.0 and gettext_activerecord does work for me, but I’m not using formtastic. You might also be interested in seeing how I integrated it all with declarative_authorization to get RBAC through LDAP groups: http://github.com/enricob/authlogic_example/tree/ldap-rbac
I had trouble with the example code. It turned out, you cannot have password_confirmation field in
view (for example, app/views/users/new.html.erb) if you set validate_password_field = false in the user model. You will get an undefined method error for password_confirmation when it tries to render the page.
I am using rail 2.3.4 and authlogic 2.1.6 gem installation.
@GW: Yes, that’s actually broken by design. I didn’t remove the action or view because I wanted to change as little of the existing code as possible, but the idea behind the example code is that you never create users through the UsersController. You create them using your own LDAP admin tools and then users can log in using their LDAP credentials. When that happens, a User entry is created in the database. The database entry is really just for storing metadata, like the “magic columns” that Authlogic provides.
With a little more work, you can create a form that creates an LDAPUser and saves it to the LDAP (independently of Authlogic).
Good stuff, this was just what I needed to use Alfresco to do authentication for with authlogic.
Now all I have to do is figure out how to get declarative_authorization to play with Alfresco’s groups.
Thanks.
Funny you should mention Alfresco; I used to work with it a bit for an old gig. If the Alfresco groups are available through the same LDAP interface that the users are, you shouldn’t have too much trouble. Just set up the appropriate mappings for groups through ActiveLdap. I have a post about this, in case you haven’t already seen it.