devise-otp 0.2.2 → 0.4.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 (64) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +36 -0
  3. data/CHANGELOG.md +17 -0
  4. data/Gemfile +1 -22
  5. data/README.md +42 -75
  6. data/app/assets/javascripts/devise-otp.js +1 -0
  7. data/app/assets/javascripts/qrcode.js +609 -0
  8. data/app/controllers/devise_otp/devise/otp_credentials_controller.rb +102 -0
  9. data/app/controllers/devise_otp/devise/otp_tokens_controller.rb +112 -0
  10. data/app/views/devise/otp_credentials/refresh.html.erb +19 -0
  11. data/app/views/devise/otp_credentials/show.html.erb +31 -0
  12. data/app/views/devise/otp_tokens/_token_secret.html.erb +23 -0
  13. data/app/views/devise/otp_tokens/_trusted_devices.html.erb +12 -0
  14. data/app/views/devise/otp_tokens/recovery.html.erb +21 -0
  15. data/app/views/devise/otp_tokens/recovery_codes.text.erb +3 -0
  16. data/app/views/devise/otp_tokens/show.html.erb +21 -0
  17. data/config/locales/en.yml +8 -8
  18. data/devise-otp.gemspec +15 -10
  19. data/docs/QR_CODES.md +48 -0
  20. data/lib/devise-otp/version.rb +2 -2
  21. data/lib/devise-otp.rb +12 -11
  22. data/lib/devise_otp_authenticatable/controllers/helpers.rb +22 -15
  23. data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +6 -7
  24. data/lib/devise_otp_authenticatable/engine.rb +22 -13
  25. data/lib/devise_otp_authenticatable/hooks/sessions.rb +8 -7
  26. data/lib/devise_otp_authenticatable/hooks.rb +1 -1
  27. data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +14 -13
  28. data/lib/devise_otp_authenticatable/routes.rb +2 -5
  29. data/lib/generators/active_record/templates/migration.rb +1 -1
  30. data/lib/generators/devise_otp/install_generator.rb +8 -5
  31. data/lib/generators/devise_otp/views_generator.rb +2 -3
  32. data/test/dummy/app/assets/config/manifest.js +2 -0
  33. data/test/dummy/app/assets/javascripts/application.js +1 -0
  34. data/test/dummy/app/controllers/application_controller.rb +1 -1
  35. data/test/dummy/app/controllers/posts_controller.rb +2 -0
  36. data/test/dummy/app/models/user.rb +1 -1
  37. data/test/dummy/config/application.rb +2 -1
  38. data/test/dummy/config/database.yml +1 -1
  39. data/test/dummy/config/environments/development.rb +0 -7
  40. data/test/dummy/config/environments/production.rb +0 -4
  41. data/test/dummy/db/migrate/20130125101430_create_users.rb +1 -1
  42. data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +1 -1
  43. data/test/dummy/db/migrate/20130131142320_create_posts.rb +1 -1
  44. data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +2 -2
  45. data/test/dummy/script/rails +0 -0
  46. data/test/integration/persistence_test.rb +81 -0
  47. data/test/integration/refresh_test.rb +2 -18
  48. data/test/integration/sign_in_test.rb +13 -3
  49. data/test/integration/token_test.rb +1 -4
  50. data/test/integration_tests_helper.rb +7 -3
  51. data/test/models/otp_authenticatable_test.rb +14 -9
  52. data/test/orm/active_record.rb +3 -1
  53. data/test/test_helper.rb +71 -2
  54. metadata +132 -25
  55. data/.travis.yml +0 -12
  56. data/app/controllers/devise_otp/credentials_controller.rb +0 -106
  57. data/app/controllers/devise_otp/tokens_controller.rb +0 -105
  58. data/app/views/devise_otp/credentials/refresh.html.erb +0 -20
  59. data/app/views/devise_otp/credentials/show.html.erb +0 -23
  60. data/app/views/devise_otp/tokens/_token_secret.html.erb +0 -17
  61. data/app/views/devise_otp/tokens/_trusted_devices.html.erb +0 -10
  62. data/app/views/devise_otp/tokens/recovery.html.erb +0 -21
  63. data/app/views/devise_otp/tokens/show.html.erb +0 -19
  64. data/lib/devise_otp_authenticatable/mapping.rb +0 -19
