Browse Source

Add support for remembering a user's 2FA session in a cookie

This makes the gem store a signed cookie for a configurable amount of
time that allows the user to bypass 2FA. Our use-case for this is that
we expire user’s Devise sessions after 12 hours, but don’t want to
force them to authenticate using 2FA every day.

Signed cookies are available since Rails 3. This requires the signing
functionality to be properly configured, but is disabled by setting the
config variable to `0`, the default.
master
Paul Bowsher 9 years ago
parent
commit
8d4da3beb5
  1. 19
      README.md
  2. 7
      app/controllers/devise/two_factor_authentication_controller.rb
  3. 4
      lib/two_factor_authentication.rb
  4. 3
      lib/two_factor_authentication/hooks/two_factor_authenticatable.rb
  5. 2
      lib/two_factor_authentication/models/two_factor_authenticatable.rb
  6. 38
      spec/features/two_factor_authenticatable_spec.rb
  7. 3
      spec/spec_helper.rb
  8. 1
      two_factor_authentication.gemspec

19
README.md

@ -9,6 +9,7 @@
* configure max login attempts
* per user level control if he really need two factor authentication
* your own sms logic
* configurable period where users won't be asked for 2FA again
## Configuration
@ -38,12 +39,13 @@ Add the following line to your model to fully enable two-factor auth:
has_one_time_password
Set config values, if desired, for maximum second factor attempts count, allowed time drift, and OTP length.
Set config values, if desired:
```ruby
config.max_login_attempts = 3
config.allowed_otp_drift_seconds = 30
config.otp_length = 6
config.max_login_attempts = 3 # Maximum second factor attempts count
config.allowed_otp_drift_seconds = 30 # Allowed time drift
config.otp_length = 6 # OTP code length
config.remember_otp_session_for_seconds = 30.days # Time before browser has to enter OTP code again
```
Override the method to send one-time passwords in your model, this is automatically called when a user logs in:
@ -67,12 +69,13 @@ Add the following line to your model to fully enable two-factor auth:
has_one_time_password
Set config values, if desired, for maximum second factor attempts count, allowed time drift, and OTP length.
Set config values, if desired:
```ruby
config.max_login_attempts = 3
config.allowed_otp_drift_seconds = 30
config.otp_length = 6
config.max_login_attempts = 3 # Maximum second factor attempts count
config.allowed_otp_drift_seconds = 30 # Allowed time drift
config.otp_length = 6 # OTP code length
config.remember_otp_session_for_seconds = 30.days # Time before browser has to enter OTP code again
```
Override the method to send one-time passwords in your model, this is automatically called when a user logs in:

7
app/controllers/devise/two_factor_authentication_controller.rb

@ -9,6 +9,13 @@ class Devise::TwoFactorAuthenticationController < DeviseController
render :show and return if params[:code].nil?
if resource.authenticate_otp(params[:code])
expires_seconds = resource.class.remember_otp_session_for_seconds
if expires_seconds && expires_seconds > 0
cookies.signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] = {
value: true,
expires: expires_seconds.from_now
}
end
warden.session(resource_name)[TwoFactorAuthentication::NEED_AUTHENTICATION] = false
sign_in resource_name, resource, :bypass => true
set_flash_message :notice, :success

4
lib/two_factor_authentication.rb

@ -16,10 +16,14 @@ module Devise
mattr_accessor :otp_length
@@otp_length = 6
mattr_accessor :remember_otp_session_for_seconds
@@remember_otp_session_for_seconds = 0
end
module TwoFactorAuthentication
NEED_AUTHENTICATION = 'need_two_factor_authentication'
REMEMBER_TFA_COOKIE_NAME = "remember_tfa"
autoload :Schema, 'two_factor_authentication/schema'
module Controllers

3
lib/two_factor_authentication/hooks/two_factor_authenticatable.rb

@ -1,5 +1,6 @@
Warden::Manager.after_authentication do |user, auth, options|
if user.respond_to?(:need_two_factor_authentication?)
if user.respond_to?(:need_two_factor_authentication?) &&
!auth.env["action_dispatch.cookies"].signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME]
if auth.session(options[:scope])[TwoFactorAuthentication::NEED_AUTHENTICATION] = user.need_two_factor_authentication?(auth.request)
user.send_two_factor_authentication_code
end

2
lib/two_factor_authentication/models/two_factor_authenticatable.rb

@ -20,7 +20,7 @@ module Devise
end
end
end
::Devise::Models.config(self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length)
::Devise::Models.config(self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length, :remember_otp_session_for_seconds)
end
module InstanceMethodsOnActivation

38
spec/features/two_factor_authenticatable_spec.rb

@ -84,5 +84,43 @@ feature "User of two factor authentication" do
expect(page).to have_content("Access completely denied")
expect(page).to have_content("You are signed out")
end
describe "rememberable TFA" do
before do
@original_remember_otp_session_for_seconds = User.remember_otp_session_for_seconds
User.remember_otp_session_for_seconds = 30.days
end
after do
User.remember_otp_session_for_seconds = @original_remember_otp_session_for_seconds
end
scenario "doesn't require TFA code again within 30 days" do
visit user_two_factor_authentication_path
fill_in "code", with: user.otp_code
click_button "Submit"
logout
login_as user
visit dashboard_path
expect(page).to have_content("Your Personal Dashboard")
expect(page).to have_content("You are signed in as Marissa")
end
scenario "requires TFA code again after 30 days" do
visit user_two_factor_authentication_path
fill_in "code", with: user.otp_code
click_button "Submit"
logout
Timecop.travel(30.days.from_now)
login_as user
visit dashboard_path
expect(page).to have_content("You are signed in as Marissa")
expect(page).to have_content("Enter your personal code")
end
end
end
end

3
spec/spec_helper.rb

@ -2,6 +2,7 @@ ENV["RAILS_ENV"] ||= "test"
require File.expand_path("../rails_app/config/environment.rb", __FILE__)
require 'rspec/rails'
require 'timecop'
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
@ -17,6 +18,8 @@ RSpec.configure do |config|
# the seed, which is printed after each run.
# --seed 1234
config.order = 'random'
config.after(:each) { Timecop.return }
end
Dir["#{Dir.pwd}/spec/support/**/*.rb"].each {|f| require f}

1
two_factor_authentication.gemspec

@ -34,4 +34,5 @@ Gem::Specification.new do |s|
s.add_development_dependency 'rspec-rails', '>= 3.0.1'
s.add_development_dependency 'capybara', '2.4.1'
s.add_development_dependency 'pry'
s.add_development_dependency 'timecop'
end

Loading…
Cancel
Save