I have a website where users need to login to be able to comment on pictures. For the most part, users will use their university credentials to login. However, I’ve found a few recently retired people who would be able to help us identify people in the pictures. But since they are retired, their university credentials are no longer valid. So for these people, I’d like to be able to make a local account for them to login. My rails app uses authlogic to check credentials. And I’ve been authenticating against the university ldap server for a while without any problems, but mixing in local accounts is going to require a few changes.

Normally, I’d just edit the user model with the following.

acts_as_authentic do |c|
		c.validate_password_field = false
		c.crypto_provider = Authlogic::CryptoProviders::SCrypt
		c.logged_in_timeout				= 2.hours
	end

	def valid_password?(password)
		ldap_settings = YAML.load_file("#{Rails.root.to_s}/config/ldap.yml")[Rails.env]

		ldap_settings[:host] = ldap_settings['host']
		ldap_settings[:port] = ldap_settings['port']
		ldap_settings[:encryption] = { method: :simple_tls } if ldap_settings['ssl']
		ldap_settings[:auth] =
			{ method: :simple, username: "uid=#{self.username}, #{ldap_settings['base']}", password: password }

		ldap = Net::LDAP.new(ldap_settings)
		ldap.bind
	end

And the fields that my user model has are:

create_table "users", force: :cascade do |t|
    t.string   "username",          limit: 255
    t.string   "firstname",         limit: 255
    t.string   "lastname",          limit: 255
    t.string   "persistence_token", limit: 255
    t.datetime "last_request_at"
    t.string   "role",              limit: 255
    t.datetime "created_at",                                    null: false
    t.datetime "updated_at",                                    null: false
  end

The default way that authlogic checks passwords is to run a method called valid_password? and return true if the password is good and false if not. To check against an ldap server, I just wrote a new valid_password? method (shown above) with the settings for my ldap server in a file called ldap.yml. The ldap.yml file looks something like this:

development:
    host: local.example.com
    port: 636
    base: ou=people,dc=test,dc=example,dc=com
    ssl: true

production:
    host: ldap.example.com
    port: 636
    base: ou=people,dc=example,dc=com
    ssl: true

Now, in order to use local accounts as well, I need to make a few changes to the user model. First, I need to determine if the account is a local or an ldap account. Since by the time I was starting this, I already had about 100 ldap accounts, but no local accounts, I added a field to the user model called local_account. If it’s true, it’s a local account and if not, it’s ldap. This way, I didn’t need to change anything on the ldap accounts I already created. Secondly, I needed to add the fields that authlogic needs to store passwords in the database. These were strings: crypted_password and password_salt.

$ rails g migration add_fields_to_users crypted_password:string password_salt:string local_account:boolean

After I migrated these in, I had to change my new user form to accept a username and password for local users. Since I don’t anticipate making many of these, I didn’t do anything fancy with the form. It looks like this:

<%= form_for(@user) do |f| %>
  Univeristy id: <%= f.text_field :username %>
Firstname: <%= f.text_field :firstname %>
Lastname: <%= f.text_field :lastname %>
Role: <%= f.collection_select :role, User::ROLES, :to_s, :humanize %>

If person doesn't have a University id, use their email address and be sure to fill out the following. Most importantly, be sure to check the local_account box. (The account name doesn't have to be an email address, but since University ids are never email addresses, they'd be safe to use.

Local_account? <%= f.check_box :local_account %>
Password: <%= f.password_field :password %>
Password confirmation: <%= f.password_field :password_confirmation %>
<%= f.submit 'Submit' %> <% end %>

Now, how should I change valid_password? to check locally for the password if the local_account value is true? After reading the code for a bit, I found that authlogic itself calls a method valid_password? to check the password. All I need to do is change my valid_password? to first ask if local_account is true. If it is, call the authlogic valid_password?. If it’s not, call my valid_password?. The first problem is obvious in that it’s confusing that we’re calling the same method, but in different locations. After a bit more searching, I found that authlogic has already thought of this. They have a method called verify_password_method which will let me change the name of my method to something else. So to fix all of this, here’s what I changed.

In the user_session.rb file

class UserSession < Authlogic::Session::Base
	verify_password_method :good_password?
end

In the user.rb file, I changed my valid_password? method to good_password? and then call the authlogic valid_password? method if local_account is true.

def good_password?(password)
	if local_account == false
		ldap_settings = YAML.load_file("#{Rails.root.to_s}/config/ldap.yml")[Rails.env]
		ldap_settings[:host] = ldap_settings['host']
		ldap_settings[:port] = ldap_settings['port']
		ldap_settings[:encryption] = { method: :simple_tls } if ldap_settings['ssl']
		ldap_settings[:auth] =
			{ method: :simple, username: "uid=#{self.username}, #{ldap_settings['base']}", password: password }

		ldap = Net::LDAP.new(ldap_settings)
		ldap.bind
	else
		# Here checking local accounts in the database
		valid_password?(password, true)
	end
end

This works exactly as I wanted. I still haven’t set things up for people to recover their local password if they forgot it. But since I think that there will be maybe 2-4 people who get local accounts, I can just ask them to email me and I can set it to something manually.