Use Separate Authentication Model with Devise on Rails

Use separate authentication model with Devise on Rails

Have been recently working on a project where I was using Devise to keep user's tokens for different services. A bit different case, but still your question got me thinking for a while.

I'd bind Devise to Account model anyway. Why? Let's see.

Since my email is the only thing that can identify me as a user (and you refer to Account as the User) I would place it in accounts table in pair with the password, so that I'm initially able do use basic email/password authentication. Also I'd keep API tokens in authentications.

As you've mentioned, OmniAuth module needs to store provider and id. If you want your user to be able to be connected with different services at the same time (and for some reason you do) then obviously you need to keep both provider-id pairs somewhere, otherwise one will simply be overwritten each time a single user authenticates. That leads us to the Authentication model which is already suitable for that and has a reference to Account.

So when looking for a provider-id pair you want to check authentications table and not accounts. If one is found, you simply return an account associated with it. If not then you check if account containing such email exists. Create new authentication if the answer is yes, otherwise create one and then create authentication for it.

To be more specific:

#callbacks_controller.rb
controller Callbacks < Devise::OmniauthCallbacksContoller
def omniauth_callback
auth = request.env['omniauth.auth']
authentication = Authentication.where(provider: auth.prodiver, uid: auth.uid).first
if authentication
@account = authentication.account
else
@account = Account.where(email: auth.info.email).first
if @account
@account.authentication.create(provider: auth.provider, uid: auth.uid,
token: auth.credentials[:token], secret: auth.credentials[:secret])
else
@account = Account.create(email: auth.info.email, password: Devise.friendly_token[0,20])
@account.authentication.create(provider: auth.provider, uid: auth.uid,
token: auth.credentials[:token], secret: auth.credentials[:secret])
end
end
sign_in_and_redirect @account, :event => :authentication
end
end

#authentication.rb
class Authentication < ActiveRecord::Base
attr_accessible :provider, :uid, :token, :secret, :account_id
belongs_to :account
end

#account.rb
class Account < ActiveRecord::Base
devise :database_authenticatable
attr_accessible :email, :password
has_many :authentications
end

#routes.rb
devise_for :accounts, controllers: { omniauth_callbacks: 'callbacks' }
devise_scope :accounts do
get 'auth/:provider/callback' => 'callbacks#omniauth_callback'
end

That should give you what you need while keeping the flexibility you want.

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

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.

Rails Devise: authenticate either two models

I think you can use the devise_group option.

Example:

inside BlogsController (or any other controller, it doesn't matter which):
devise_group :blogger, contains: [:user, :admin]

Generated methods:
authenticate_blogger! # Redirects unless user or admin are signed in
blogger_signed_in? # Checks whether there is either a user or an admin signed in
current_blogger # Currently signed in user or admin
current_bloggers # Currently signed in user and admin

Use:
before_action :authenticate_blogger! # Redirects unless either a user or an admin are authenticated
before_action ->{ authenticate_blogger! :admin } # Redirects to the admin login page
current_blogger :user # Preferably returns a User if one is signed in

Multiple model authentication in Rails using devise

Is email a required field for the User? If so, you can overwrite the Devise SessionController locate the user's email if username is used and process the authentication.

I would also STRONGLY advise against having a separate User and Admin model, unless Admin inherits User. You are only asking for problems in user management. I would recommend setting a role[s] attribute on the User model and using a framework like Pundit to manage access policy.

Using Devise for Two Different Models but the same Login Form

I think the only way to handle this would be to have your own custom sign in form and controller that determined the type of user and then sign them in correctly. I would recommend an approach like what mark mentioned for simplicity (take a look at something like CanCan to manage roles).

Another potential problem with having multiple user models is that you will have multiple versions of all the devise helper methods. So for current_<resource> and <resource>_signed_in? you would have current_user, current_business_user, user_signed_in? and business_user_signed_in?. Then you would either have to implement your own versions of these methods or you would need to check both versions everywhere you used them.

Two different login pages for one model with devise on rails

Devise sign in form is just a form with action matching to devise controller.

You can write this form code anywhere you like,

<%= form_for(:user, :url => session_path(:user)) do |f| %>
<%= f.text_field :email %>
<%= f.password_field :password %>
<%= f.check_box :remember_me %>
<%= f.label :remember_me %>
<%= f.submit 'Sign in' %>
<%= link_to "Forgot your password?", new_password_path(:user) %>
<% end %>

When using devise in rails, how do I add two different types of users that will use the site in two different ways?

This type of functionality is tricky, namely because you have to put functionality before implementation (IE most people get hung up about Devise, whereas it might not feature at all)


You have two ways:

  • Roles (authorization)
  • Multiple models (authentication)

Devise is an authentication system (user logged in); you may be better using authorization (can user do x or y). Authorization is out of Devise's scope.

Whilst you could use multiple models (Devise), I think it creates too much unnecessary bloat for what you need.

Instead, I would use a very simple role system (using enum):

#app/models/user.rb
class User < ActiveRecord::Base
enum role: [:job_seeker, :employer]

has_one :profile
before_create :build_profile

has_many :applications
has_many :listings, through: :applications
end

#app/models/application.rb
class Application < ActiveRecord::Base
belongs_to :listing
belongs_to :user
end

#app/models/listing.rb
class Listing < ActiveRecord::Base
has_many :applications
has_many :applicants, through: :applications, class_name: "User", foreign_key: :user_id
end

You'll need to add a role column (int) to your users table. You'll create the default role by using a default: [x] switch when creating your column:

def change
add_column :users, :role, :integer, default: 0 #-> defaults to job seeker
end

--

You've described several factors which would lend themselves perfectly to this:

  • Job seeker will create a profile
  • Employer will create a profile and post listings

... all meaning your "flow" will remain similar for both user types. You'd just have to manage what each user can do with authorization.



Setup

#config/routes.rb
resource :profile, controller: :users, only: [:show, :update] #-> url.com/profile
resources :listings, only: [:show] do
post :apply, on: :member #-> url.com/listings/:id/apply
end
resources :companies, controller: :users, only: [:show]

#app/controllers/users_controller.rb
class UsersController < ApplicationController
#show will automatically be loaded

def update
current_user.update profile_params
end

private

def profile_params
params.require(:user).permit(profile_attributes: [:name, :etc, :etc])
end
end

#app/views/users/show.html.erb
<%= form_for current_user do |f| %>
<%= f.fields_for :profile do |p|
<% if current_user.job_seeker? %>
<%= f.text_field :name, placeholder: "Your name" %>
<% elsif current_user.employer? %>
<%= f.text_field :name, placeholder: "Company name" %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>

You'd then be able to use the following to check whether a user can create listings, or just view:

#app/controllers/listings_controller.rb
class ListingsController < ApplicationController
before_action :check_seeker, only: [:apply]
before_action :check_employer, only: [:new, :create, :destroy]

def new #-> employers
@listing = current_user.listings.new
end

def apply #-> job seekers
@listing = Listing.find params[:id]
@application = current_user.applications.new
@application.listing = @listing
redirect_to @listing, notice: "Application successful!" if @application.save
end

private

def check_seeker
redirect_to listings_path, notice: "Only Job Seekers Allowed" unless current_user.job_seeker?
end

def check_employer
redirect_to root_url, notice: "Only Employers Allowed" unless current_user.employer?
end
end

Hopefully this gives you the gist.



Devise

To get Devise working with your new column, you'll need to extend the Devise Sanitizer:

#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?

protected

def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :role
end
end

This will allow you to change the role field on signup:

#app/views/devise/registrations/new.html.erb
.....
<%= f.select :role, User.roles %>


Related Topics



Leave a reply



Submit