two_factor_authentication 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +2 -0
- data/Gemfile +4 -0
- data/README.md +12 -9
- data/app/controllers/devise/two_factor_authentication_controller.rb +47 -23
- data/lib/two_factor_authentication.rb +4 -1
- data/lib/two_factor_authentication/controllers/helpers.rb +1 -1
- data/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb +2 -1
- data/lib/two_factor_authentication/models/two_factor_authenticatable.rb +3 -1
- data/lib/two_factor_authentication/version.rb +1 -1
- data/spec/features/two_factor_authenticatable_spec.rb +38 -0
- data/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +1 -1
- data/spec/rails_app/config/environments/test.rb +7 -1
- data/spec/spec_helper.rb +3 -0
- data/two_factor_authentication.gemspec +1 -0
- metadata +37 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d9dcc7b4346ba04374293811dba157adfd55db2
|
4
|
+
data.tar.gz: aaa9d10892c25956248fa6c2325c0632e34f4cf3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5ac4fa24344211e03465875098f6fb5a09af71335747a9972829f5f304c8b10b12037fd581a47de6ad973cc562dba29c110ca3171d833b8d08d9fb8bd7faec9
|
7
|
+
data.tar.gz: 8ce9ce6d83afafdd9014f7cf7c4aef87a0409a54f193a1943c3b071a0e86e3a60b93fe9a58e06beb380ecb42857988c521132fc4b44d16a926c8fbe2c729367c
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -9,6 +9,7 @@
|
|
9
9
|
* configure max login attempts
|
10
10
|
* per user level control if he really need two factor authentication
|
11
11
|
* your own sms logic
|
12
|
+
* configurable period where users won't be asked for 2FA again
|
12
13
|
|
13
14
|
## Configuration
|
14
15
|
|
@@ -24,7 +25,7 @@ Once that's done, run:
|
|
24
25
|
|
25
26
|
### Automatic installation
|
26
27
|
|
27
|
-
In order to add two factor
|
28
|
+
In order to add two factor authentication to a model, run the command:
|
28
29
|
|
29
30
|
bundle exec rails g two_factor_authentication MODEL
|
30
31
|
|
@@ -38,12 +39,13 @@ Add the following line to your model to fully enable two-factor auth:
|
|
38
39
|
|
39
40
|
has_one_time_password
|
40
41
|
|
41
|
-
Set config values, if desired
|
42
|
+
Set config values, if desired:
|
42
43
|
|
43
44
|
```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
|
47
49
|
```
|
48
50
|
|
49
51
|
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:
|
|
67
69
|
|
68
70
|
has_one_time_password
|
69
71
|
|
70
|
-
Set config values, if desired
|
72
|
+
Set config values to devise.rb, if desired:
|
71
73
|
|
72
74
|
```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
|
76
79
|
```
|
77
80
|
|
78
81
|
Override the method to send one-time passwords in your model, this is automatically called when a user logs in:
|
@@ -9,36 +9,60 @@ class Devise::TwoFactorAuthenticationController < DeviseController
|
|
9
9
|
render :show and return if params[:code].nil?
|
10
10
|
|
11
11
|
if resource.authenticate_otp(params[:code])
|
12
|
-
|
13
|
-
sign_in resource_name, resource, :bypass => true
|
14
|
-
set_flash_message :notice, :success
|
15
|
-
redirect_to stored_location_for(resource_name) || :root
|
16
|
-
resource.update_attribute(:second_factor_attempts_count, 0)
|
12
|
+
after_two_factor_success_for(resource)
|
17
13
|
else
|
18
|
-
resource
|
19
|
-
resource.save
|
20
|
-
flash.now[:error] = find_message(:attempt_failed)
|
21
|
-
if resource.max_login_attempts?
|
22
|
-
sign_out(resource)
|
23
|
-
render :max_login_attempts_reached
|
24
|
-
else
|
25
|
-
render :show
|
26
|
-
end
|
14
|
+
after_two_factor_fail_for(resource)
|
27
15
|
end
|
28
16
|
end
|
29
17
|
|
30
18
|
private
|
31
19
|
|
32
|
-
|
33
|
-
|
20
|
+
def after_two_factor_success_for(resource)
|
21
|
+
expires_seconds = resource.class.remember_otp_session_for_seconds
|
22
|
+
|
23
|
+
if expires_seconds && expires_seconds > 0
|
24
|
+
cookies.signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] = {
|
25
|
+
value: true,
|
26
|
+
expires: expires_seconds.from_now
|
27
|
+
}
|
34
28
|
end
|
35
29
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
30
|
+
warden.session(resource_name)[TwoFactorAuthentication::NEED_AUTHENTICATION] = false
|
31
|
+
sign_in resource_name, resource, :bypass => true
|
32
|
+
set_flash_message :notice, :success
|
33
|
+
resource.update_attribute(:second_factor_attempts_count, 0)
|
34
|
+
|
35
|
+
redirect_to after_two_factor_success_path_for(resource)
|
36
|
+
end
|
37
|
+
|
38
|
+
def after_two_factor_success_path_for(resource)
|
39
|
+
stored_location_for(resource_name) || :root
|
40
|
+
end
|
41
|
+
|
42
|
+
def after_two_factor_fail_for(resource)
|
43
|
+
resource.second_factor_attempts_count += 1
|
44
|
+
resource.save
|
45
|
+
flash.now[:error] = find_message(:attempt_failed)
|
46
|
+
|
47
|
+
if resource.max_login_attempts?
|
48
|
+
sign_out(resource)
|
49
|
+
render :max_login_attempts_reached
|
50
|
+
|
51
|
+
else
|
52
|
+
render :show
|
43
53
|
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def authenticate_scope!
|
57
|
+
self.resource = send("current_#{resource_name}")
|
58
|
+
end
|
59
|
+
|
60
|
+
def prepare_and_validate
|
61
|
+
redirect_to :root and return if resource.nil?
|
62
|
+
@limit = resource.max_login_attempts
|
63
|
+
if resource.max_login_attempts?
|
64
|
+
sign_out(resource)
|
65
|
+
render :max_login_attempts_reached and return
|
66
|
+
end
|
67
|
+
end
|
44
68
|
end
|
@@ -5,7 +5,6 @@ require "active_model"
|
|
5
5
|
require "active_record"
|
6
6
|
require "active_support/core_ext/class/attribute_accessors"
|
7
7
|
require "cgi"
|
8
|
-
require "rotp"
|
9
8
|
|
10
9
|
module Devise
|
11
10
|
mattr_accessor :max_login_attempts
|
@@ -16,10 +15,14 @@ module Devise
|
|
16
15
|
|
17
16
|
mattr_accessor :otp_length
|
18
17
|
@@otp_length = 6
|
18
|
+
|
19
|
+
mattr_accessor :remember_otp_session_for_seconds
|
20
|
+
@@remember_otp_session_for_seconds = 0
|
19
21
|
end
|
20
22
|
|
21
23
|
module TwoFactorAuthentication
|
22
24
|
NEED_AUTHENTICATION = 'need_two_factor_authentication'
|
25
|
+
REMEMBER_TFA_COOKIE_NAME = "remember_tfa"
|
23
26
|
|
24
27
|
autoload :Schema, 'two_factor_authentication/schema'
|
25
28
|
module Controllers
|
@@ -21,7 +21,7 @@ module TwoFactorAuthentication
|
|
21
21
|
|
22
22
|
def handle_failed_second_factor(scope)
|
23
23
|
if request.format.present? and request.format.html?
|
24
|
-
session["#{scope}_return_to"] =
|
24
|
+
session["#{scope}_return_to"] = request.original_fullpath if request.get?
|
25
25
|
redirect_to two_factor_authentication_path_for(scope)
|
26
26
|
else
|
27
27
|
render nothing: true, status: :unauthorized
|
@@ -1,5 +1,6 @@
|
|
1
1
|
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]
|
3
4
|
if auth.session(options[:scope])[TwoFactorAuthentication::NEED_AUTHENTICATION] = user.need_two_factor_authentication?(auth.request)
|
4
5
|
user.send_two_factor_authentication_code
|
5
6
|
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'two_factor_authentication/hooks/two_factor_authenticatable'
|
2
|
+
require 'rotp'
|
3
|
+
|
2
4
|
module Devise
|
3
5
|
module Models
|
4
6
|
module TwoFactorAuthenticatable
|
@@ -20,7 +22,7 @@ module Devise
|
|
20
22
|
end
|
21
23
|
end
|
22
24
|
end
|
23
|
-
::Devise::Models.config(self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length)
|
25
|
+
::Devise::Models.config(self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length, :remember_otp_session_for_seconds)
|
24
26
|
end
|
25
27
|
|
26
28
|
module InstanceMethodsOnActivation
|
@@ -84,5 +84,43 @@ feature "User of two factor authentication" do
|
|
84
84
|
expect(page).to have_content("Access completely denied")
|
85
85
|
expect(page).to have_content("You are signed out")
|
86
86
|
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
|
87
125
|
end
|
88
126
|
end
|
@@ -9,7 +9,13 @@ Dummy::Application.configure do
|
|
9
9
|
config.eager_load = false
|
10
10
|
|
11
11
|
# Configure static asset server for tests with Cache-Control for performance
|
12
|
-
|
12
|
+
if Rails::VERSION::MAJOR == 4 && Rails::VERSION::MINOR >= 2 ||
|
13
|
+
Rails::VERSION::MAJOR >= 5
|
14
|
+
config.serve_static_files = true
|
15
|
+
else
|
16
|
+
config.serve_static_assets = true
|
17
|
+
end
|
18
|
+
|
13
19
|
config.static_cache_control = "public, max-age=3600"
|
14
20
|
|
15
21
|
# Show full error reports and disable caching
|
data/spec/spec_helper.rb
CHANGED
@@ -2,6 +2,7 @@ ENV["RAILS_ENV"] ||= "test"
|
|
2
2
|
require File.expand_path("../rails_app/config/environment.rb", __FILE__)
|
3
3
|
|
4
4
|
require 'rspec/rails'
|
5
|
+
require 'timecop'
|
5
6
|
|
6
7
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
8
|
RSpec.configure do |config|
|
@@ -17,6 +18,8 @@ RSpec.configure do |config|
|
|
17
18
|
# the seed, which is printed after each run.
|
18
19
|
# --seed 1234
|
19
20
|
config.order = 'random'
|
21
|
+
|
22
|
+
config.after(:each) { Timecop.return }
|
20
23
|
end
|
21
24
|
|
22
25
|
Dir["#{Dir.pwd}/spec/support/**/*.rb"].each {|f| require f}
|
metadata
CHANGED
@@ -1,111 +1,111 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: two_factor_authentication
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dmitrii Golub
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: 3.1.1
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 3.1.1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: devise
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: randexp
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rotp
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: bundler
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: rake
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- -
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
89
|
version: '0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- -
|
94
|
+
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: rspec-rails
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- -
|
101
|
+
- - ">="
|
102
102
|
- !ruby/object:Gem::Version
|
103
103
|
version: 3.0.1
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- -
|
108
|
+
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: 3.0.1
|
111
111
|
- !ruby/object:Gem::Dependency
|
@@ -126,14 +126,28 @@ dependencies:
|
|
126
126
|
name: pry
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
|
-
- -
|
129
|
+
- - ">="
|
130
130
|
- !ruby/object:Gem::Version
|
131
131
|
version: '0'
|
132
132
|
type: :development
|
133
133
|
prerelease: false
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
135
135
|
requirements:
|
136
|
-
- -
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: timecop
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
137
151
|
- !ruby/object:Gem::Version
|
138
152
|
version: '0'
|
139
153
|
description: |2
|
@@ -148,8 +162,8 @@ executables: []
|
|
148
162
|
extensions: []
|
149
163
|
extra_rdoc_files: []
|
150
164
|
files:
|
151
|
-
- .gitignore
|
152
|
-
- .travis.yml
|
165
|
+
- ".gitignore"
|
166
|
+
- ".travis.yml"
|
153
167
|
- Gemfile
|
154
168
|
- LICENSE
|
155
169
|
- README.md
|
@@ -234,17 +248,17 @@ require_paths:
|
|
234
248
|
- lib
|
235
249
|
required_ruby_version: !ruby/object:Gem::Requirement
|
236
250
|
requirements:
|
237
|
-
- -
|
251
|
+
- - ">="
|
238
252
|
- !ruby/object:Gem::Version
|
239
253
|
version: '0'
|
240
254
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
241
255
|
requirements:
|
242
|
-
- -
|
256
|
+
- - ">="
|
243
257
|
- !ruby/object:Gem::Version
|
244
258
|
version: '0'
|
245
259
|
requirements: []
|
246
260
|
rubyforge_project: two_factor_authentication
|
247
|
-
rubygems_version: 2.
|
261
|
+
rubygems_version: 2.5.0
|
248
262
|
signing_key:
|
249
263
|
specification_version: 4
|
250
264
|
summary: Two factor authentication plugin for devise
|