@@ -0,0 +1,102 @@
1
+ module DeviseOtp
2
+ module Devise
3
+ class OtpCredentialsController < DeviseController
4
+ helper_method :new_session_path
5
+
6
+ prepend_before_action :authenticate_scope!, :only => [:get_refresh, :set_refresh]
7
+ prepend_before_action :require_no_authentication, :only => [ :show, :update ]
8
+
9
+ #
10
+ # show a request for the OTP token
11
+ #
12
+ 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
28
+ end
29
+ end
30
+
31
+ #
32
+ # signs the resource in, if the OTP token is valid and the user has a valid challenge
33
+ #
34
+ 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?
40
+ 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
+ else
47
+ if resource.otp_challenge_valid? && resource.validate_otp_token(params[resource_name][:token], recovery)
48
+ set_flash_message(:success, :signed_in) if is_navigational_format?
49
+ sign_in(resource_name, resource)
50
+
51
+ otp_set_trusted_device_for(resource) if params[:enable_persistence] == "true"
52
+ otp_refresh_credentials_for(resource)
53
+ respond_with resource, :location => after_sign_in_path_for(resource)
54
+ else
55
+ otp_set_flash_message :alert, :token_invalid
56
+ redirect_to new_session_path(resource_name)
57
+ end
58
+ end
59
+ end
60
+
61
+ #
62
+ # displays the request for a credentials refresh
63
+ #
64
+ def get_refresh
65
+ ensure_resource!
66
+ render :refresh
67
+ end
68
+
69
+ #
70
+ # lets the user through is the refresh is valid
71
+ #
72
+ def set_refresh
73
+ ensure_resource!
74
+
75
+ if resource.valid_password?(params[resource_name][:refresh_password])
76
+ done_valid_refresh
77
+ else
78
+ failed_refresh
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def done_valid_refresh
85
+ otp_refresh_credentials_for(resource)
86
+ otp_set_flash_message :success, :valid_refresh if is_navigational_format?
87
+
88
+ respond_with resource, :location => otp_fetch_refresh_return_url
89
+ end
90
+
91
+ def failed_refresh
92
+ otp_set_flash_message :alert, :invalid_refresh
93
+ render :refresh
94
+ end
95
+
96
+ def self.controller_path
97
+ "#{::Devise.otp_controller_path}/otp_credentials"
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,112 @@
1
+ module DeviseOtp
2
+ module Devise
3
+ class OtpTokensController < DeviseController
4
+ include ::Devise::Controllers::Helpers
5
+
6
+ prepend_before_action :ensure_credentials_refresh
7
+ prepend_before_action :authenticate_scope!
8
+
9
+ protect_from_forgery :except => [:clear_persistence, :delete_persistence]
10
+
11
+ #
12
+ # Displays the status of OTP authentication
13
+ #
14
+ def show
15
+ if resource.nil?
16
+ redirect_to stored_location_for(scope) || :root
17
+ else
18
+ render :show
19
+ end
20
+ end
21
+
22
+ #
23
+ # Updates the status of OTP authentication
24
+ #
25
+ def update
26
+ enabled = params[resource_name][:otp_enabled] == '1'
27
+ if (enabled ? resource.enable_otp! : resource.disable_otp!)
28
+ otp_set_flash_message :success, :successfully_updated
29
+ end
30
+
31
+ render :show
32
+ end
33
+
34
+ #
35
+ # Resets OTP authentication, generates new credentials, sets it to off
36
+ #
37
+ def destroy
38
+ if resource.reset_otp_credentials!
39
+ otp_set_flash_message :success, :successfully_reset_creds
40
+ end
41
+
42
+ redirect_to :action => :show
43
+ end
44
+
45
+ #
46
+ # makes the current browser persistent
47
+ #
48
+ def get_persistence
49
+ if otp_set_trusted_device_for(resource)
50
+ otp_set_flash_message :success, :successfully_set_persistence
51
+ end
52
+
53
+ redirect_to :action => :show
54
+ end
55
+
56
+ #
57
+ # clears persistence for the current browser
58
+ #
59
+ def clear_persistence
60
+ if otp_clear_trusted_device_for(resource)
61
+ otp_set_flash_message :success, :successfully_cleared_persistence
62
+ end
63
+
64
+ redirect_to :action => :show
65
+ end
66
+
67
+ #
68
+ # rehash the persistence secret, thus, making all the persistence cookies invalid
69
+ #
70
+ def delete_persistence
71
+ if otp_reset_persistence_for(resource)
72
+ otp_set_flash_message :notice, :successfully_reset_persistence
73
+ end
74
+
75
+ redirect_to :action => :show
76
+ end
77
+
78
+ #
79
+ #
80
+ #
81
+ def recovery
82
+ respond_to do |format|
83
+ format.html
84
+ format.js
85
+ format.text do
86
+ send_data render_to_string(template: "#{controller_path}/recovery_codes"), filename: "otp-recovery-codes.txt", format: "text"
87
+ end
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def ensure_credentials_refresh
94
+ ensure_resource!
95
+
96
+ if needs_credentials_refresh?(resource)
97
+ otp_set_flash_message :notice, :need_to_refresh_credentials
98
+ redirect_to refresh_otp_credential_path_for(resource)
99
+ end
100
+ end
101
+
102
+ def scope
103
+ resource_name.to_sym
104
+ end
105
+
106
+ def self.controller_path
107
+ "#{::Devise.otp_controller_path}/otp_tokens"
108
+ end
109
+
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,19 @@
1
+ <h2><%= I18n.t('title', :scope => 'devise.otp.credentials_refresh') %></h2>
2
+ <p><%= I18n.t('explain', :scope => 'devise.otp.credentials_refresh') %></p>
3
+
4
+ <%= form_for(resource, :as => resource_name, :url => [:refresh, resource_name, :otp_credential], :html => { :method => :put, "data-turbo" => false }) do |f| %>
5
+
6
+ <%= render "devise/shared/error_messages", resource: resource %>
7
+
8
+ <div>
9
+ <%= f.label :email %><br />
10
+ <%= f.text_field :email, :disabled => :true%>
11
+ </div>
12
+
13
+ <div>
14
+ <%= f.label :password %><br />
15
+ <%= f.password_field :refresh_password, :autocomplete => :off, :autofocus => true %>
16
+ </div>
17
+
18
+ <div><%= f.submit I18n.t(:go_on, :scope => 'devise.otp.credentials_refresh') %></div>
19
+ <% end %>
@@ -0,0 +1,31 @@
1
+ <h2><%= I18n.t('title', :scope => 'devise.otp.submit_token') %></h2>
2
+ <p><%= I18n.t('explain', :scope => 'devise.otp.submit_token') %></p>
3
+
4
+ <%= form_for(resource, :as => resource_name, :url => [resource_name, :otp_credential], :html => { :method => :put, "data-turbo" => false }) do |f| %>
5
+
6
+ <%= f.hidden_field :challenge, {:value => @challenge} %>
7
+ <%= f.hidden_field :recovery, {:value => @recovery} %>
8
+
9
+ <% if @recovery %>
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 %>
13
+ </p>
14
+ <% else %>
15
+ <p>
16
+ <%= f.label :token, I18n.t('prompt', :scope => 'devise.otp.submit_token') %><br />
17
+ </p>
18
+ <% end %>
19
+
20
+ <%= f.text_field :token, :autocomplete => :off, :autofocus => true, :size => 6, :value => '' %><br>
21
+
22
+ <%= label_tag :enable_persistence do %>
23
+ <%= check_box_tag :enable_persistence, true, false %> Remember this browser
24
+ <% end %>
25
+
26
+ <p><%= f.submit I18n.t('submit', :scope => 'devise.otp.submit_token') %></p>
27
+
28
+ <% if !@recovery && recovery_enabled? %>
29
+ <p><%= link_to I18n.t('recovery_link', :scope => 'devise.otp.submit_token'), otp_credential_path_for(resource_name, :challenge => @challenge, :recovery => true) %></p>
30
+ <% end %>
31
+ <% end %>
@@ -0,0 +1,23 @@
1
+ <h3><%= I18n.t('title', :scope => 'devise.otp.token_secret') %></h3>
2
+ <p><%= I18n.t('explain', :scope => 'devise.otp.token_secret') %></p>
3
+
4
+ <%= otp_authenticator_token_image(resource) %>
5
+
6
+ <p>
7
+ <strong><%= I18n.t('manual_provisioning', :scope => 'devise.otp.token_secret') %>:</strong>
8
+ <code><%= resource.otp_auth_secret %></code>
9
+ </p>
10
+
11
+ <p><%= button_to I18n.t('reset_otp', :scope => 'devise.otp.token_secret'), @resource, :method => :delete, :data => { "turbo-method": "DELETE" } %></p>
12
+
13
+ <p>
14
+ <%= I18n.t('reset_explain', :scope => 'devise.otp.token_secret') %>
15
+ <strong><%= I18n.t('reset_explain_warn', :scope => 'devise.otp.token_secret') %></strong>
16
+ </p>
17
+
18
+ <%- if recovery_enabled? %>
19
+ <h3><%= I18n.t('title', :scope => 'devise.otp.tokens.recovery') %></h3>
20
+ <p><%= I18n.t('explain', :scope => 'devise.otp.tokens.recovery') %></p>
21
+ <p><%= link_to I18n.t('codes_list', :scope => 'devise.otp.tokens.recovery'), recovery_otp_token_for(resource_name) %></p>
22
+ <p><%= link_to I18n.t('download_codes', :scope => 'devise.otp.tokens.recovery'), recovery_otp_token_for(resource_name, format: :text) %></p>
23
+ <% end %>
@@ -0,0 +1,12 @@
1
+ <h3><%= I18n.t('title', :scope => 'devise.otp.trusted_browsers') %></h3>
2
+ <p><%= I18n.t('explain', :scope => 'devise.otp.trusted_browsers') %></p>
3
+
4
+ <%- if is_otp_trusted_browser_for? resource %>
5
+ <p><em><%= I18n.t('browser_trusted', :scope => 'devise.otp.trusted_browsers') %></em></p>
6
+ <p><%= link_to I18n.t('trust_remove', :scope => 'devise.otp.trusted_browsers'), persistence_otp_token_path_for(resource_name), :method => :post, :data => { "turbo-method": "POST" } %></p>
7
+ <% else %>
8
+ <p><%= I18n.t('browser_not_trusted', :scope => 'devise.otp.trusted_browsers') %></p>
9
+ <p><%= link_to I18n.t('trust_add', :scope => 'devise.otp.trusted_browsers'), persistence_otp_token_path_for(resource_name) %></p>
10
+ <% end %>
11
+
12
+ <p><%= button_to I18n.t('trust_clear', :scope => 'devise.otp.trusted_browsers'), persistence_otp_token_path_for(resource_name), :method => :delete, :data => { "turbo-method": "DELETE" } %></p>
@@ -0,0 +1,21 @@
1
+ <h2><%= I18n.t('title', :scope => 'devise.otp.tokens.recovery') %></h2>
2
+ <p><%= I18n.t('explain', :scope => 'devise.otp.tokens.recovery') %></p>
3
+
4
+ <table>
5
+ <caption>
6
+ <thead>
7
+ <tr>
8
+ <th><%= I18n.t('sequence', :scope => 'devise.otp.tokens.recovery') %></th>
9
+ <th><%= I18n.t('code', :scope => 'devise.otp.tokens.recovery') %></th>
10
+ </tr>
11
+ </thead>
12
+ <tbody>
13
+ <%- resource.next_otp_recovery_tokens.each do |seq, code| %>
14
+ <tr>
15
+ <td><%= seq %></td>
16
+ <td><%= code %></td>
17
+ </tr>
18
+ <% end %>
19
+ </tbody>
20
+ </caption>
21
+ </table>
@@ -0,0 +1,3 @@
1
+ <% resource.next_otp_recovery_tokens.each do |seq, code| %>
2
+ <%= code %>
3
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <h2><%= I18n.t('title', :scope => 'devise.otp.tokens') %></h2>
2
+ <p><%= I18n.t('explain', :scope => 'devise.otp.tokens') %></p>
3
+
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.tokens') %></h3>
9
+
10
+ <p>
11
+ <%= f.label :otp_enabled, I18n.t('status', :scope => 'devise.otp.tokens') %><br />
12
+ <%= f.check_box :otp_enabled %>
13
+ </p>
14
+
15
+ <p><%= f.submit I18n.t('submit', :scope => 'devise.otp.tokens') %></p>
16
+ <% end %>
17
+
18
+ <%- if resource.otp_enabled? %>
19
+ <%= render :partial => 'token_secret' if resource.otp_enabled? %>
20
+ <%= render :partial => 'trusted_devices' if trusted_devices_enabled? %>
21
+ <% end %>
@@ -34,7 +34,7 @@ en:
34
34
 
