devise-2fa 0.1.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 (95) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +36 -0
  3. data/.hound.yml +2 -0
  4. data/.ruby-style.yml +1248 -0
  5. data/.travis.yml +28 -0
  6. data/Gemfile +25 -0
  7. data/LICENSE +21 -0
  8. data/README.md +130 -0
  9. data/Rakefile +41 -0
  10. data/app/controllers/devise/credentials_controller.rb +100 -0
  11. data/app/controllers/devise/tokens_controller.rb +99 -0
  12. data/app/views/devise/credentials/refresh.html.erb +20 -0
  13. data/app/views/devise/credentials/show.html.erb +23 -0
  14. data/app/views/devise/tokens/_token_secret.html.erb +19 -0
  15. data/app/views/devise/tokens/_trusted_devices.html.erb +10 -0
  16. data/app/views/devise/tokens/recovery.html.erb +21 -0
  17. data/app/views/devise/tokens/recovery_codes.text.erb +3 -0
  18. data/app/views/devise/tokens/show.html.erb +19 -0
  19. data/config/locales/en.yml +57 -0
  20. data/devise-2fa.gemspec +27 -0
  21. data/lib/devise-2fa.rb +74 -0
  22. data/lib/devise-2fa/version.rb +5 -0
  23. data/lib/devise_two_factorable/controllers/helpers.rb +136 -0
  24. data/lib/devise_two_factorable/controllers/url_helpers.rb +30 -0
  25. data/lib/devise_two_factorable/engine.rb +22 -0
  26. data/lib/devise_two_factorable/helpers.rb +136 -0
  27. data/lib/devise_two_factorable/hooks.rb +11 -0
  28. data/lib/devise_two_factorable/hooks/sessions.rb +49 -0
  29. data/lib/devise_two_factorable/mapping.rb +12 -0
  30. data/lib/devise_two_factorable/models/two_factorable.rb +131 -0
  31. data/lib/devise_two_factorable/routes.rb +26 -0
  32. data/lib/devise_two_factorable/two_factorable.rb +131 -0
  33. data/lib/generators/active_record/devise_two_factor_generator.rb +32 -0
  34. data/lib/generators/active_record/templates/migration.rb +27 -0
  35. data/lib/generators/devise_two_factor/devise_two_factor_generator.rb +16 -0
  36. data/lib/generators/devise_two_factor/install_generator.rb +52 -0
  37. data/lib/generators/devise_two_factor/views_generator.rb +19 -0
  38. data/lib/generators/mongoid/devise_two_factor_generator.rb +34 -0
  39. data/test/dummy/README.rdoc +261 -0
  40. data/test/dummy/Rakefile +7 -0
  41. data/test/dummy/app/assets/javascripts/application.js +13 -0
  42. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  43. data/test/dummy/app/controllers/application_controller.rb +4 -0
  44. data/test/dummy/app/controllers/posts_controller.rb +83 -0
  45. data/test/dummy/app/helpers/application_helper.rb +2 -0
  46. data/test/dummy/app/helpers/posts_helper.rb +2 -0
  47. data/test/dummy/app/mailers/.gitkeep +0 -0
  48. data/test/dummy/app/models/post.rb +2 -0
  49. data/test/dummy/app/models/user.rb +20 -0
  50. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  51. data/test/dummy/app/views/posts/_form.html.erb +25 -0
  52. data/test/dummy/app/views/posts/edit.html.erb +6 -0
  53. data/test/dummy/app/views/posts/index.html.erb +25 -0
  54. data/test/dummy/app/views/posts/new.html.erb +5 -0
  55. data/test/dummy/app/views/posts/show.html.erb +15 -0
  56. data/test/dummy/config.ru +4 -0
  57. data/test/dummy/config/application.rb +67 -0
  58. data/test/dummy/config/boot.rb +10 -0
  59. data/test/dummy/config/database.yml +25 -0
  60. data/test/dummy/config/environment.rb +5 -0
  61. data/test/dummy/config/environments/development.rb +37 -0
  62. data/test/dummy/config/environments/production.rb +73 -0
  63. data/test/dummy/config/environments/test.rb +36 -0
  64. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  65. data/test/dummy/config/initializers/devise.rb +251 -0
  66. data/test/dummy/config/initializers/inflections.rb +15 -0
  67. data/test/dummy/config/initializers/mime_types.rb +5 -0
  68. data/test/dummy/config/initializers/secret_token.rb +8 -0
  69. data/test/dummy/config/initializers/session_store.rb +8 -0
  70. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  71. data/test/dummy/config/locales/en.yml +5 -0
  72. data/test/dummy/config/routes.rb +6 -0
  73. data/test/dummy/db/migrate/20130125101430_create_users.rb +9 -0
  74. data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +52 -0
  75. data/test/dummy/db/migrate/20130131142320_create_posts.rb +10 -0
  76. data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +28 -0
  77. data/test/dummy/lib/assets/.gitkeep +0 -0
  78. data/test/dummy/public/404.html +26 -0
  79. data/test/dummy/public/422.html +26 -0
  80. data/test/dummy/public/500.html +25 -0
  81. data/test/dummy/public/favicon.ico +0 -0
  82. data/test/dummy/script/rails +6 -0
  83. data/test/integration/persistence_test.rb +63 -0
  84. data/test/integration/refresh_test.rb +103 -0
  85. data/test/integration/sign_in_test.rb +85 -0
  86. data/test/integration/token_test.rb +30 -0
  87. data/test/integration_tests_helper.rb +64 -0
  88. data/test/model_tests_helper.rb +20 -0
  89. data/test/models/two_factorable_test.rb +120 -0
  90. data/test/orm/active_record.rb +4 -0
  91. data/test/orm/mongoid.rb +13 -0
  92. data/test/support/mongoid.yml +6 -0
  93. data/test/support/symmetric_encryption.yml +70 -0
  94. data/test/test_helper.rb +18 -0
  95. metadata +269 -0
