Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 8, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ def update
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
Expand Down
4 changes: 4 additions & 0 deletions lib/two_factor_authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def self.attributes_protected_by_default #:nodoc:
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
Expand Down
38 changes: 38 additions & 0 deletions spec/features/two_factor_authenticatable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,43 @@
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 changes: 3 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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|
Expand All @@ -17,6 +18,8 @@
# 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 change: 1 addition & 0 deletions two_factor_authentication.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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