I'm building a site that must support authentication both via LDAP, and with 'local' users that are managed in the site only.
Currently I have the following Devise models:
class User < ActiveRecord::Base
end
class LdapUser < User
devise :ldap_authenticatable, :rememberable, :trackable
end
class LocalUser < User
devise :database_authenticatable, :registerable, :confirmable, :recoverable, :trackable
end
Devise is generating routes that are separate for each of these, i.e. /local_users/sign_in
and /ldap_users/sign_in
. This isn't ideal, users shouldn't need to know which type of user they are, so I'd like to unify it all into one form, with one set of sign in/out URLs.
I've looked at some solutions for how to do this, but they seem to rely on the fact that the models have the same Devise configuration, or the same authentication method.
The only other example online of this sort of problem that I've found is this Google Groups thread: https://groups.google.com/forum/#!topic/plataformatec-devise/x7ZI6TsdI2E - which hasn't been answered.
This has taken me quite a while to figure out, but I've eventually got a working solution.
I also want to give most of the credit for this to Jordan MacDonald who posted the question I mentioned above in the Devise Google Group. While that thread didn't have an answer on it, I found the project he had been working on, read the code and adapted it to my needs. The project is Triage and I highly recommend reading the implementations of SessionController and the routes.
I also recommend Jordan's blog post on Devise: http://www.wastedintelligence.com/blog/2013/04/07/understanding-devise/
As above, my model is as follows, and I'm using the gem devise_ldap_authenticatable
. In this example, I have two users, LdapUser
and LocalUser
, but I see no reason why this wouldn't work for any two Devise user models, as long as you have some way of differentiating between them.
class User < ActiveRecord::Base
end
class LdapUser < User
devise :ldap_authenticatable, :rememberable, :trackable
end
class LocalUser < User
devise :database_authenticatable, :registerable, :confirmable, :recoverable, :trackable
end
The first part we need is the controller. It should inherit from Devise::SessionsController
, and it chooses which type of user we are authenticating, then explicitly passing this on to the authentication stage, which is handled by Warden.
As I was using LDAP against an Active Directory domain for one part of the authentication, I could easily tell which details should be authenticated against LDAP, and which shouldn't, but this is implementation specific.
class SessionsController < Devise::SessionsController
def create
# Figure out which type of user we are authenticating.
# The 'type_if_user' method is implementation specific, and not provided.
user_class = nil
error_string = 'Login failed'
if type_of_user(request.params['user']) == :something
user_class = :local_user
error_string = 'Username or password incorrect'
else
user_class = :ldap_user
error_string = 'LDAP details incorrect'
end
# Copy user data to ldap_user and local_user
request.params['ldap_user'] = request.params['local_user'] = request.params['user']
# Use Warden to authenticate the user, if we get nil back, it failed.
self.resource = warden.authenticate scope: user_class
if self.resource.nil?
flash[:error] = error_string
return redirect_to new_session_path
end
# Now we know the user is authenticated, sign them in to the site with Devise
# At this point, self.resource is a valid user account.
sign_in(user_class, self.resource)
respond_with self.resource, :location => after_sign_in_path_for(self.resource)
end
def destroy
# Destroy session
end
def new
# Set up a blank resource for the view rendering
self.resource = User.new
end
end
Devise sets up lots of routes for each type of user, and for the most part we want to let it do this, but as we are overriding the SessionsController
, so need it to skip this part.
After it has set up its routes, we then want to add our own handlers for sign_in
and sign_out
. Note that the devise scope being local_user
doesn't matter, it just needs a default scope, we are overriding this in the controller anyway. Also note that this is local_user
singular, this caught me out and caused lots of trouble.
devise_for :ldap_users, :local_users, skip: [ :sessions ]
devise_scope :local_user do
get 'sign_in' => 'sessions#new', :as => :new_session
post 'sign_in' => 'sessions#create', :as => :create_session
delete 'sign_out' => 'sessions#destroy', :as => :destroy_session
end
The view is very simple, and can modified without causing too many issues.
<div>
<%= form_for(resource, :as => 'user', url: create_session_path) do %>
<fieldset>
<legend>Log In</legend>
<label>LDAP Username or Database Email</label>
<input type="text" placeholder="Username or Email" name="user[email]" />
<label>Password</label>
<input type="password" placeholder="Password" name="user[password]" />
<input type="submit" class="button" value="Log In" />
</fieldset>
<% end %>
</div>
I hope this helps someone else. This is the second web app I've worked on that had to have both LDAP and local authentication (the first being a C# MVC4 application), and both times I've had significant trouble getting authentication frameworks to handle this nicely.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With