@@ -0,0 +1,20 @@
1
+ <h2><%= I18n.t('title', {scope: 'devise.two_factor.credentials_refresh'}) %></h2>
2
+ <p><%= I18n.t('explain', {scope: 'devise.two_factor.credentials_refresh'}) %></p>
3
+
4
+ <%= form_for(resource, as: resource_name, url: [:refresh, resource_name, :credential], html: { method: :put }) do |f| %>
5
+
6
+ <%= devise_error_messages! %>
7
+
8
+ <div><%= f.label :email %><br />
9
+ <%= f.text_field :email, disabled: :true%></div>
10
+
11
+ <div><%= f.label :password %><br />
12
+ <%= f.password_field :refresh_password, autocomplete: :off, autofocus: true %></div>
13
+
14
+ <%- if resource.otp_enabled? %>
15
+ <div><%= f.label :token, I18n.t(:token, {scope: 'devise.two_factor.credentials_refresh'}) %></p><br />
16
+ <%= f.password_field :token, autocomplete: :off%></div>
17
+ <% end %>
18
+
19
+ <div><%= f.submit I18n.t(:go_on, {scope: 'devise.two_factor.credentials_refresh'}) %></div>
20
+ <% end %>
@@ -0,0 +1,23 @@
1
+ <h2><%= I18n.t('title', {scope: 'devise.two_factor.submit_token'}) %></h2>
2
+ <p><%= I18n.t('explain', {scope: 'devise.two_factor.submit_token'}) %></p>
3
+
4
+ <%= form_for(resource, as: resource_name, url: [resource_name, :credential], html: { method: :put }) do |f| %>
5
+
6
+ <%= f.hidden_field :challenge, {value: @challenge} %>
7
+ <%= f.hidden_field :recovery, {value: @recovery} %>
8
+
9
+ <%- if @recovery %>
10
+ <p><%= f.label :token, I18n.t('recovery_prompt', {scope: 'devise.two_factor.submit_token'}) %><br />
11
+ <%= f.text_field :otp_recovery_counter, autocomplete: :off, disabled: true, size: 4 %>
12
+ <% else %>
13
+ <p><%= f.label :token, I18n.t('prompt', {scope: 'devise.two_factor.submit_token'}) %><br />
14
+ <% end %>
15
+
16
+ <%= f.text_field :token, autocomplete: :off, autofocus: true, size: 6, value: '' %>
17
+ </p>
18
+
19
+ <p><%= f.submit I18n.t('submit', {scope: 'devise.two_factor.submit_token'}) %></p>
20
+ <%- if !@recovery && recovery_enabled? %>
21
+ <p><%= link_to I18n.t('recovery_link', {scope: 'devise.two_factor.submit_token'}), credential_path_for(resource_name, challenge: @challenge, recovery: true) %></p>
22
+ <% end %>
23
+ <% end %>
@@ -0,0 +1,19 @@
1
+ <h3><%= I18n.t('title', {scope: 'devise.two_factor.token_secret'}) %></h3>
2
+ <p><%= I18n.t('explain', {scope: 'devise.two_factor.token_secret'}) %></p>
3
+
4
+ <%= otp_authenticator_token_image(resource) %>
5
+
6
+ <p><strong><%= I18n.t('manual_provisioning', {scope: 'devise.two_factor.token_secret'}) %>:</strong>
7
+ <code><%= resource.otp_auth_secret %></code></p>
8
+
9
+ <p><%= link_to I18n.t('reset_otp', {scope: 'devise.two_factor.token_secret'}), @resource, method: :delete %></p>
10
+ <p><%= I18n.t('reset_explain', {scope: 'devise.two_factor.token_secret'}) %>
11
+ <strong><%= I18n.t('reset_explain_warn', {scope: 'devise.two_factor.token_secret'}) %></strong></p>
12
+
13
+ <%- if recovery_enabled? %>
14
+ <h3><%= I18n.t('title', {scope: 'devise.two_factor.tokens.recovery'}) %></h3>
15
+ <p><%= I18n.t('explain', {scope: 'devise.two_factor.tokens.recovery'}) %></p>
16
+ <p><%= link_to I18n.t('codes_list', {scope: 'devise.two_factor.tokens.recovery'}), recovery_token_for(resource_name) %></p>
17
+ <p><%= link_to I18n.t('download_codes', {scope: 'devise.two_factor.tokens.recovery'}), recovery_token_for(resource_name, format: :text) %></p>
18
+
19
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <h3><%= I18n.t('title', {scope: 'devise.two_factor.trusted_devices'}) %></h3>
2
+ <p><%= I18n.t('explain', {scope: 'devise.two_factor.trusted_devices'}) %></p>
3
+ <%- if is_otp_trusted_device_for? resource %>
4
+ <p><em><%= I18n.t('device_trusted', {scope: 'devise.two_factor.trusted_devices'}) %></em></p>
5
+ <p><%= link_to I18n.t('trust_remove', {scope: 'devise.two_factor.trusted_devices'}), persistence_token_path_for(resource_name), method: :post %></p>
6
+ <% else %>
7
+ <p><%= I18n.t('device_not_trusted', {scope: 'devise.two_factor.trusted_devices'}) %></p>
8
+ <p><%= link_to I18n.t('trust_add', {scope: 'devise.two_factor.trusted_devices'}), persistence_token_path_for(resource_name) %></p>
9
+ <% end %>
10
+ <p><%= link_to I18n.t('trust_clear', {scope: 'devise.two_factor.trusted_devices'}), persistence_token_path_for(resource_name), method: :delete %></p>
@@ -0,0 +1,21 @@
1
+ <h2><%= I18n.t('title', {scope: 'devise.two_factor.tokens.recovery'}) %></h2>
2
+ <p><%= I18n.t('explain', {scope: 'devise.two_factor.tokens.recovery'}) %></p>
3
+
4
+ <table>
5
+ <caption>
6
+ <thead>
7
+ <tr>
8
+ <th><%= I18n.t('sequence', {scope: 'devise.two_factor.tokens.recovery'}) %></th>
9
+ <th><%= I18n.t('code', {scope: 'devise.two_factor.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,19 @@
1
+ <h2><%= I18n.t('title', {scope: 'devise.two_factor.tokens'}) %></h2>
2
+ <p><%= I18n.t('explain', {scope: 'devise.two_factor.tokens'}) %></p>
3
+
4
+ <%= form_for(resource, as: resource_name, url: [resource_name, :token], html: { method: :put }) do |f| %>
5
+
6
+ <%= devise_error_messages! %>
7
+
8
+ <h3><%= I18n.t('enable_request', {scope: 'devise.two_factor.tokens'}) %></h3>
9
+
10
+ <p><%= f.label :otp_enabled, I18n.t('status', {scope: 'devise.two_factor.tokens'}) %><br />
11
+ <%= f.check_box :otp_enabled %></p>
12
+
13
+ <p><%= f.submit I18n.t('submit', {scope: 'devise.two_factor.tokens'}) %></p>
14
+ <% end %>
15
+
16
+ <%- if resource.otp_enabled? %>
17
+ <%= render partial: 'token_secret' if resource.otp_enabled? %>
18
+ <%= render partial: 'trusted_devices' if trusted_devices_enabled? %>
19
+ <% end %>
@@ -0,0 +1,57 @@
1
+ en:
2
+ devise:
3
+ two_factor:
4
+ submit_token:
5
+ title: 'Check Token'
6
+ explain: "A token is required because two-factor authentication is enabled on your account"
7
+ prompt: 'Please enter your two-factor authentication token:'
8
+ recovery_prompt: 'Please enter your recovery code:'
9
+ submit: 'Submit Token'
10
+ submit_recovery: 'Submit Recovery Code'
11
+ recovery_link: "I don't have my device, I want to use a recovery code"
12
+ credentials:
13
+ token_invalid: 'The token you provided was invalid.'
14
+ token_blank: 'Please provide a token generated by your device.'
15
+ need_to_refresh_credentials: 'We need to check your credentials before you can change these settings.'
16
+ valid_refresh: 'Thank you, your credentials were accepted.'
17
+ invalid_refresh: 'Sorry, you provided the wrong credentials.'
18
+ credentials_refresh:
19
+ title: 'Please enter your password again.'
20
+ explain: 'To confirm your identity, please re-enter your password.'
21
+ go_on: 'Continue'
22
+ identity: 'Identity'
23
+ token: 'Your two-factor authentication token'
24
+ token_secret:
25
+ title: 'Token Secret'
26
+ explain: 'Take a photo of this QR code with your mobile device.'
27
+ manual_provisioning: 'Manual provisioning code'
28
+ reset_otp: 'Reset your Two-Factor Authentication status'
29
+ reset_explain: 'This will reset your credentials, and disable two-factor authentication.'
30
+ reset_explain_warn: 'You will need to enroll your mobile device again.'
31
+ tokens:
32
+ title: 'Two-Factor Authentication'
33
+ explain: 'Two-Factor 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.'
34
+ enable_request: 'Would you like to enable Two-Factor Authentication?'
35
+ status: 'Enable Two-Factor Authentication'
36
+ submit: 'Continue'
37
+ successfully_updated: 'Your two-factor authentication settings have been updated.'
38
+ successfully_reset_creds: 'Your two-factor authentication credentials have been reset.'
39
+ successfully_set_persistence: 'Your device is now trusted.'
40
+ successfully_cleared_persistence: 'Your device has been removed from the list of trusted devices.'
41
+ successfully_reset_persistence: 'Your list of trusted devices has been cleared.'
42
+ need_to_refresh_credentials: 'We need to check your credentials before you can change these settings.'
43
+ recovery:
44
+ title: 'Recovery Codes'
45
+ explain: 'Store these recovery codes in a safe place. They will allow you to log back in if your token device is lost, stolen, or unavailable.'
46
+ sequence: 'Sequence'
47
+ code: 'Recovery Code'
48
+ codes_list: 'View recovery codes'
49
+ download_codes: 'Download recovery codes'
50
+ trusted_devices:
51
+ title: 'Trusted Browsers'
52
+ explain: 'If you set this browser as trusted, you will not be asked to perform two-factor authentication when logging in for one month.'
53
+ device_trusted: 'This browser is trusted.'
54
+ device_not_trusted: 'This browser is not trusted.'
55
+ trust_remove: 'Untrust this browser'
56
+ trust_add: 'Trust this browser'
57
+ trust_clear: 'Clear all trusted browsers'
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'devise-2fa/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'devise-2fa'
8
+ gem.version = Devise::TwoFactor::VERSION
9
+ gem.authors = ['William A. Todd']
10
+ gem.email = ['info@investinwaffles.com']
11
+ gem.description = 'Time Based OTP/rfc6238 authentication for Devise'
12
+ gem.summary = 'Includes ActiveRecord and Mongoid ORM support'
13
+ gem.homepage = 'http://www.github.com/williamatodd/devise-2fa'
14
+ gem.license = 'MIT'
15
+
16
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ['lib']
20
+
21
+ gem.add_runtime_dependency 'devise', '~> 3.2', '>= 3.2.0'
22
+ gem.add_runtime_dependency 'rotp', '~> 3.0'
23
+ gem.add_runtime_dependency 'rqrcode', '~> 0.10.1'
24
+ gem.add_runtime_dependency 'symmetric-encryption', '~> 3.8'
25
+
26
+ gem.add_development_dependency 'sqlite3', '~> 0'
27
+ end
@@ -0,0 +1,74 @@
1
+ module DeviseTwoFactorable
2
+ autoload :Hooks, 'devise_two_factorable/hooks'
3
+ autoload :Mapping, 'devise_two_factorable/mapping'
4
+
5
+ module Controllers
6
+ autoload :Helpers, 'devise_two_factorable/controllers/helpers'
7
+ autoload :UrlHelpers, 'devise_two_factorable/controllers/url_helpers'
8
+ end
9
+ end
10
+
11
+ require 'devise-2fa/version'
12
+ require 'active_support'
13
+ require 'active_support/core_ext'
14
+ require 'active_support/core_ext/integer'
15
+ require 'active_support/core_ext/string'
16
+ require 'active_support/ordered_hash'
17
+ require 'active_support/concern'
18
+ require 'devise_two_factorable/routes'
19
+ require 'devise_two_factorable/engine'
20
+ require 'devise'
21
+
22
+ module Devise
23
+ #
24
+ #
25
+ #
26
+ mattr_accessor :otp_mandatory
27
+ @@otp_mandatory = false
28
+
29
+ #
30
+ #
31
+ #
32
+ mattr_accessor :otp_authentication_timeout
33
+ @@otp_authentication_timeout = 3.minutes
34
+
35
+ #
36
+ #
37
+ #
38
+ mattr_accessor :otp_recovery_tokens
39
+ @@otp_recovery_tokens = 10 ## false to disable
40
+
41
+ #
42
+ # If the user is given the chance to set his browser as trusted, how long will it stay trusted.
43
+ # set to nil/false to disable the ability to set a device as trusted
44
+ #
45
+ mattr_accessor :otp_trust_persistence
46
+ @@otp_trust_persistence = 30.days
47
+
48
+ #
49
+ #
50
+ #
51
+ mattr_accessor :otp_drift_window
52
+ @@otp_drift_window = 3 # in minutes
53
+
54
+ #
55
+ # if the user wants to change Otp settings,
56
+ # ask the password (and the token) again if this time has passed since the last
57
+ # time the user has provided valid credentials
58
+ #
59
+ mattr_accessor :otp_credentials_refresh
60
+ @@otp_credentials_refresh = 15.minutes # or like 15.minutes, false to disable
61
+
62
+ #
63
+ # the name of the token issuer
64
+ #
65
+ mattr_accessor :otp_issuer
66
+ @@otp_issuer = Rails.application.class.parent_name
67
+
68
+ module TwoFactor
69
+ end
70
+ end
71
+
72
+ Devise.add_module :two_factorable,
73
+ controller: :tokens,
74
+ model: 'devise_two_factorable/models/two_factorable', route: :token
@@ -0,0 +1,5 @@
1
+ module Devise
2
+ module TwoFactor
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,136 @@
1
+ require 'rqrcode'
2
+ require 'base64'
3
+
4
+ module DeviseTwoFactorable
5
+ module Controllers
6
+ module Helpers
7
+ def authenticate_scope!
8
+ send(:"authenticate_#{resource_name}!", force: true)
9
+ self.resource = send("current_#{resource_name}")
10
+ end
11
+
12
+ #
13
+ # similar to DeviseController#set_flash_message, but sets the scope inside
14
+ # the otp controller
15
+ #
16
+ def otp_set_flash_message(key, kind, options = {})
17
+ options[:scope] ||= "devise.two_factor.#{controller_name}"
18
+ options[:default] = Array(options[:default]).unshift(kind.to_sym)
19
+ options[:resource_name] = resource_name
20
+ options = devise_i18n_options(options) if respond_to?(:devise_i18n_options, true)
21
+ message = I18n.t("#{options[:resource_name]}.#{kind}", options)
22
+ flash[key] = message if message.present?
23
+ end
24
+
25
+ def otp_t
26
+ end
27
+
28
+ def trusted_devices_enabled?
29
+ resource.class.otp_trust_persistence && (resource.class.otp_trust_persistence > 0)
30
+ end
31
+
32
+ def recovery_enabled?
33
+ resource_class.otp_recovery_tokens && (resource_class.otp_recovery_tokens > 0)
34
+ end
35
+
36
+ #
37
+ # Sanity check for resource validity
38
+ #
39
+ def ensure_resource!
40
+ raise ArgumentError, 'Should not happen' if resource.nil?
41
+ end
42
+
43
+ # FIXME: do cookies and persistence need to be scoped? probably
44
+
45
+ #
46
+ # check if the resource needs a credentials refresh. IE, they need to be asked a password again to access
47
+ # this resource.
48
+ #
49
+ def needs_credentials_refresh?(resource)
50
+ return false unless resource.class.otp_credentials_refresh
51
+
52
+ (!session[otp_scoped_refresh_property].present? ||
53
+ (session[otp_scoped_refresh_property] < DateTime.now)).tap { |need| otp_set_refresh_return_url if need }
54
+ end
55
+
56
+ #
57
+ # credentials are refreshed
58
+ #
59
+ def otp_refresh_credentials_for(resource)
60
+ return false unless resource.class.otp_credentials_refresh
61
+ session[otp_scoped_refresh_property] = (Time.now + resource.class.otp_credentials_refresh)
62
+ end
63
+
64
+ #
65
+ # is the current browser trusted?
66
+ #
67
+ def is_otp_trusted_device_for?(resource)
68
+ return false unless resource.class.otp_trust_persistence
69
+ if cookies[otp_scoped_persistence_cookie].present?
70
+ cookies.signed[otp_scoped_persistence_cookie] ==
71
+ [resource.to_key, resource.authenticatable_salt, resource.otp_persistence_seed]
72
+ else
73
+ false
74
+ end
75
+ end
76
+
77
+ #
78
+ # make the current browser trusted
79
+ #
80
+ def otp_set_trusted_device_for(resource)
81
+ return unless resource.class.otp_trust_persistence
82
+ cookies.signed[otp_scoped_persistence_cookie] = {
83
+ httponly: true,
84
+ expires: Time.now + resource.class.otp_trust_persistence,
85
+ value: [resource.to_key, resource.authenticatable_salt, resource.otp_persistence_seed]
86
+ }
87
+ end
88
+
89
+ def otp_set_refresh_return_url
90
+ session[otp_scoped_refresh_return_url_property] = request.fullpath
91
+ end
92
+
93
+ def otp_fetch_refresh_return_url
94
+ session.delete(otp_scoped_refresh_return_url_property) { :root }
95
+ end
96
+
97
+ def otp_scoped_refresh_return_url_property
98
+ "otp_#{resource_name}refresh_return_url".to_sym
99
+ end
100
+
101
+ def otp_scoped_refresh_property
102
+ "otp_#{resource_name}refresh_after".to_sym
103
+ end
104
+
105
+ def otp_scoped_persistence_cookie
106
+ "otp_#{resource_name}_device_trusted"
107
+ end
108
+
109
+ #
110
+ # make the current browser NOT trusted
111
+ #
112
+ def otp_clear_trusted_device_for(_resource)
113
+ cookies.delete(otp_scoped_persistence_cookie)
114
+ end
115
+
116
+ #
117
+ # clears the persistence list for this kind of resource
118
+ #
119
+ def otp_reset_persistence_for(resource)
120
+ otp_clear_trusted_device_for(resource)
121
+ resource.reset_otp_persistence!
122
+ end
123
+
124
+ #
125
+ # returns the URL for the QR Code to initialize the Authenticator device
126
+ #
127
+ def otp_authenticator_token_image(resource)
128
+ data = resource.otp_provisioning_uri
129
+ qrcode = RQRCode::QRCode.new(data, level: :m, mode: :byte_8bit)
130
+ png = qrcode.as_png(fill: 'white', color: 'black', border_modules: 1, module_px_size: 4)
131
+ url = "data:image/png;base64,#{Base64.encode64(png.to_s).strip}"
132
+ image_tag(url, alt: 'OTP Authenticator QRCode')
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,30 @@
1
+ module DeviseTwoFactorable
2
+ module Controllers
3
+ module UrlHelpers
4
+ def recovery_token_for(resource_or_scope, opts = {})
5
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
6
+ send("recovery_#{scope}_token_path", opts)
7
+ end
8
+
9
+ def refresh_credential_path_for(resource_or_scope, opts = {})
10
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
11
+ send("refresh_#{scope}_credential_path", opts)
12
+ end
13
+
14
+ def persistence_token_path_for(resource_or_scope, opts = {})
15
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
16
+ send("persistence_#{scope}_token_path", opts)
17
+ end
18
+
19
+ def token_path_for(resource_or_scope, opts = {})
20
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
21
+ send("#{scope}_token_path", opts)
22
+ end
23
+
24
+ def credential_path_for(resource_or_scope, opts = {})
25
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
26
+ send("#{scope}_credential_path", opts)
27
+ end
28
+ end
29
+ end
30
+ end