something alliterative
ActiveDirectory Integration for Ruby apps
So you're at work, building an awesome rails app for your coworkers to use. Then someone says "can you make it so people can log in with their Windows credentials?" That means ActiveDirectory integration.
Before you code something up, you should check out a couple off the shelf options.
If you're on apache, mod_authz_ldap might do the trick. The problem there is that it's using http basic auth, which doesn't have a logout function.
I haven't tried it, but Rack::Auth::Kerberos might also do the trick.
I wanted a deeper integration with the app than it seemed like I was going to get from either of those, so I ended up rolling my own using the net-ldap gem.
Checking passwords
The first step is to check passwords. For this, you need to know the domain the user is logging in on and the ip and port of your ActiveDirectory server.
The act of connecting to the server (and checking credentials in the process) is called binding. You don't need a special account for it - you can just use the credentials your user gave you.
require 'net-ldap' host = "10.0.0.1" port = 389 # default port domain = "MYDOMAIN" ldap = Net::LDAP.new(:host => host, :port => port) ldap.auth("#{self.username}@#{domain}", password) # ldap.bind is false if username/password are bad if ldap.bind # logged in else # fail end
Checking group membership
Too bad no one ever wants to give access to everyone on the domain. It seems like you always end up needing to restrict it some more. Besides, there's a really big win to be had from letting the sysadmins manage permissions via their normal A/D groups.
Unfortunately, this doesn't work out as cleanly as checking the password. You end up iterating over user data until you find the right items and then parsing those items to find the group names.
You also need to find out where in the ldap data tree your accounts are stored. This is going to be different everywhere - you'll need to talk to your sysadmins to find out. It should be something that looks like ou=Users,ou=Accounts,ou=This Office,dc=something,dc=orother,dc=com. The more sophisticated your A/D setup, the longer and more complicated the string.
Once you search for your user, you get back a bunch of different pieces of data about them. The one we care about is "memberof", which has a list of groups they belong to. Except they don't just list the names of the groups, they prefix them with CN= and maybe suffix them with some other stuff, so we still need to parse things a little more.
treebase = 'ou=Users,ou=Accounts,ou=This Office,dc=something,dc=orother,dc=com' group = 'Uber Users' # ldap.bind is false if username/password are bad if ldap.bind filter = Net::LDAP::Filter.eq("sAMAccountName", self.username) # find the list of groups they belong to and iterate over it group_member = false ldap.search(:base => treebase, :filter => filter) do |entry| entry.each do |attribute, values| if attribute.match(/memberof/) values.each do |value| a = value.split(',') md = a[0].match(/CN=(.+)/) # user is a member of the right group if md[1] == group group_member = true end end end end end result = ldap.get_operation_result unless result.code == 0 self.errors.add_to_base("Error while checking group membership.") end unless result.code == 0 && group_member self.errors.add_to_base("Must be a member of the #{group} group.") end end
Summary
With this technique, you can pretty easily authenticate your users without making them remember yet another username and password. Since you're always going right back to the source, there won't be any weird lags after password changes. You can make a list of their groups and use those for fine-grained access controls (I'd suggest storing them in the session).
0 comments »
Powered by Typo – Thème Frédéric de Villamil | Photo Glenn