Adding 2FA to Rails Devise - ruby-on-rails

I'm adding 2FA to an existing Rails app using Devise, using the active_model_otp gem to handle the authenticator code. Based on my unit testing, it seems to work. But given my limited knowledge about devise, I'd like to double check if there is any security issues with the implementation, and the sequencing of the calls.
For simplicity, I will only include the controller code for login:
# /app/controllers/users/sessions_controller.rb
class SessionsController < Devise::SessionsController
def create
self.resource = resource_class.find_for_authentication(sign_in_params.except(:password, :otp_response_code, :remember_me))
if resource
if resource.active_for_authentication?
if resource && !resource.user_2fa_activation_date.present?
super
elsif resource && resource.user_2fa_activation_date.present?
if params[:user][:otp_response_code].present?
if resource.authenticate_otp(params[:user][:otp_response_code])
super
else
sign_out(resource)
back_to_login_page("Invalid 2fa token value.")
end
else
sign_out(resource)
back_to_login_page("Your account requires 2-factor authentication.")
end
end
else
super
end
else
# case when email is invalid.
super
end
end
def back_to_login_page(alert_message)
redirect_to new_user_session_path, :alert => alert_message
return false
end
end

Related

Customize Devise SessionsController create action

I want to add a feature that will redirect a user with an expired password to the reset password page.
My controller looks like this
class Users::SessionsController < Devise::SessionsController
def create
user = User.find_by(email: params[:user][:email].downcase)
if user.password_expire?
raw, enc = Devise.token_generator.generate(current_user.class,
:reset_password_token)
user.reset_password_token = enc
user.reset_password_sent_at = Time.now.utc
user.save(validate: false)
redirect_to edit_password_url(user, reset_password_token: raw)
else
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in)
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource)
end
end
end
When I debug with binding.pry at the top of the action, I find that current_user exists and user_signed_in? is true. How is it possible I'm signed in before the create method completes?
If you are using the security extension you don't need at all to take care of the implementation of password expiration.
And if you are not using it, you should check it out - devise_security_extension.
Devise remembers the current user using cookies until they log out.
Just because they've hit your sign in route doesn't mean they're signed out.
Diving into Devise
Tracing the code in Devise to understand what happens we see:
1. Devise::SessionsController#create
class Devise::SessionsController < ApplicationController
# ...
def create
# ...
sign_in(resource_name, resource)
# ...
end
end
2. Devise::Controllers::Helpers#sign_in
def sign_in(resource_or_scope, *args)
# ...
if options[:bypass]
warden.session_serializer.store(resource, scope)
elsif warden.user(scope) == resource && !options.delete(:force)
# Do nothing. User already signed in and we are not forcing it.
true # <=== Here's the moment of truth
else
# ...
end
Conclusion
The user can hit your sessions#create when they're already logged in
In this case the default Devise behaviour is to do nothing
Not quite sure what you want to achieve above, but calling super might come in handy somewhere to resume the default Devise behaviour

Flash messages not working on redirect after setting up Multiple Devise User Models

I implemented this solution
How to Setup Multiple Devise User Models
However the devise flash messages are not working after that ,I have tried reemoving this line
flash.clear but still the flash messages are not being displayed ,
# ../controllers/concerns/accessible.rb
module Accessible
extend ActiveSupport::Concern
included do
before_action :check_user
end
protected
def check_user
flash.clear
if current_tenant
redirect_to(authenticated_tenant_root_path) && return
elsif current_user
redirect_to(authenticated_user_root_path) && return
end
end
end
did you try to change your Devise localization ? It's possible that you don't have appropriate keys for your models. It should be something like this:
en:
devise:
failure:
user:
invalid: 'Welcome user, you are signed in.'
not_found: 'User not found.'
admin:
invalid: 'Invalid admin credentials'
not_found: 'Admin not found'
Hope this helps?
The devise stored_location_for fixed it for me ,found the instance method here http://www.rubydoc.info/github/plataformatec/devise/Devise/Controllers/StoreLocation
module Accessible
extend ActiveSupport::Concern
included do
before_action :check_user
end
protected
def check_user
if current_tenant
redirect_to stored_location_for(:tenant) ||
authenticated_tenant_root_url && return
elsif current_user
redirect_to stored_location_for(:user) ||
authenticated_user_root_url && return
end
end
end

API signin to generate token using devise in rails 4

