Skip to content

Commit d5820a2

Browse files
committed
Merge pull request #54 from alphagov/rememberable-tfa
Add support for remembering a user's 2FA session in a cookie
2 parents d1d4fe3 + 8d4da3b commit d5820a2

File tree

8 files changed

+67
-10
lines changed

8 files changed

+67
-10
lines changed

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* configure max login attempts
1010
* per user level control if he really need two factor authentication
1111
* your own sms logic
12+
* configurable period where users won't be asked for 2FA again
1213

1314
## Configuration
1415

@@ -38,12 +39,13 @@ Add the following line to your model to fully enable two-factor auth:
3839

3940
has_one_time_password
4041

41-
Set config values, if desired, for maximum second factor attempts count, allowed time drift, and OTP length.
42+
Set config values, if desired:
4243

4344
```ruby
44-
config.max_login_attempts = 3
45-
config.allowed_otp_drift_seconds = 30
46-
config.otp_length = 6
45+
config.max_login_attempts = 3 # Maximum second factor attempts count
46+
config.allowed_otp_drift_seconds = 30 # Allowed time drift
47+
config.otp_length = 6 # OTP code length
48+
config.remember_otp_session_for_seconds = 30.days # Time before browser has to enter OTP code again
4749
```
4850

4951
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:
6769

6870
has_one_time_password
6971

70-
Set config values, if desired, for maximum second factor attempts count, allowed time drift, and OTP length.
72+
Set config values, if desired:
7173

7274
```ruby
73-
config.max_login_attempts = 3
74-
config.allowed_otp_drift_seconds = 30
75-
config.otp_length = 6
75+
config.max_login_attempts = 3 # Maximum second factor attempts count
76+
config.allowed_otp_drift_seconds = 30 # Allowed time drift
77+
config.otp_length = 6 # OTP code length
78+
config.remember_otp_session_for_seconds = 30.days # Time before browser has to enter OTP code again
7679
```
7780

7881
Override the method to send one-time passwords in your model, this is automatically called when a user logs in:

app/controllers/devise/two_factor_authentication_controller.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ def update
99
render :show and return if params[:code].nil?
1010

1111
if resource.authenticate_otp(params[:code])
12+
expires_seconds = resource.class.remember_otp_session_for_seconds
13+
if expires_seconds && expires_seconds > 0
14+
cookies.signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] = {
15+
value: true,
16+
expires: expires_seconds.from_now
17+
}
18+
end
1219
warden.session(resource_name)[TwoFactorAuthentication::NEED_AUTHENTICATION] = false
1320
sign_in resource_name, resource, :bypass => true
1421
set_flash_message :notice, :success

lib/two_factor_authentication.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ module Devise
1616

1717
mattr_accessor :otp_length
1818
@@otp_length = 6
19+
20+
mattr_accessor :remember_otp_session_for_seconds
21+
@@remember_otp_session_for_seconds = 0
1922
end
2023

2124
module TwoFactorAuthentication
2225
NEED_AUTHENTICATION = 'need_two_factor_authentication'
26+
REMEMBER_TFA_COOKIE_NAME = "remember_tfa"
2327

2428
autoload :Schema, 'two_factor_authentication/schema'
2529
module Controllers

lib/two_factor_authentication/hooks/two_factor_authenticatable.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Warden::Manager.after_authentication do |user, auth, options|
2-
if user.respond_to?(:need_two_factor_authentication?)
2+
if user.respond_to?(:need_two_factor_authentication?) &&
3+
!auth.env["action_dispatch.cookies"].signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME]
34
if auth.session(options[:scope])[TwoFactorAuthentication::NEED_AUTHENTICATION] = user.need_two_factor_authentication?(auth.request)
45
user.send_two_factor_authentication_code
56
end

lib/two_factor_authentication/models/two_factor_authenticatable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def self.attributes_protected_by_default #:nodoc:
2020
end
2121
end
2222
end
23-
::Devise::Models.config(self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length)
23+
::Devise::Models.config(self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length, :remember_otp_session_for_seconds)
2424
end
2525

2626
module InstanceMethodsOnActivation

spec/features/two_factor_authenticatable_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,43 @@
8484
expect(page).to have_content("Access completely denied")
8585
expect(page).to have_content("You are signed out")
8686
end
87+
88+
describe "rememberable TFA" do
89+
before do
90+
@original_remember_otp_session_for_seconds = User.remember_otp_session_for_seconds
91+
User.remember_otp_session_for_seconds = 30.days
92+
end
93+
94+
after do
95+
User.remember_otp_session_for_seconds = @original_remember_otp_session_for_seconds
96+
end
97+
98+
scenario "doesn't require TFA code again within 30 days" do
99+
visit user_two_factor_authentication_path
100+
fill_in "code", with: user.otp_code
101+
click_button "Submit"
102+
103+
logout
104+
105+
login_as user
106+
visit dashboard_path
107+
expect(page).to have_content("Your Personal Dashboard")
108+
expect(page).to have_content("You are signed in as Marissa")
109+
end
110+
111+
scenario "requires TFA code again after 30 days" do
112+
visit user_two_factor_authentication_path
113+
fill_in "code", with: user.otp_code
114+
click_button "Submit"
115+
116+
logout
117+
118+
Timecop.travel(30.days.from_now)
119+
login_as user
120+
visit dashboard_path
121+
expect(page).to have_content("You are signed in as Marissa")
122+
expect(page).to have_content("Enter your personal code")
123+
end
124+
end
87125
end
88126
end

spec/spec_helper.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require File.expand_path("../rails_app/config/environment.rb", __FILE__)
33

44
require 'rspec/rails'
5+
require 'timecop'
56

67
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
78
RSpec.configure do |config|
@@ -17,6 +18,8 @@
1718
# the seed, which is printed after each run.
1819
# --seed 1234
1920
config.order = 'random'
21+
22+
config.after(:each) { Timecop.return }
2023
end
2124

2225
Dir["#{Dir.pwd}/spec/support/**/*.rb"].each {|f| require f}

two_factor_authentication.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ Gem::Specification.new do |s|
3434
s.add_development_dependency 'rspec-rails', '>= 3.0.1'
3535
s.add_development_dependency 'capybara', '2.4.1'
3636
s.add_development_dependency 'pry'
37+
s.add_development_dependency 'timecop'
3738
end

0 commit comments

Comments
 (0)