35
35
  tokens:
36
36
  title: 'Two-factors Authentication:'
37
- explain: 'Two factors authentication adds 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.'
37
+ 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.'
38
38
  enable_request: 'Would you like to enable Two Factors Authenticator?'
39
39
 
40
40
  status: 'Enable Two-Factors Authentication.'
@@ -54,13 +54,13 @@ en:
54
54
  sequence: 'Sequence'
55
55
  code: 'Recovery Code'
56
56
  codes_list: 'Here is the list of your recovery codes'
57
+ download_codes: 'Download recovery codes'
57
58
 
58
-
59
- trusted_devices:
59
+ trusted_browsers:
60
60
  title: 'Trusted Browsers'
61
- explain: 'If you set your browser as trusted, you will not be asked to perform a 2-factors authentication when logging in from that browser, for a time of one month.'
62
- device_trusted: 'Your browser is trusted.'
63
- device_not_trusted: 'Your browser is not trusted.'
64
- trust_remove: 'Remove this device from the list of trusted browsers'
61
+ 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
+ browser_trusted: 'Your browser is trusted.'
63
+ browser_not_trusted: 'Your browser is not trusted.'
64
+ trust_remove: 'Remove this browser from the list of trusted browsers'
65
65
  trust_add: 'Trust this browser'
