Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using one sign-in form with two Devise user models and different authentication methods

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.

like image 246
danpalmer Avatar asked Jan 15 '14 23:01

danpalmer


1 Answers

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/


Model

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

Controller

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

Routes

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

View

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.

like image 88
danpalmer Avatar answered Oct 01 '22 23:10

danpalmer