devise-otp 0.6.0 → 0.7.0
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/.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
|