devise-otp 0.5.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +0 -2
  3. data/.gitignore +3 -1
  4. data/CHANGELOG.md +51 -8
  5. data/README.md +12 -2
  6. data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +46 -29
  7. data/app/controllers/devise_otp/devise/otp_tokens_controller.rb +24 -6
  8. data/app/views/devise/otp_credentials/show.html.erb +6 -6
  9. data/app/views/devise/otp_tokens/_token_secret.html.erb +3 -4
  10. data/app/views/devise/otp_tokens/edit.html.erb +26 -0
  11. data/app/views/devise/otp_tokens/show.html.erb +7 -14
  12. data/config/locales/en.yml +23 -14
  13. data/devise-otp.gemspec +2 -3
  14. data/lib/devise/strategies/database_authenticatable.rb +64 -0
  15. data/lib/devise-otp/version.rb +1 -1
  16. data/lib/devise-otp.rb +31 -11
  17. data/lib/devise_otp_authenticatable/controllers/helpers.rb +9 -10
  18. data/lib/devise_otp_authenticatable/controllers/public_helpers.rb +39 -0
  19. data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +10 -0
  20. data/lib/devise_otp_authenticatable/engine.rb +2 -5
  21. data/lib/devise_otp_authenticatable/hooks/refreshable.rb +5 -0
  22. data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +22 -20
  23. data/lib/devise_otp_authenticatable/routes.rb +3 -1
  24. data/test/dummy/app/controllers/admin_posts_controller.rb +85 -0
  25. data/test/dummy/app/controllers/application_controller.rb +0 -1
  26. data/test/dummy/app/controllers/base_controller.rb +6 -0
  27. data/test/dummy/app/models/admin.rb +25 -0
  28. data/test/dummy/app/views/admin_posts/_form.html.erb +25 -0
  29. data/test/dummy/app/views/admin_posts/edit.html.erb +6 -0
  30. data/test/dummy/app/views/admin_posts/index.html.erb +25 -0
  31. data/test/dummy/app/views/admin_posts/new.html.erb +5 -0
  32. data/test/dummy/app/views/admin_posts/show.html.erb +15 -0
  33. data/test/dummy/app/views/base/home.html.erb +1 -0
  34. data/test/dummy/config/application.rb +0 -2
  35. data/test/dummy/config/routes.rb +4 -1
  36. data/test/dummy/db/migrate/20240604000001_create_admins.rb +9 -0
  37. data/test/dummy/db/migrate/20240604000002_add_devise_to_admins.rb +52 -0
  38. data/test/dummy/db/migrate/20240604000003_devise_otp_add_to_admins.rb +28 -0
  39. data/test/integration/disable_token_test.rb +53 -0
  40. data/test/integration/enable_otp_form_test.rb +57 -0
  41. data/test/integration/persistence_test.rb +3 -6
  42. data/test/integration/refresh_test.rb +32 -0
  43. data/test/integration/reset_token_test.rb +45 -0
  44. data/test/integration/sign_in_test.rb +10 -14
  45. data/test/integration/trackable_test.rb +50 -0
  46. data/test/integration_tests_helper.rb +24 -6
  47. data/test/models/otp_authenticatable_test.rb +62 -27
  48. data/test/test_helper.rb +1 -71
  49. metadata +28 -25
  50. data/lib/devise_otp_authenticatable/hooks/sessions.rb +0 -57
  51. data/lib/devise_otp_authenticatable/hooks.rb +0 -11
  52. data/test/integration/token_test.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40f03e3fee5939da23d1f583390f794a2e4e456a08fd311d53a605e94ba77075
4
- data.tar.gz: cc3e95dcebf0186cbb7a5f3552184093dc6e2209e421b359c6384f788f8ccaed
3
+ metadata.gz: 7ab31b9f7027a1468e535a1ac862c26cfc95891bdf10a618115a66767e1c5d16
4
+ data.tar.gz: c603692b0be3e3fbd22d4296299ccd0d5b9a42a1f9ab1f4d6cd7186ea3a864a8
5
5
  SHA512:
6
- metadata.gz: a62beac8100a3f98a60b6b19f279d795d1f942dc0b44b5d7ad345527b315363185439630b3df7e34dce8d0c0bb154329b882965f45cabbb50ff1a93c7c7cdb09
7
- data.tar.gz: 651d1dc15b3444db54eb8e2d14d6273858d2ec66ac7feee0ce314085175d5d3722f7062e914979b2b05a647ba8470a873179d962aadcb0657bcf8bd9bd814c66
6
+ metadata.gz: d7668d8f50ee7704c7b18eff92c16384eb47186ebf646260391686addd2acc9a06f3dcb92ffd7a70f87ec20106acbaa8e2e1cda4d6e1ebaabdc68ad27c80996b
7
+ data.tar.gz: 47ab6592ff48f37715e61f07e81813d75a99bd2d971a79642051b27fb14efd6de2dcddf71a6d579f4cbd8d0ed379044cb59e80da78683b2573e4cf0e47a1d329
@@ -13,8 +13,6 @@ jobs:
13
13
  matrix:
14
14
  ruby:
15
15
  - '3.1'
16
- - '3.0'
17
- - '2.7'
18
16
 
19
17
  steps:
20
18
  - name: Checkout
