diff --git a/README.md b/README.md index b56eae3c..d6c9c88d 100644 --- a/README.md +++ b/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: diff --git a/app/controllers/devise/two_factor_authentication_controller.rb b/app/controllers/devise/two_factor_authentication_controller.rb index e302e01e..deed574a 100644 --- a/app/controllers/devise/two_factor_authentication_controller.rb +++ b/app/controllers/devise/two_factor_authentication_controller.rb @@ -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 diff --git a/lib/two_factor_authentication.rb b/lib/two_factor_authentication.rb index b976721b..1f7b342f 100644 --- a/lib/two_factor_authentication.rb +++ b/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 diff --git a/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb b/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb index 985dc345..5b479c4f 100644 --- a/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb +++ b/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 diff --git a/lib/two_factor_authentication/models/two_factor_authenticatable.rb b/lib/two_factor_authentication/models/two_factor_authenticatable.rb index 8e3e6973..dcfc1ae4 100644 --- a/lib/two_factor_authentication/models/two_factor_authenticatable.rb +++ b/lib/two_factor_authentication/models/two_factor_authenticatable.rb @@ -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 diff --git a/spec/features/two_factor_authenticatable_spec.rb b/spec/features/two_factor_authenticatable_spec.rb index a1a424a0..466010b2 100644 --- a/spec/features/two_factor_authenticatable_spec.rb +++ b/spec/features/two_factor_authenticatable_spec.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bed93aad..32803c50 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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| @@ -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} diff --git a/two_factor_authentication.gemspec b/two_factor_authentication.gemspec index 32bbba3f..148dd0a6 100644 --- a/two_factor_authentication.gemspec +++ b/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