Skip to content

Instantly share code, notes, and snippets.

@aganov
Created March 19, 2019 16:26
Show Gist options
  • Save aganov/1e5bb3b67540c4337b3a3590c1b11a38 to your computer and use it in GitHub Desktop.
Save aganov/1e5bb3b67540c4337b3a3590c1b11a38 to your computer and use it in GitHub Desktop.
devise two-factor two steps
# frozen_string_literal: true
# Controller concern to handle two-factor authentication
# Upon inclusion, skips `require_no_authentication` on `:create`.
module AuthenticatesWithTwoFactor
extend ActiveSupport::Concern
included do
# This action comes from DeviseController, but because we call `sign_in`
# manually, not skipping this action would cause a "You are already signed
# in." error message to be shown upon successful login.
skip_before_action :require_no_authentication, only: [:create], raise: false
before_action :reset_otp_user_id, only: [:new]
prepend_before_action :authenticate_with_two_factor,
if: :otp_required_for_login?, only: [:create]
end
private
def find_user
if session[:otp_user_id]
User.find(session[:otp_user_id])
elsif sign_in_params[:email]
User.find_by(email: sign_in_params[:email])
end
end
def reset_otp_user_id
session.delete(:otp_user_id)
end
def otp_required_for_login?
find_user.try :otp_required_for_login?
end
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(sign_in_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(sign_in_params[:otp_attempt])
end
# Store the user's ID in the session for later retrieval and render the
# two factor code prompt
#
# The user must have been authenticated with a valid login and password
# before calling this method!
def prompt_for_two_factor(user)
self.resource = user
session[:otp_user_id] = user.id
render "devise/sessions/two_factor"
end
def authenticate_with_two_factor
self.resource = find_user
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
if sign_in_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(resource)
elsif resource && resource.valid_password?(resource_params[:password])
prompt_for_two_factor(resource)
end
end
def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user)
reset_otp_user_id # Remove any lingering user data from login
sign_in(resource_name, resource)
else
flash.now[:alert] = t("devise.failure.invalid_opt_attempt")
prompt_for_two_factor(user)
end
end
end
# frozen_string_literal: true
Rails.application.routes.draw do
devise_for :users, controllers: { sessions: :sessions }
end
# frozen_string_literal: true
class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor
end
- if resource.otp_required_for_login?
= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
.form-inputs
= f.input :otp_attempt, required: false, label: false, autofocus: true
.form-actions
= f.button :submit
= render "devise/shared/links"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment