Browse Source

Merge pull request #54 from alphagov/rememberable-tfa

Add support for remembering a user's 2FA session in a cookie
master
Dmitrii Golub 9 years ago
parent
commit
d5820a2f3d
  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