devise-otp 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +0 -2
- data/.gitignore +3 -1
- data/CHANGELOG.md +51 -8
- data/README.md +8 -2
- data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +46 -27
- data/app/controllers/devise_otp/devise/otp_tokens_controller.rb +24 -6
- data/app/views/devise/otp_credentials/show.html.erb +6 -6
- data/app/views/devise/otp_tokens/_token_secret.html.erb +3 -4
- data/app/views/devise/otp_tokens/edit.html.erb +26 -0
- data/app/views/devise/otp_tokens/show.html.erb +7 -14
- data/config/locales/en.yml +23 -14
- data/devise-otp.gemspec +1 -2
- data/lib/devise/strategies/database_authenticatable.rb +64 -0
- data/lib/devise-otp/version.rb +1 -1
- data/lib/devise-otp.rb +31 -11
- data/lib/devise_otp_authenticatable/controllers/helpers.rb +9 -10
- data/lib/devise_otp_authenticatable/controllers/public_helpers.rb +39 -0
- data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +10 -0
- data/lib/devise_otp_authenticatable/engine.rb +2 -5
- data/lib/devise_otp_authenticatable/hooks/refreshable.rb +5 -0
- data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +22 -20
- data/lib/devise_otp_authenticatable/routes.rb +3 -1
- data/test/dummy/app/controllers/admin_posts_controller.rb +85 -0
- data/test/dummy/app/controllers/application_controller.rb +0 -1
- data/test/dummy/app/controllers/base_controller.rb +6 -0
- data/test/dummy/app/models/admin.rb +25 -0
- data/test/dummy/app/views/admin_posts/_form.html.erb +25 -0
- data/test/dummy/app/views/admin_posts/edit.html.erb +6 -0
- data/test/dummy/app/views/admin_posts/index.html.erb +25 -0
- data/test/dummy/app/views/admin_posts/new.html.erb +5 -0
- data/test/dummy/app/views/admin_posts/show.html.erb +15 -0
- data/test/dummy/app/views/base/home.html.erb +1 -0
- data/test/dummy/config/application.rb +0 -2
- data/test/dummy/config/routes.rb +4 -1
- data/test/dummy/db/migrate/20240604000001_create_admins.rb +9 -0
- data/test/dummy/db/migrate/20240604000002_add_devise_to_admins.rb +52 -0
- data/test/dummy/db/migrate/20240604000003_devise_otp_add_to_admins.rb +28 -0
- data/test/integration/disable_token_test.rb +53 -0
- data/test/integration/enable_otp_form_test.rb +57 -0
- data/test/integration/persistence_test.rb +3 -6
- data/test/integration/refresh_test.rb +32 -0
- data/test/integration/reset_token_test.rb +45 -0
- data/test/integration/sign_in_test.rb +10 -14
- data/test/integration/trackable_test.rb +50 -0
- data/test/integration_tests_helper.rb +24 -6
- data/test/models/otp_authenticatable_test.rb +62 -27
- data/test/test_helper.rb +1 -71
- metadata +26 -23
- data/lib/devise_otp_authenticatable/hooks/sessions.rb +0 -58
- data/lib/devise_otp_authenticatable/hooks.rb +0 -11
- data/test/integration/token_test.rb +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ab31b9f7027a1468e535a1ac862c26cfc95891bdf10a618115a66767e1c5d16
|
4
|
+
data.tar.gz: c603692b0be3e3fbd22d4296299ccd0d5b9a42a1f9ab1f4d6cd7186ea3a864a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7668d8f50ee7704c7b18eff92c16384eb47186ebf646260391686addd2acc9a06f3dcb92ffd7a70f87ec20106acbaa8e2e1cda4d6e1ebaabdc68ad27c80996b
|
7
|
+
data.tar.gz: 47ab6592ff48f37715e61f07e81813d75a99bd2d971a79642051b27fb14efd6de2dcddf71a6d579f4cbd8d0ed379044cb59e80da78683b2573e4cf0e47a1d329
|
data/.github/workflows/ci.yml
CHANGED
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,17 +1,60 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## 0.
|
3
|
+
## 0.7.0
|
4
4
|
|
5
5
|
Breaking changes:
|
6
6
|
|
7
|
-
-
|
8
|
-
-
|
9
|
-
-
|
7
|
+
- Require confirmation token before enabling Two Factor Authentication (2FA) to ensure that user has added OTP token properly to their device
|
8
|
+
- Update DeviseAuthenticatable to redirect user (rather than login user) when OTP is enabled
|
9
|
+
- Remove OtpAuthenticatable callbacks for setting OTP credentials on create action (no longer needed)
|
10
|
+
- Replace OtpAuthenticatable "reset_otp_credentials" methods with "clear_otp_fields!" method
|
11
|
+
- Update otp_tokens#edit to populate OTP secrets (rather than assuming they are populated via callbacks in OTPDeviseAuthenticatable module)
|
12
|
+
- Repurpose otp_tokens#destroy to disable 2FA and clear OTP secrets (rather than resetting them)
|
13
|
+
- Add reset token action and hide/repurpose disable token action
|
14
|
+
- Update disable action to preserve the existing token secret
|
15
|
+
- Hide button for mandatory OTP
|
16
|
+
- Add Refreshable hook, and tie into after\_set\_user calback
|
17
|
+
- Utilize native warden session for scoping of credentials\_refreshed\_at and refresh\_return\_url properties
|
18
|
+
- Require adding "ensure\_mandatory\_{scope}\_otp! to controllers for mandatory OTP
|
19
|
+
- Update locales to support the new workflow
|
10
20
|
|
11
|
-
|
21
|
+
### Upgrading
|
12
22
|
|
13
|
-
|
23
|
+
Regenerate your views with `rails g devise_otp:views` and update locales.
|
14
24
|
|
15
|
-
|
25
|
+
Changes to locales:
|
26
|
+
|
27
|
+
- Remove:
|
28
|
+
- otp_tokens.enable_request
|
29
|
+
- otp_tokens.status
|
30
|
+
- otp_tokens.submit
|
31
|
+
- Add to otp_tokens scope:
|
32
|
+
- enable_link
|
33
|
+
- Move/rename devise.otp.token_secret.reset_\* values to devise.otp.otp_tokens.disable_\* (for consistency with "enable_link")
|
34
|
+
- disable_link
|
35
|
+
- disable_explain
|
36
|
+
- disable_explain_warn
|
37
|
+
- Add to new edit_otp_token scope:
|
38
|
+
- title
|
39
|
+
- lead_in
|
40
|
+
- step1
|
41
|
+
- step2
|
42
|
+
- confirmation_code
|
43
|
+
- submit
|
44
|
+
- Move "explain" to new edit_otp_token scope
|
45
|
+
- Add devise.otp.otp_tokens.could_not_confirm
|
46
|
+
- Rename "successfully_reset_creds" to "successfully_disabled_otp"
|
47
|
+
|
48
|
+
You can grab the full locale file [here](https://github.com/wmlele/devise-otp/blob/master/config/locales/en.yml).
|
49
|
+
|
50
|
+
## 0.6.0
|
51
|
+
|
52
|
+
Improvements:
|
53
|
+
|
54
|
+
- support rails 6.1 by @cotcomsol in #67
|
55
|
+
|
56
|
+
Fixes:
|
57
|
+
|
58
|
+
- mandatory otp fix by @cotcomsol in #68
|
59
|
+
- remove success message by @strzibny in #69
|
16
60
|
|
17
|
-
A long awaited update bringing Devise::OTP from the dead!
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Devise::OTP
|
2
2
|
|
3
|
-
Devise OTP is a
|
3
|
+
Devise OTP is a Two-Factor Authentication extension for Devise. The second factor is done using an [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238) Time-Based One-Time Password (TOTP) implemented by the [rotp library](https://github.com/mdp/rotp).
|
4
4
|
|
5
5
|
It has the following features:
|
6
6
|
|
@@ -19,7 +19,7 @@ Device OTP was recently updated to work with Rails 7 and Turbo.
|
|
19
19
|
|
20
20
|
Devise::OTP development is sponsored by [Business Class](https://businessclasskit.com/) Rails SaaS starter kit. If you don't want to setup OTP yourself for your new project, consider starting one on Business Class.
|
21
21
|
|
22
|
-
## Two-
|
22
|
+
## Two-Factor Authentication using OTP
|
23
23
|
|
24
24
|
* A shared secret is generated on the server, and stored both on the token device (e.g. the phone) and the server itself.
|
25
25
|
* The secret is used to generate short numerical tokens that are either time or sequence based.
|
@@ -94,6 +94,12 @@ The install generator adds some options to the end of your Devise config file (`
|
|
94
94
|
* `config.otp_issuer`: The name of the token issuer, to be added to the provisioning url. Display will vary based on token application. (defaults to the Rails application class)
|
95
95
|
* `config.otp_controller_path`: The view path for Devise OTP controllers. The default being 'devise' to match Devise default installation.
|
96
96
|
|
97
|
+
## Mandatory OTP
|
98
|
+
Enforcing mandatory OTP requires adding the ensure\_mandatory\_{scope}\_otp! method to the desired controller(s) to ensure that the user is redirected to the Enable Two-Factor Authentication form before proceeding to other parts of the application. This functions the same way as the authenticate\_{scope}! methods, and can be included inline with them in the controllers, e.g.:
|
99
|
+
|
100
|
+
before_action :authenticate_user!
|
101
|
+
before_action :ensure_mandatory_user_otp!
|
102
|
+
|
97
103
|
## Authors
|
98
104
|
|
99
105
|
The project was originally started by Lele Forzani by forking [devise_google_authenticator](https://github.com/AsteriskLabs/devise_google_authenticator) and still contains some devise_google_authenticator code. It's now maintained by [Josef Strzibny](https://github.com/strzibny/).
|
@@ -5,45 +5,31 @@ module DeviseOtp
|
|
5
5
|
|
6
6
|
prepend_before_action :authenticate_scope!, only: [:get_refresh, :set_refresh]
|
7
7
|
prepend_before_action :require_no_authentication, only: [:show, :update]
|
8
|
+
before_action :set_challenge, only: [:show, :update]
|
9
|
+
before_action :set_recovery, only: [:show, :update]
|
10
|
+
before_action :set_resource, only: [:show, :update]
|
11
|
+
before_action :set_token, only: [:update]
|
12
|
+
before_action :skip_challenge_if_trusted_browser, only: [:show, :update]
|
8
13
|
|
9
14
|
#
|
10
15
|
# show a request for the OTP token
|
11
16
|
#
|
12
17
|
def show
|
13
|
-
@
|
14
|
-
|
15
|
-
|
16
|
-
if @challenge.nil?
|
17
|
-
redirect_to :root
|
18
|
-
else
|
19
|
-
self.resource = resource_class.find_valid_otp_challenge(@challenge)
|
20
|
-
if resource.nil?
|
21
|
-
redirect_to :root
|
22
|
-
elsif @recovery
|
23
|
-
@recovery_count = resource.otp_recovery_counter
|
24
|
-
render :show
|
25
|
-
else
|
26
|
-
render :show
|
27
|
-
end
|
18
|
+
if @recovery
|
19
|
+
@recovery_count = resource.otp_recovery_counter
|
28
20
|
end
|
21
|
+
|
22
|
+
render :show
|
29
23
|
end
|
30
24
|
|
31
25
|
#
|
32
26
|
# signs the resource in, if the OTP token is valid and the user has a valid challenge
|
33
27
|
#
|
34
28
|
def update
|
35
|
-
|
36
|
-
recovery = (params[resource_name][:recovery] == "true") && recovery_enabled?
|
37
|
-
token = params[resource_name][:token]
|
38
|
-
|
39
|
-
if token.blank?
|
29
|
+
if @token.blank?
|
40
30
|
otp_set_flash_message(:alert, :token_blank)
|
41
|
-
redirect_to otp_credential_path_for(resource_name, challenge:
|
42
|
-
|
43
|
-
elsif resource.nil?
|
44
|
-
otp_set_flash_message(:alert, :otp_session_invalid)
|
45
|
-
redirect_to new_session_path(resource_name)
|
46
|
-
elsif resource.otp_challenge_valid? && resource.validate_otp_token(params[resource_name][:token], recovery)
|
31
|
+
redirect_to otp_credential_path_for(resource_name, challenge: @challenge, recovery: @recovery)
|
32
|
+
elsif resource.otp_challenge_valid? && resource.validate_otp_token(@token, @recovery)
|
47
33
|
sign_in(resource_name, resource)
|
48
34
|
|
49
35
|
otp_set_trusted_device_for(resource) if params[:enable_persistence] == "true"
|
@@ -51,7 +37,7 @@ module DeviseOtp
|
|
51
37
|
respond_with resource, location: after_sign_in_path_for(resource)
|
52
38
|
else
|
53
39
|
otp_set_flash_message :alert, :token_invalid
|
54
|
-
|
40
|
+
render :show
|
55
41
|
end
|
56
42
|
end
|
57
43
|
|
@@ -78,6 +64,39 @@ module DeviseOtp
|
|
78
64
|
|
79
65
|
private
|
80
66
|
|
67
|
+
def set_challenge
|
68
|
+
@challenge = params[:challenge]
|
69
|
+
|
70
|
+
unless @challenge.present?
|
71
|
+
redirect_to :root
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def set_recovery
|
76
|
+
@recovery = (recovery_enabled? && params[:recovery] == "true")
|
77
|
+
end
|
78
|
+
|
79
|
+
def set_resource
|
80
|
+
self.resource = resource_class.find_valid_otp_challenge(@challenge)
|
81
|
+
|
82
|
+
unless resource.present?
|
83
|
+
otp_set_flash_message(:alert, :otp_session_invalid)
|
84
|
+
redirect_to new_session_path(resource_name)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def set_token
|
89
|
+
@token = params[:token]
|
90
|
+
end
|
91
|
+
|
92
|
+
def skip_challenge_if_trusted_browser
|
93
|
+
if is_otp_trusted_browser_for?(resource)
|
94
|
+
sign_in(resource_name, resource)
|
95
|
+
otp_refresh_credentials_for(resource)
|
96
|
+
redirect_to after_sign_in_path_for(resource)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
81
100
|
def done_valid_refresh
|
82
101
|
otp_refresh_credentials_for(resource)
|
83
102
|
respond_with resource, location: otp_fetch_refresh_return_url
|
@@ -19,24 +19,33 @@ module DeviseOtp
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
+
#
|
23
|
+
# Displays the QR Code and Validation Token form for enabling the OTP
|
24
|
+
#
|
25
|
+
def edit
|
26
|
+
resource.populate_otp_secrets!
|
27
|
+
end
|
28
|
+
|
22
29
|
#
|
23
30
|
# Updates the status of OTP authentication
|
24
31
|
#
|
25
32
|
def update
|
26
|
-
|
27
|
-
|
33
|
+
if resource.valid_otp_token?(params[:confirmation_code])
|
34
|
+
resource.enable_otp!
|
28
35
|
otp_set_flash_message :success, :successfully_updated
|
36
|
+
redirect_to action: :show
|
37
|
+
else
|
38
|
+
otp_set_flash_message :danger, :could_not_confirm
|
39
|
+
render :edit
|
29
40
|
end
|
30
|
-
|
31
|
-
render :show
|
32
41
|
end
|
33
42
|
|
34
43
|
#
|
35
44
|
# Resets OTP authentication, generates new credentials, sets it to off
|
36
45
|
#
|
37
46
|
def destroy
|
38
|
-
if resource.
|
39
|
-
otp_set_flash_message :success, :
|
47
|
+
if resource.disable_otp!
|
48
|
+
otp_set_flash_message :success, :successfully_disabled_otp
|
40
49
|
end
|
41
50
|
|
42
51
|
redirect_to action: :show
|
@@ -85,6 +94,15 @@ module DeviseOtp
|
|
85
94
|
end
|
86
95
|
end
|
87
96
|
|
97
|
+
def reset
|
98
|
+
if resource.disable_otp!
|
99
|
+
resource.clear_otp_fields!
|
100
|
+
otp_set_flash_message :success, :successfully_reset_otp
|
101
|
+
end
|
102
|
+
|
103
|
+
redirect_to action: :edit
|
104
|
+
end
|
105
|
+
|
88
106
|
private
|
89
107
|
|
90
108
|
def ensure_credentials_refresh
|
@@ -3,21 +3,21 @@
|
|
3
3
|
|
4
4
|
<%= form_for(resource, :as => resource_name, :url => [resource_name, :otp_credential], :html => { :method => :put, "data-turbo" => false }) do |f| %>
|
5
5
|
|
6
|
-
<%=
|
7
|
-
<%=
|
6
|
+
<%= hidden_field_tag :challenge, @challenge %>
|
7
|
+
<%= hidden_field_tag :recovery, @recovery %>
|
8
8
|
|
9
9
|
<% if @recovery %>
|
10
10
|
<p>
|
11
|
-
<%=
|
12
|
-
<%=
|
11
|
+
<%= label_tag :token, I18n.t('recovery_prompt', :scope => 'devise.otp.submit_token') %><br />
|
12
|
+
<%= text_field_tag :otp_recovery_counter, resource.otp_recovery_counter, :autocomplete => :off, :disabled => true, :size => 4 %>
|
13
13
|
</p>
|
14
14
|
<% else %>
|
15
15
|
<p>
|
16
|
-
<%=
|
16
|
+
<%= label_tag :token, I18n.t('prompt', :scope => 'devise.otp.submit_token') %><br />
|
17
17
|
</p>
|
18
18
|
<% end %>
|
19
19
|
|
20
|
-
<%=
|
20
|
+
<%= text_field_tag :token, nil, :autocomplete => :off, :autofocus => true, :size => 6 %><br>
|
21
21
|
|
22
22
|
<%= label_tag :enable_persistence do %>
|
23
23
|
<%= check_box_tag :enable_persistence, true, false %> <%= I18n.t('remember', :scope => 'devise.otp.general') %>
|
@@ -1,5 +1,4 @@
|
|
1
1
|
<h3><%= I18n.t('title', :scope => 'devise.otp.token_secret') %></h3>
|
2
|
-
<p><%= I18n.t('explain', :scope => 'devise.otp.token_secret') %></p>
|
3
2
|
|
4
3
|
<%= otp_authenticator_token_image(resource) %>
|
5
4
|
|
@@ -8,11 +7,11 @@
|
|
8
7
|
<code><%= resource.otp_auth_secret %></code>
|
9
8
|
</p>
|
10
9
|
|
11
|
-
<p><%= button_to I18n.t('
|
10
|
+
<p><%= button_to I18n.t('reset_link', :scope => 'devise.otp.otp_tokens'), reset_otp_token_path_for(resource), :method => :post , :data => { "turbo-method": "POST" } %></p>
|
12
11
|
|
13
12
|
<p>
|
14
|
-
<%= I18n.t('reset_explain', :scope => 'devise.otp.
|
15
|
-
<strong><%= I18n.t('reset_explain_warn', :scope => 'devise.otp.
|
13
|
+
<%= I18n.t('reset_explain', :scope => 'devise.otp.otp_tokens') %>
|
14
|
+
<strong><%= I18n.t('reset_explain_warn', :scope => 'devise.otp.otp_tokens') %></strong>
|
16
15
|
</p>
|
17
16
|
|
18
17
|
<%- if recovery_enabled? %>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<h2><%= I18n.t('title', :scope => 'devise.otp.edit_otp_token') %></h2>
|
2
|
+
<p><%= I18n.t('explain', :scope => 'devise.otp.edit_otp_token') %></p>
|
3
|
+
|
4
|
+
<h2><%= I18n.t('lead_in', :scope => 'devise.otp.edit_otp_token') %></h2>
|
5
|
+
|
6
|
+
<p><%= I18n.t('step_1', :scope => 'devise.otp.edit_otp_token') %></p>
|
7
|
+
|
8
|
+
<%= otp_authenticator_token_image(resource) %>
|
9
|
+
|
10
|
+
<p>
|
11
|
+
<strong><%= I18n.t('manual_provisioning', :scope => 'devise.otp.token_secret') %>:</strong>
|
12
|
+
<code><%= resource.otp_auth_secret %></code>
|
13
|
+
</p>
|
14
|
+
|
15
|
+
<p><%= I18n.t('step_2', :scope => 'devise.otp.edit_otp_token') %></p>
|
16
|
+
|
17
|
+
<%= form_with(:url => [resource_name, :otp_token], :method => :put) do |f| %>
|
18
|
+
|
19
|
+
<p>
|
20
|
+
<%= f.label :confirmation_code, I18n.t('confirmation_code', :scope => 'devise.otp.edit_otp_token') %>
|
21
|
+
<%= f.text_field :confirmation_code %>
|
22
|
+
</p>
|
23
|
+
|
24
|
+
<p><%= f.submit I18n.t('submit', :scope => 'devise.otp.edit_otp_token') %></p>
|
25
|
+
|
26
|
+
<% end %>
|
@@ -1,21 +1,14 @@
|
|
1
1
|
<h2><%= I18n.t('title', :scope => 'devise.otp.otp_tokens') %></h2>
|
2
|
-
<p><%= I18n.t('explain', :scope => 'devise.otp.otp_tokens') %></p>
|
3
2
|
|
4
|
-
<%=
|
5
|
-
|
6
|
-
<%= render "devise/shared/error_messages", resource: resource %>
|
7
|
-
|
8
|
-
<h3><%= I18n.t('enable_request', :scope => 'devise.otp.otp_tokens') %></h3>
|
9
|
-
|
10
|
-
<p>
|
11
|
-
<%= f.label :otp_enabled, I18n.t('status', :scope => 'devise.otp.otp_tokens') %><br />
|
12
|
-
<%= f.check_box :otp_enabled %>
|
13
|
-
</p>
|
14
|
-
|
15
|
-
<p><%= f.submit I18n.t('submit', :scope => 'devise.otp.otp_tokens') %></p>
|
16
|
-
<% end %>
|
3
|
+
<p><strong>Status:</strong> <%= resource.otp_enabled? ? "Enabled" : "Disabled" %></p>
|
17
4
|
|
18
5
|
<%- if resource.otp_enabled? %>
|
19
6
|
<%= render :partial => 'token_secret' if resource.otp_enabled? %>
|
20
7
|
<%= render :partial => 'trusted_devices' if trusted_devices_enabled? %>
|
8
|
+
|
9
|
+
<% unless otp_mandatory_on?(resource) %>
|
10
|
+
<%= button_to I18n.t('disable_link', :scope => 'devise.otp.otp_tokens'), @resource, :method => :delete, :data => { "turbo-method": "DELETE" } %>
|
11
|
+
<% end %>
|
12
|
+
<% else %>
|
13
|
+
<%= link_to I18n.t('enable_link', :scope => 'devise.otp.otp_tokens'), edit_otp_token_path_for(resource) %>
|
21
14
|
<% end %>
|
data/config/locales/en.yml
CHANGED
@@ -5,12 +5,13 @@ en:
|
|
5
5
|
remember: Remember me
|
6
6
|
submit_token:
|
7
7
|
title: 'Check Token'
|
8
|
-
explain: "You're getting this because you enabled
|
9
|
-
prompt: 'Please enter your
|
8
|
+
explain: "You're getting this because you enabled Two-Factor Authentication on your account"
|
9
|
+
prompt: 'Please enter your Two-Factor Authentication token:'
|
10
10
|
recovery_prompt: 'Please enter your recovery code:'
|
11
11
|
submit: 'Submit Token'
|
12
12
|
recovery_link: "I don't have my device, I want to use a recovery code"
|
13
13
|
otp_credentials:
|
14
|
+
otp_session_invalid: Session invalid. Please start again.
|
14
15
|
token_invalid: 'The token you provided was invalid.'
|
15
16
|
token_blank: 'You need to type in the token you generated with your device.'
|
16
17
|
need_to_refresh_credentials: 'We need to check your credentials before you can change these settings.'
|
@@ -21,22 +22,22 @@ en:
|
|
21
22
|
explain: 'In order to ensure this is safe, please enter your password again.'
|
22
23
|
go_on: 'Continue...'
|
23
24
|
identity: 'Identity:'
|
24
|
-
token: 'Your
|
25
|
+
token: 'Your Two-Factor Authentication token'
|
25
26
|
token_secret:
|
26
27
|
title: 'Your token secret'
|
27
28
|
explain: 'Take a photo of this QR code with your mobile'
|
28
29
|
manual_provisioning: 'Manual provisioning code'
|
29
|
-
reset_otp: 'Reset your Two Factors Authentication status'
|
30
|
-
reset_explain: 'This will reset your credentials, and disable two-factors authentication.'
|
31
|
-
reset_explain_warn: 'You will need to enroll your mobile device again.'
|
32
30
|
otp_tokens:
|
33
|
-
title: 'Two-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
31
|
+
title: 'Two-Factor Authentication:'
|
32
|
+
enable_link: 'Enable Two-Factor Authentication'
|
33
|
+
disable_link: 'Disable Two-Factor Authentication'
|
34
|
+
reset_link: 'Reset Token Secret'
|
35
|
+
reset_explain: 'Resetting your token secret will temporarilly disable Two-Factor Authentication.'
|
36
|
+
reset_explain_warn: 'To re-enable Two-Factor Authentication, you will need to re-enroll your mobile device with the new token secret.'
|
37
|
+
successfully_updated: 'Your Two-Factor Authentication settings have been updated.'
|
38
|
+
could_not_confirm: 'The Confirmation Code you entered did not match the QR code shown below.'
|
39
|
+
successfully_disabled_otp: 'Two-Factor Authentication has been disabled.'
|
40
|
+
successfully_reset_otp: 'Your token secret has been reset. Please confirm your new token secret below.'
|
40
41
|
successfully_set_persistence: 'Your device is now trusted.'
|
41
42
|
successfully_cleared_persistence: 'Your device has been removed from the list of trusted devices.'
|
42
43
|
successfully_reset_persistence: 'Your list of trusted devices has been cleared.'
|
@@ -48,9 +49,17 @@ en:
|
|
48
49
|
code: 'Recovery Code'
|
49
50
|
codes_list: 'Here is the list of your recovery codes'
|
50
51
|
download_codes: 'Download recovery codes'
|
52
|
+
edit_otp_token:
|
53
|
+
title: 'Enable Two-factor Authentication'
|
54
|
+
explain: 'Two-Factor Authentication adds an additional layer of security to your account. When logging in you will be asked for a code that you can generate on a physical device, like your phone.'
|
55
|
+
lead_in: 'To Enable Two-Factor Authentication:'
|
56
|
+
step_1: '1. Open your authenticator app and scan the QR code shown below:'
|
57
|
+
step_2: '2. Enter the 6-digit code shown in your authenticator app below:'
|
58
|
+
confirmation_code: "Confirmation Code"
|
59
|
+
submit: 'Continue...'
|
51
60
|
trusted_browsers:
|
52
61
|
title: 'Trusted Browsers'
|
53
|
-
explain: 'If you set your browser as trusted, you will not be asked to provide a Two-
|
62
|
+
explain: 'If you set your browser as trusted, you will not be asked to provide a Two-Factor Authentication token when logging in from that browser.'
|
54
63
|
browser_trusted: 'Your browser is trusted.'
|
55
64
|
browser_not_trusted: 'Your browser is not trusted.'
|
56
65
|
trust_remove: 'Remove this browser from the list of trusted browsers'
|
data/devise-otp.gemspec
CHANGED
@@ -19,12 +19,11 @@ Gem::Specification.new do |gem|
|
|
19
19
|
gem.add_runtime_dependency "rotp", ">= 2.0.0"
|
20
20
|
|
21
21
|
gem.add_development_dependency "capybara"
|
22
|
-
gem.add_development_dependency "cuprite"
|
23
22
|
gem.add_development_dependency "minitest-reporters", ">= 0.5.0"
|
24
23
|
gem.add_development_dependency "puma"
|
25
24
|
gem.add_development_dependency "rdoc"
|
26
25
|
gem.add_development_dependency "shoulda"
|
27
26
|
gem.add_development_dependency "sprockets-rails"
|
28
|
-
gem.add_development_dependency "sqlite3"
|
27
|
+
gem.add_development_dependency "sqlite3", "~> 1.4"
|
29
28
|
gem.add_development_dependency "standardrb"
|
30
29
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'devise/strategies/authenticatable'
|
4
|
+
|
5
|
+
module Devise
|
6
|
+
module Strategies
|
7
|
+
# Default strategy for signing in a user, based on their email and password in the database.
|
8
|
+
class DatabaseAuthenticatable < Authenticatable
|
9
|
+
def authenticate!
|
10
|
+
resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
|
11
|
+
hashed = false
|
12
|
+
|
13
|
+
if validate(resource){ hashed = true; resource.valid_password?(password) }
|
14
|
+
if otp_challenge_required_on?(resource)
|
15
|
+
# Redirect to challenge
|
16
|
+
challenge = resource.generate_otp_challenge!
|
17
|
+
redirect!(otp_challenge_url, {:challenge => challenge})
|
18
|
+
else
|
19
|
+
# Sign in user as usual
|
20
|
+
remember_me(resource)
|
21
|
+
resource.after_database_authentication
|
22
|
+
success!(resource)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# In paranoid mode, hash the password even when a resource doesn't exist for the given authentication key.
|
27
|
+
# This is necessary to prevent enumeration attacks - e.g. the request is faster when a resource doesn't
|
28
|
+
# exist in the database if the password hashing algorithm is not called.
|
29
|
+
mapping.to.new.password = password if !hashed && Devise.paranoid
|
30
|
+
unless resource
|
31
|
+
Devise.paranoid ? fail(:invalid) : fail(:not_found_in_database)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
#
|
38
|
+
# resource should be challenged for otp
|
39
|
+
#
|
40
|
+
def otp_challenge_required_on?(resource)
|
41
|
+
resource.respond_to?(:otp_enabled?) && resource.otp_enabled?
|
42
|
+
end
|
43
|
+
|
44
|
+
def otp_challenge_url
|
45
|
+
if Rails.env.development?
|
46
|
+
host = "#{request.host}:#{request.port}"
|
47
|
+
else
|
48
|
+
host = "#{request.host}"
|
49
|
+
end
|
50
|
+
|
51
|
+
path_fragments = ["otp", mapping.path_names[:credentials]]
|
52
|
+
if mapping.fullpath == "/"
|
53
|
+
path = mapping.fullpath + path_fragments.join("/")
|
54
|
+
else
|
55
|
+
path = path_fragments.prepend(mapping.fullpath).join("/")
|
56
|
+
end
|
57
|
+
|
58
|
+
request.protocol + host + path
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabaseAuthenticatable)
|
data/lib/devise-otp/version.rb
CHANGED
data/lib/devise-otp.rb
CHANGED
@@ -9,6 +9,24 @@ require "active_support/concern"
|
|
9
9
|
|
10
10
|
require "devise"
|
11
11
|
|
12
|
+
#
|
13
|
+
# define DeviseOtpAuthenticatable module, and autoload hooks and helpers
|
14
|
+
#
|
15
|
+
module DeviseOtpAuthenticatable
|
16
|
+
module Controllers
|
17
|
+
autoload :Helpers, "devise_otp_authenticatable/controllers/helpers"
|
18
|
+
autoload :UrlHelpers, "devise_otp_authenticatable/controllers/url_helpers"
|
19
|
+
autoload :PublicHelpers, "devise_otp_authenticatable/controllers/public_helpers"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require "devise_otp_authenticatable/routes"
|
24
|
+
require "devise_otp_authenticatable/engine"
|
25
|
+
require "devise_otp_authenticatable/hooks/refreshable"
|
26
|
+
|
27
|
+
#
|
28
|
+
# update Devise module with additions needed for DeviseOtpAuthenticatable
|
29
|
+
#
|
12
30
|
module Devise
|
13
31
|
mattr_accessor :otp_mandatory
|
14
32
|
@@otp_mandatory = false
|
@@ -49,22 +67,24 @@ module Devise
|
|
49
67
|
mattr_accessor :otp_controller_path
|
50
68
|
@@otp_controller_path = "devise"
|
51
69
|
|
70
|
+
#
|
71
|
+
# add PublicHelpers to helpers class variable to ensure that per-mapping helpers are present.
|
72
|
+
# this integrates with the "define_helpers," which is run when adding each mapping in the Devise gem (lib/devise.rb#541)
|
73
|
+
#
|
74
|
+
@@helpers << DeviseOtpAuthenticatable::Controllers::PublicHelpers
|
75
|
+
|
52
76
|
module Otp
|
53
77
|
end
|
54
|
-
end
|
55
|
-
|
56
|
-
module DeviseOtpAuthenticatable
|
57
|
-
autoload :Hooks, "devise_otp_authenticatable/hooks"
|
58
78
|
|
59
|
-
module Controllers
|
60
|
-
autoload :Helpers, "devise_otp_authenticatable/controllers/helpers"
|
61
|
-
autoload :UrlHelpers, "devise_otp_authenticatable/controllers/url_helpers"
|
62
|
-
end
|
63
79
|
end
|
64
80
|
|
65
|
-
require "devise_otp_authenticatable/routes"
|
66
|
-
require "devise_otp_authenticatable/engine"
|
67
|
-
|
68
81
|
Devise.add_module :otp_authenticatable,
|
69
82
|
controller: :tokens,
|
70
83
|
model: "devise_otp_authenticatable/models/otp_authenticatable", route: :otp
|
84
|
+
|
85
|
+
#
|
86
|
+
# add PublicHelpers after adding Devise module to ensure that per-mapping routes from above are included
|
87
|
+
#
|
88
|
+
ActiveSupport.on_load(:action_controller) do
|
89
|
+
include DeviseOtpAuthenticatable::Controllers::PublicHelpers
|
90
|
+
end
|