66
- trust_clear: 'Clear the list of trusted browser'
66
+ trust_clear: 'Clear the list of trusted browsers'
data/devise-otp.gemspec CHANGED
@@ -1,25 +1,30 @@
1
- # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'devise-otp/version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/devise-otp/version'
5
4
 
6
5
  Gem::Specification.new do |gem|
7
6
  gem.name = "devise-otp"
8
- gem.version = Devise::Otp::VERSION
9
- gem.authors = ["Lele Forzani"]
10
- gem.email = ["lele@windmill.it"]
7
+ gem.version = Devise::OTP::VERSION
8
+ gem.authors = ["Lele Forzani", "Josef Strzibny"]
9
+ gem.email = ["lele@windmill.it", "strzibny@strzibny.name"]
11
10
  gem.description = %q{Time Based OTP/rfc6238 compatible authentication for Devise}
12
11
  gem.summary = %q{Time Based OTP/rfc6238 compatible authentication for Devise}
13
12
  gem.homepage = "http://git.windmill.it/wm/devise-otp"
14
13
 
15
14
  gem.files = `git ls-files`.split($/)
16
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
15
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
16
  gem.require_paths = ["lib"]
19
17
 
20
- gem.add_runtime_dependency 'rails', '>= 3.2.6', '< 5'
21
- gem.add_runtime_dependency 'devise', '>= 3.1.0', '< 4.0.0'
18
+ gem.add_runtime_dependency 'rails', '>= 7.0', '< 7.1'
19
+ gem.add_runtime_dependency 'devise', '>= 4.8.0', '< 4.9.0'
22
20
  gem.add_runtime_dependency 'rotp', '>= 2.0.0'