I am implementing api via rails.
I want to implement following feature but unable to figure out how?
I tried this sample app
I have user model with email, password and access_token
class UsersController < ApplicationController
def signin
c_user = User.find_by_email(params[:email])
pass = params[:password]
if c_user.password == pass
render json: c_user.access_token
end
end
private
def users_params
params.require(:user).permit(:email, :password)
end
end
If user request via api for http://localhost:3000/signin?email=t1#t.com&password=password
then it will check email and password and return access_token that user can use for future request.
I want to implement same with devise or any other gem please help me to understand it.
Thanks in advance
This is how I emplement such mechanism in my apps:
Generate an access_token whenever a user is created.
Respond with that access_token whenever the user signs in.
Require an access_token authentication for every request needed.
user.rb
class User < ActiveRecord::Base
# Use this before callback to set up User access_token.
before_save :ensure_authentication_token
# If the user has no access_token, generate one.
def ensure_authentication_token
if access_token.blank?
self.access_token = generate_access_token
end
end
private
def generate_access_token
loop do
token = Devise.friendly_token
break token unless User.where(access_token: token).first
end
end
end
application_controller.rb
class ApplicationController < ActionController::Base
private
# To make authentication mechanism more safe,
# require an access_token and a user_email.
def authenticate_user_from_token!
user_email = params[:user_email].presence
user = user_email && User.find_by_email(user_email)
# Use Devise.secure_compare to compare the access_token
# in the database with the access_token given in the params.
if user && Devise.secure_compare(user.access_token, params[:access_token])
# Passing store false, will not store the user in the session,
# so an access_token is needed for every request.
# If you want the access_token to work as a sign in token,
# you can simply remove store: false.
sign_in user, store: false
end
end
end
Then you can use this before_filter in any controller you want to protect with access_token authentication:
before_filter :authenticate_user_from_token!
You also needs to override Devise sessions controller, so it responds with a JSON holding the access_token.
users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
# Disable CSRF protection
skip_before_action :verify_authenticity_token
# Be sure to enable JSON.
respond_to :html, :json
# POST /resource/sign_in
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in) if is_flashing_format?
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource) do |format|
format.json { render json: {user_email: resource.email, access_token: resource.access_token} }
end
end
end
And make sure to specify it in your routes:
routes.rb
devise_for :users, controllers: { sessions: 'users/sessions' }
I think the reason you can't do that is because there is no password field as you were expected. There is only encrypted_password in the table.
And you must not save any user's password explicitly in a field like password for security reasons.
The way you can make your api work is by using valid_password? method provided by devise to authenticate the user.
if c_user.valid_password?(params[:password])
... ...
end

Check if user is active before allowing user to sign in with devise (rails)

I am using devise and created a User field called :active which is either true or false. I have to manually make the user active (true) before the user is allowed to log in. At least that is the intent. I have tried this...
class SessionsController < Devise::SessionsController
# POST /resource/sign_in
def create
"resource/signin CREATE"
self.resource = warden.authenticate!(auth_options)
unless resource.active?
sign_out
redirect_to :sorry_not_active_url
return
end
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
respond_with resource, :location => after_sign_in_path_for(resource)
end
end
However this does not catch all the places where a user can log in, for example, when a user changes their password, the site automatically logs them in automatically after. However, if the user is not active, I do not want them to be allowed to log in, but rather be redirected to a sorry_not_active_url.
What would be the best way to prevent the user from signing in if the user is not active?
Thank you.
Add these two methods to your user model, devise should pick them up automatically - you should NOT need to extend Devise::SessionsController
def active_for_authentication?
super && self.your_method_for_checking_active # i.e. super && self.is_active
end
def inactive_message
"Sorry, this account has been deactivated."
end
Devise (If you have devise 3.2+) now support block parameter in (session) create
# assuming this is your session controller
class SessionsController < Devise::SessionsController
def create
super do |resource|
unless resource.active?
sign_out
# you can set flash message as well.
redirect_to :sorry_not_active_url
return
end
end
end

Override Devise Sign-in with reCaptcha

I'm trying to override the Rails devise login to include recaptcha. I followed the steps here
http://presentations.royvandewater.com/authentication-with-devise.html#8
however for some reason, authentication always fails. To isolate the problem, I removed all my code and called super directly
class SessionsController < Devise::SessionsController
def create
super
end
end
file is at: Rails.root/app/controllers/sessions_controller.rb the slide suggest Rails.root/app/controllers/sessions.rb but I assume that was just a mistake. Trying it out didn't help either.
I even copied the full Sessions Controller code into my own, still gives the problem. Authentication fails here specifically:
resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
Any idea what I might be doing wrong?
Not sure what you could be doing wrong - not sure if this will help, but here's some other resources for setting up recaptcha with devise:
https://github.com/plataformatec/devise/wiki/How-To:-Use-Recaptcha-with-Devise
recaptcha:
https://github.com/ambethia/recaptcha/
Here's how to do it on creation. I imagine it's a similar approach for sign on, just override sessions controller like you are doing instead of registrations controller
class Users::RegistrationsController < Devise::RegistrationsController
def create
if session[:omniauth] == nil #OmniAuth
if verify_recaptcha
super
session[:omniauth] = nil unless #user.new_record? #OmniAuth
else
build_resource
clean_up_passwords(resource)
flash[:alert] = "There was an error with the recaptcha code below. Please re-enter the code and click submit."
render_with_scope :new
end
else
super
session[:omniauth] = nil unless #user.new_record? #OmniAuth
end
end

Resources