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.
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 +8 -2
  6. data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +46 -27
  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 +1 -2
  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 +26 -23
  50. data/lib/devise_otp_authenticatable/hooks/sessions.rb +0 -58
  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: 40ba57c939a2a84a81a014b151a2ea0fe37a4253942df15a703a0fd83aa9e80c
4
- data.tar.gz: bba9a1e8d78ea6760c9c32ed643c05ed9138635095c0f9a16451f6a9922bfeba
3
+ metadata.gz: 7ab31b9f7027a1468e535a1ac862c26cfc95891bdf10a618115a66767e1c5d16
4
+ data.tar.gz: c603692b0be3e3fbd22d4296299ccd0d5b9a42a1f9ab1f4d6cd7186ea3a864a8
5
5
  SHA512:
6
- metadata.gz: 79f30180843989aed3f784e8d775d8d86a206a328f3032b7f202815e1f590fcc5d3bf3d65cccc20731af5d1de227dd2d857111432fc21ad305524a402219f91a
7
- data.tar.gz: 905e257c92ef4cad86876a2e37ec46cea614dafc715ebff364e58c580f7ab1528181771ff96534e5e61978168840f4a9820729b9d819f4c7c002ec970de81da5
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
 
@@ -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-factors authentication using OTP
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
- @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,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
- 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
@@ -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)
@@ -1,5 +1,5 @@
1
1
  module Devise
2
2
  module OTP
3
- VERSION = "0.6.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