23
21
 
22
+ gem.add_development_dependency "capybara"
23
+ gem.add_development_dependency "cuprite"
24
+ gem.add_development_dependency "minitest-reporters", ">= 0.5.0"
25
+ gem.add_development_dependency "puma"
26
+ gem.add_development_dependency "rdoc"
27
+ gem.add_development_dependency "shoulda"
28
+ gem.add_development_dependency "sprockets-rails"
24
29
  gem.add_development_dependency "sqlite3"
25
30
  end
data/docs/QR_CODES.md ADDED
@@ -0,0 +1,48 @@
1
+ # QR code rendering
2
+
3
+ By default, Devise OTP assumes that you use [Sprockets](https://github.com/rails/sprockets) to render assets and so will use the ([qrcode.js](/app/assets/javascripts/qrcode.js)) embeded library to render the QR code.
4
+
5
+ To do that, add the the following line to your `application.js` file:
6
+
7
+ //= require devise-otp
8
+
9
+ You can change this behavior by overriding the `otp_authenticator_token_image` method in your view helper to call `otp_authenticator_token_image_google`:
10
+
11
+ ```ruby
12
+ def otp_authenticator_token_image(resource)
13
+ otp_authenticator_token_image_google(resource.otp_provisioning_uri)
14
+ end
15
+ ```
16
+
17
+ This will call [Google API](https://github.com/wmlele/devise-otp/tree/master/lib/devise_otp_authenticatable/controllers/helpers.rb#L160) to render the QR code.
18
+
19
+ If your application is configured to use CSP policies, you'll need to authorize `chart.googleapis.com`. Here's an example with [secure_headers](https://github.com/github/secure_headers)):
20
+
21
+ ```ruby
22
+ config.csp[:img_src] << 'chart.googleapis.com'
23
+ ```
24
+
25
+ A third option consists in installing [jquery-qrcode]https://github.com/jeromeetienne/jquery-qrcode with Yarn or [shakapacker](https://github.com/shakacode/shakapacker) and overriding `otp_authenticator_token_image` to render some HTML :
26
+
27
+ ```ruby
28
+ def otp_authenticator_token_image(resource)
29
+ tag(:span, data: { toggle: 'qrcode', otp_url: resource.otp_provisioning_uri, width: 192, height: 192, render: 'canvas' })
30
+ end
31
+ ```
32
+ The QR code is then rendered by `jquery-qrcode` by setting a JS listener in your `application.js` :
33
+
34
+ ```js
35
+ $(document).on('turbo:load', function() {
36
+ return $('[data-toggle=qrcode]').each(function() {
37
+ var data;
38
+ data = $(this).data();
39
+ return $(this).qrcode({
40
+ text: data['otpUrl'],
41
+ width: data['width'],
42
+ height: data['height'],
43
+ render: data['render']
44
+ });
45
+ });
46
+ });
47
+ ```
48
+ This way you don't rely on external services to render the QR codes.
@@ -1,5 +1,5 @@
1
1
  module Devise
2
- module Otp
3
- VERSION = "0.2.2"
2
+ module OTP
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
data/lib/devise-otp.rb CHANGED
@@ -9,10 +9,8 @@ require 'active_support/concern'
9
9
 
10
10
  require 'devise'
11
11
 
12
-
13
12
  module Devise
14
13
 
15
-
16
14
  #
17
15
  #
18
16
  #
@@ -53,20 +51,24 @@ module Devise
53
51
  @@otp_credentials_refresh = 15.minutes # or like 15.minutes, false to disable
54
52
 
55
53
  #
56
- # the user identifier for the token is <email>/Application_name
54
+ # the name of the token issuer
57
55
  #
58
- mattr_accessor :otp_uri_application
59
- @@otp_uri_application = Rails.application.class.parent_name
56
+ mattr_accessor :otp_issuer
57
+ @@otp_issuer = Rails.application.class.module_parent_name
60
58
 
61
- module Otp
62
59
 
60
+ #
61
+ # custom view path
62
+ #
63
+ mattr_accessor :otp_controller_path
64
+ @@otp_controller_path = "devise"
65
+
66
+ module Otp
63
67
  end
64
68
  end
65
69
 
66
70
  module DeviseOtpAuthenticatable
67
-
68
71
  autoload :Hooks, 'devise_otp_authenticatable/hooks'
69
- autoload :Mapping, 'devise_otp_authenticatable/mapping'
70
72
 
71
73
  module Controllers
72
74
  autoload :Helpers, 'devise_otp_authenticatable/controllers/helpers'
@@ -74,10 +76,9 @@ module DeviseOtpAuthenticatable
74
76
  end
75
77
  end
76
78
 
77
-
78
79
  require 'devise_otp_authenticatable/routes'
79
80
  require 'devise_otp_authenticatable/engine'
80
81
 
81
82
  Devise.add_module :otp_authenticatable,
82
- :controller => :otp_tokens,
83
- :model => 'devise_otp_authenticatable/models/otp_authenticatable', :route => :otp
83
+ :controller => :tokens,
84
+ :model => 'devise_otp_authenticatable/models/otp_authenticatable', :route => :otp
@@ -1,9 +1,7 @@
1
1
  module DeviseOtpAuthenticatable
2
-
3
2
  module Controllers
4
3
  module Helpers
5
4
 
6
-
7
5
  def authenticate_scope!
8
6
  send(:"authenticate_#{resource_name}!", :force => true)
9
7
  self.resource = send("current_#{resource_name}")
@@ -18,15 +16,13 @@ module DeviseOtpAuthenticatable
18
16
  options[:default] = Array(options[:default]).unshift(kind.to_sym)
19
17
  options[:resource_name] = resource_name
20
18
  options = devise_i18n_options(options) if respond_to?(:devise_i18n_options, true)
21
- message = I18n.t("#{options[:resource_name]}.#{kind}", options)
19
+ message = I18n.t("#{options[:resource_name]}.#{kind}", **options)
22
20
  flash[key] = message if message.present?
23
21
  end
24
22
 
25
23
  def otp_t()
26
-
27
24
  end
28
25
 
29
-
30
26
  def trusted_devices_enabled?
31
27
  resource.class.otp_trust_persistence && (resource.class.otp_trust_persistence > 0)
32
28
  end
@@ -44,9 +40,7 @@ module DeviseOtpAuthenticatable
44
40
  end
45
41
  end
46
42
 
47
-
48
43
  # fixme do cookies and persistence need to be scoped? probably
49
-
50
44
  #
51
45
  # check if the resource needs a credentials refresh. IE, they need to be asked a password again to access
52
46
  # this resource.
@@ -66,16 +60,14 @@ module DeviseOtpAuthenticatable
66
60
  session[otp_scoped_refresh_property] = (Time.now + resource.class.otp_credentials_refresh)
67
61
  end
68
62
 
69
-
70
63
  #
71
64
  # is the current browser trusted?
72
65
  #
73
- def is_otp_trusted_device_for?(resource)
66
+ def is_otp_trusted_browser_for?(resource)
74
67
  return false unless resource.class.otp_trust_persistence
75
68
  if cookies[otp_scoped_persistence_cookie].present?
76
69
  cookies.signed[otp_scoped_persistence_cookie] ==
77
- [resource.class.serialize_into_cookie(resource), resource.otp_persistence_seed].tap do
78
- end
70
+ [resource.to_key, resource.authenticatable_salt, resource.otp_persistence_seed]
79
71
  else
80
72
  false
81
73
  end
@@ -89,7 +81,7 @@ module DeviseOtpAuthenticatable
89
81
  cookies.signed[otp_scoped_persistence_cookie] = {
90
82
  :httponly => true,
91
83
  :expires => Time.now + resource.class.otp_trust_persistence,
92
- :value => [resource.class.serialize_into_cookie(resource), resource.otp_persistence_seed]
84
+ :value => [resource.to_key, resource.authenticatable_salt, resource.otp_persistence_seed]
93
85
  }
94
86
  end
95
87
 
@@ -121,7 +113,6 @@ module DeviseOtpAuthenticatable
121
113
  cookies.delete(otp_scoped_persistence_cookie)
122
114
  end
123
115
 
124
-
125
116
  #
126
117
  # clears the persistence list for this kind of resource
127
118
  #
@@ -134,11 +125,27 @@ module DeviseOtpAuthenticatable
134
125
  # returns the URL for the QR Code to initialize the Authenticator device
135
126
  #
136
127
  def otp_authenticator_token_image(resource)
137
- otp_authenticator_token_image_google(resource.otp_provisioning_uri)
128
+ otp_authenticator_token_image_js(resource.otp_provisioning_uri)
138
129
  end
139
130
 
140
131
  private
141
132
 
133
+ def otp_authenticator_token_image_js(otp_url)
134
+ content_tag(:div, :class => 'qrcode-container') do
135
+ tag(:div, :id => 'qrcode', :class => 'qrcode') +
136
+ javascript_tag(%Q[
137
+ new QRCode("qrcode", {
138
+ text: "#{otp_url}",
139
+ width: 256,
140
+ height: 256,
141
+ colorDark : "#000000",
142
+ colorLight : "#ffffff",
143
+ correctLevel : QRCode.CorrectLevel.H
144
+ });
145
+ ]) + tag("/div")
146
+ end
147
+ end
148
+
142
149
  def otp_authenticator_token_image_google(otp_url)
143
150
  otp_url = Rack::Utils.escape(otp_url)
144
151
  url = "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=#{otp_url}"
@@ -147,4 +154,4 @@ module DeviseOtpAuthenticatable
147
154
 
148
155
  end
149
156
  end
150
- end
157
+ end