data/.gitignore CHANGED
@@ -35,8 +35,10 @@ lib/bundler/man
35
35
  test/dummy/log/**
36
36
  test/dummy/tmp/**
37
37
  test/dummy/db/*.sqlite3
38
+ test/dummy/db/*.sqlite3-shm
39
+ test/dummy/db/*.sqlite3-wal
38
40
 
39
41
  Gemfile.lock
40
42
 
41
43
  # Generated test files
42
- tmp/*
44
+ tmp/*
data/CHANGELOG.md CHANGED
@@ -1,17 +1,60 @@
1
1
  # Changelog
2
2
 
3
- ## 0.4.0
3
+ ## 0.7.0
4
4
 
5
5
  Breaking changes:
6
6
 
7
- - rename `Devise::Otp` to `Devise::OTP`
8
- - change `credentials` directory to `otp_credentials`
9
- - change `tokens` directory to `otp_tokens`
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
- Other improvements:
21
+ ### Upgrading
12
22
 
13
- - Fix file permissions
23
+ Regenerate your views with `rails g devise_otp:views` and update locales.
14
24
 
15
- ## 0.3.0
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 two-factors 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).
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
 
@@ -15,7 +15,11 @@ Some of the compatible token devices are:
15
15
 
16
16
  Device OTP was recently updated to work with Rails 7 and Turbo.
17
17
 
18
- ## Two-factors authentication using OTP
18
+ ## Sponsor
19
+
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
+
22
+ ## Two-Factor Authentication using OTP
19
23
 
20
24
  * A shared secret is generated on the server, and stored both on the token device (e.g. the phone) and the server itself.
21
25
  * The secret is used to generate short numerical tokens that are either time or sequence based.
@@ -90,6 +94,12 @@ The install generator adds some options to the end of your Devise config file (`
90
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)
91
95
  * `config.otp_controller_path`: The view path for Devise OTP controllers. The default being 'devise' to match Devise default installation.
92
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
+
93
103
  ## Authors
94
104
 
95
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
- @challenge = params[:challenge]
14
- @recovery = (params[:recovery] == "true") && recovery_enabled?
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
- resource = resource_class.find_valid_otp_challenge(params[resource_name][:challenge])
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: params[resource_name][:challenge],
42
- recovery: recovery)
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
- redirect_to new_session_path(resource_name)
40
+ render :show
55
41
  end
56
42
  end
57
43
 
@@ -78,10 +64,41 @@ 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
- otp_set_flash_message :success, :valid_refresh if is_navigational_format?
84
-
85
102
  respond_with resource, location: otp_fetch_refresh_return_url
86
103
  end
87
104
 
@@ -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
- enabled = params[resource_name][:otp_enabled] == "1"
27
- if enabled ? resource.enable_otp! : resource.disable_otp!
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.reset_otp_credentials!
39
- otp_set_flash_message :success, :successfully_reset_creds
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
- <%= f.hidden_field :challenge, {:value => @challenge} %>
7
- <%= f.hidden_field :recovery, {:value => @recovery} %>
6
+ <%= hidden_field_tag :challenge, @challenge %>
7
+ <%= hidden_field_tag :recovery, @recovery %>
8
8
 
9
9
  <% if @recovery %>
10
10
  <p>
11
- <%= f.label :token, I18n.t('recovery_prompt', :scope => 'devise.otp.submit_token') %><br />
12
- <%= f.text_field :otp_recovery_counter, :autocomplete => :off, :disabled => true, :size => 4 %>
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
- <%= f.label :token, I18n.t('prompt', :scope => 'devise.otp.submit_token') %><br />
16
+ <%= label_tag :token, I18n.t('prompt', :scope => 'devise.otp.submit_token') %><br />
17
17
  </p>
18
18
  <% end %>
19
19
 
20
- <%= f.text_field :token, :autocomplete => :off, :autofocus => true, :size => 6, :value => '' %><br>
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('reset_otp', :scope => 'devise.otp.token_secret'), @resource, :method => :delete, :data => { "turbo-method": "DELETE" } %></p>
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.token_secret') %>
15
- <strong><%= I18n.t('reset_explain_warn', :scope => 'devise.otp.token_secret') %></strong>
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
- <%= form_for(resource, :as => resource_name, :url => [resource_name, :otp_token], :html => { :method => :put, "data-turbo" => false }) do |f| %>
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 %>
@@ -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 two-factors authentication on your account"
9
- prompt: 'Please enter your two-factors authentication token:'
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 two-factors authentication token'
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-factors Authentication:'
34
- explain: 'Two factors 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.'
35
- enable_request: 'Would you like to enable Two Factors Authenticator?'
36
- status: 'Enable Two-Factors Authentication.'
37
- submit: 'Continue...'
38
- successfully_updated: 'Your two-factors authentication settings have been updated.'
39
- successfully_reset_creds: 'Your two-factors credentials has been reset.'
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-factor authentication token when logging in from that browser.'
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
@@ -14,17 +14,16 @@ Gem::Specification.new do |gem|
14
14
  gem.files = `git ls-files`.split($/)
15
15
  gem.require_paths = ["lib"]
16
16
 
17
- gem.add_runtime_dependency "rails", ">= 7.0", "< 7.2"
17
+ gem.add_runtime_dependency "rails", ">= 6.1", "< 7.2"
18
18
  gem.add_runtime_dependency "devise", ">= 4.8.0", "< 5.0"
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)
@@ -1,5 +1,5 @@
1
1
  module Devise
2
2
  module OTP
3
- VERSION = "0.5.0"
3
+ VERSION = "0.7.0"
4
4
  end
5
5
  end
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