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,22 @@
1
+ module DeviseTwoFactorable
2
+ class Engine < ::Rails::Engine
3
+ ActiveSupport.on_load(:action_controller) do
4
+ include DeviseTwoFactorable::Controllers::UrlHelpers
5
+ include DeviseTwoFactorable::Controllers::Helpers
6
+ end
7
+ ActiveSupport.on_load(:action_view) do
8
+ include DeviseTwoFactorable::Controllers::UrlHelpers
9
+ include DeviseTwoFactorable::Controllers::Helpers
10
+ end
11
+
12
+ # We use to_prepare instead of after_initialize here because Devise is a Rails engine;
13
+ config.to_prepare do
14
+ DeviseTwoFactorable::Hooks.apply
15
+ end
16
+
17
+ # extend mapping with after_initialize because is not reloaded
18
+ config.after_initialize do
19
+ Devise::Mapping.send :prepend, DeviseTwoFactorable::Mapping
20
+ end
21
+ end
22
+ 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,11 @@
1
+ module DeviseTwoFactorable
2
+ module Hooks
3
+ autoload :Sessions, 'devise_two_factorable/hooks/sessions.rb'
4
+
5
+ class << self
6
+ def apply
7
+ Devise::SessionsController.send :prepend, Hooks::Sessions
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,49 @@
1
+ module DeviseTwoFactorable::Hooks
2
+ module Sessions
3
+ extend ActiveSupport::Concern
4
+ include DeviseTwoFactorable::Controllers::UrlHelpers
5
+ #
6
+ # replaces Devise::SessionsController#create
7
+ #
8
+ def create
9
+ resource = warden.authenticate!(auth_options)
10
+
11
+ devise_stored_location = stored_location_for(resource) # Grab the current stored location before it gets lost by warden.logout
12
+
13
+ otp_refresh_credentials_for(resource)
14
+
15
+ if otp_challenge_required_on?(resource)
16
+ challenge = resource.generate_otp_challenge!
17
+ warden.logout
18
+ store_location_for(resource, devise_stored_location) # restore the stored location
19
+ respond_with resource, location: credential_path_for(resource, challenge: challenge)
20
+ elsif otp_mandatory_on?(resource) # if mandatory, log in user but send him to the must activate otp
21
+ set_flash_message(:notice, :signed_in_but_otp) if is_navigational_format?
22
+ sign_in(resource_name, resource)
23
+ respond_with resource, location: token_path_for(resource)
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ #
32
+ # resource should be challenged for otp
33
+ #
34
+ def otp_challenge_required_on?(resource)
35
+ return false unless resource.respond_to?(:otp_enabled) && resource.respond_to?(:otp_auth_secret)
36
+ resource.otp_enabled && !is_otp_trusted_device_for?(resource)
37
+ end
38
+
39
+ #
40
+ # the resource -should- have otp turned on, but it isn't
41
+ #
42
+ def otp_mandatory_on?(resource)
43
+ return true if resource.class.otp_mandatory
44
+ return false unless resource.respond_to?(:otp_mandatory)
45
+
46
+ resource.otp_mandatory && !resource.otp_enabled
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,12 @@
1
+ module DeviseTwoFactorable
2
+ module Mapping
3
+ private
4
+
5
+ def default_controllers(options)
6
+ options[:controllers] ||= {}
7
+ options[:controllers][:tokens] ||= 'devise/tokens'
8
+ options[:controllers][:credentials] ||= 'devise/credentials'
9
+ super
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,131 @@
1
+ require 'rotp'
2
+
3
+ module Devise::Models
4
+ module TwoFactorable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_validation :generate_otp_auth_secret, on: :create
9
+ before_validation :generate_otp_persistence_seed, on: :create
10
+ scope :with_valid_otp_challenge, ->{ where(:otp_challenge_expires.gt => Time.current) }
11
+ end
12
+
13
+ module ClassMethods
14
+ ::Devise::Models.config(self, :otp_authentication_timeout, :otp_drift_window, :otp_trust_persistence,
15
+ :otp_mandatory, :otp_credentials_refresh, :otp_issuer, :otp_recovery_tokens)
16
+
17
+ def find_valid_otp_challenge(challenge)
18
+ with_valid_otp_challenge.where(otp_session_challenge: challenge).first
19
+ end
20
+ end
21
+
22
+ def time_based_otp
23
+ @time_based_otp ||= ROTP::TOTP.new(otp_auth_secret, issuer: (self.class.otp_issuer || Rails.application.class.parent_name).to_s)
24
+ end
25
+
26
+ def recovery_otp
27
+ @recovery_otp ||= ROTP::HOTP.new(otp_recovery_secret)
28
+ end
29
+
30
+ def otp_provisioning_uri
31
+ time_based_otp.provisioning_uri(otp_provisioning_identifier)
32
+ end
33
+
34
+ def otp_provisioning_identifier
35
+ email
36
+ end
37
+
38
+ def reset_otp_credentials
39
+ @time_based_otp = nil
40
+ @recovery_otp = nil
41
+ generate_otp_auth_secret
42
+ reset_otp_persistence
43
+ update_attributes!(otp_enabled: false,
44
+ otp_session_challenge: nil,
45
+ otp_challenge_expires: nil,
46
+ otp_recovery_counter: 0)
47
+ end
48
+
49
+ def reset_otp_credentials!
50
+ reset_otp_credentials
51
+ save!
52
+ end
53
+
54
+ def reset_otp_persistence
55
+ generate_otp_persistence_seed
56
+ end
57
+
58
+ def reset_otp_persistence!
59
+ reset_otp_persistence
60
+ save!
61
+ end
62
+
63
+ def enable_otp!
64
+ reset_otp_credentials! if otp_persistence_seed.nil?
65
+
66
+ update_attributes!(otp_enabled: true, otp_enabled_on: Time.now)
67
+ end
68
+
69
+ def disable_otp!
70
+ update_attributes!(otp_enabled: false, otp_enabled_on: nil)
71
+ end
72
+
73
+ def generate_otp_challenge!(expires = nil)
74
+ update_attributes!(otp_session_challenge: SecureRandom.hex,
75
+ otp_challenge_expires: DateTime.now + (expires || self.class.otp_authentication_timeout))
76
+ otp_session_challenge
77
+ end
78
+
79
+ def otp_challenge_valid?
80
+ (otp_challenge_expires.nil? || otp_challenge_expires > Time.now)
81
+ end
82
+
83
+ def validate_otp_token(token, recovery = false)
84
+ if recovery
85
+ validate_otp_recovery_token token
86
+ else
87
+ validate_otp_time_token token
88
+ end
89
+ end
90
+ alias valid_otp_token? validate_otp_token
91
+
92
+ def validate_otp_time_token(token)
93
+ return false if token.blank?
94
+ validate_otp_token_with_drift(token)
95
+ end
96
+ alias valid_otp_time_token? validate_otp_time_token
97
+
98
+ def next_otp_recovery_tokens(number = self.class.otp_recovery_tokens)
99
+ (otp_recovery_counter..otp_recovery_counter + number).inject({}) do |h, index|
100
+ h[index] = recovery_otp.at(index)
101
+ h
102
+ end
103
+ end
104
+
105
+ def validate_otp_recovery_token(token)
106
+ recovery_otp.verify(token, otp_recovery_counter).tap do
107
+ self.otp_recovery_counter += 1
108
+ save!
109
+ end
110
+ end
111
+ alias valid_otp_recovery_token? validate_otp_recovery_token
112
+
113
+ private
114
+
115
+ def validate_otp_token_with_drift(token)
116
+ # should be centered around saved drift
117
+ (-self.class.otp_drift_window..self.class.otp_drift_window).any? do |drift|
118
+ time_based_otp.verify(token, Time.now.ago(30 * drift))
119
+ end
120
+ end
121
+
122
+ def generate_otp_persistence_seed
123
+ self.otp_persistence_seed = SecureRandom.hex
124
+ end
125
+
126
+ def generate_otp_auth_secret
127
+ self.otp_auth_secret = ROTP::Base32.random_base32
128
+ self.otp_recovery_secret = ROTP::Base32.random_base32
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,26 @@
1
+ module ActionDispatch::Routing
2
+ class Mapper
3
+ protected
4
+
5
+ #########
6
+
7
+ def devise_token(mapping, controllers)
8
+ resource :token, only: [:show, :update, :destroy],
9
+ path: mapping.path_names[:token], controller: controllers[:tokens] do
10
+ if Devise.otp_trust_persistence
11
+ get :persistence, action: 'get_persistence'
12
+ post :persistence, action: 'clear_persistence'
13
+ delete :persistence, action: 'delete_persistence'
14
+ end
15
+
16
+ get :recovery
17
+ end
18
+
19
+ resource :credential, only: [:show, :update],
20
+ path: mapping.path_names[:credential], controller: controllers[:credentials] do
21
+ get :refresh, action: 'get_refresh'
22
+ put :refresh, action: 'set_refresh'
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,131 @@
1
+ require 'rotp'
2
+
3
+ module Devise::Models
4
+ module TwoFactorable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_validation :generate_otp_auth_secret, on: :create
9
+ before_validation :generate_otp_persistence_seed, on: :create
10
+ scope :with_valid_otp_challenge, ->{ where(:otp_challenge_expires.gt => Time.current) }
11
+ end
12
+
13
+ module ClassMethods
14
+ ::Devise::Models.config(self, :otp_authentication_timeout, :otp_drift_window, :otp_trust_persistence,
15
+ :otp_mandatory, :otp_credentials_refresh, :otp_issuer, :otp_recovery_tokens)
16
+
17
+ def find_valid_otp_challenge(challenge)
18
+ with_valid_otp_challenge.where(otp_session_challenge: challenge).first
19
+ end
20
+ end
21
+
22
+ def time_based_otp
23
+ @time_based_otp ||= ROTP::TOTP.new(otp_auth_secret, issuer: (self.class.otp_issuer || Rails.application.class.parent_name).to_s)
24
+ end
25
+
26
+ def recovery_otp
27
+ @recovery_otp ||= ROTP::HOTP.new(otp_recovery_secret)
28
+ end
29
+
30
+ def otp_provisioning_uri
31
+ time_based_otp.provisioning_uri(otp_provisioning_identifier)
32
+ end
33
+
34
+ def otp_provisioning_identifier
35
+ email
36
+ end
37
+
38
+ def reset_otp_credentials
39
+ @time_based_otp = nil
40
+ @recovery_otp = nil
41
+ generate_otp_auth_secret
42
+ reset_otp_persistence
43
+ update_attributes!(otp_enabled: false,
44
+ otp_session_challenge: nil,
45
+ otp_challenge_expires: nil,
46
+ otp_recovery_counter: 0)
47
+ end
48
+
49
+ def reset_otp_credentials!
50
+ reset_otp_credentials
51
+ save!
52
+ end
53
+
54
+ def reset_otp_persistence
55
+ generate_otp_persistence_seed
56
+ end
57
+
58
+ def reset_otp_persistence!
59
+ reset_otp_persistence
60
+ save!
61
+ end
62
+
63
+ def enable_otp!
64
+ reset_otp_credentials! if otp_persistence_seed.nil?
65
+
66
+ update_attributes!(otp_enabled: true, otp_enabled_on: Time.now)
67
+ end
68
+
69
+ def disable_otp!
70
+ update_attributes!(otp_enabled: false, otp_enabled_on: nil)
71
+ end
72
+
73
+ def generate_otp_challenge!(expires = nil)
74
+ update_attributes!(otp_session_challenge: SecureRandom.hex,
75
+ otp_challenge_expires: DateTime.now + (expires || self.class.otp_authentication_timeout))
76
+ otp_session_challenge
77
+ end
78
+
79
+ def otp_challenge_valid?
80
+ (otp_challenge_expires.nil? || otp_challenge_expires > Time.now)
81
+ end
82
+
83
+ def validate_otp_token(token, recovery = false)
84
+ if recovery
85
+ validate_otp_recovery_token token
86
+ else
87
+ validate_otp_time_token token
88
+ end
89
+ end
90
+ alias valid_otp_token? validate_otp_token
91
+
92
+ def validate_otp_time_token(token)
93
+ return false if token.blank?
94
+ validate_otp_token_with_drift(token)
95
+ end
96
+ alias valid_otp_time_token? validate_otp_time_token
97
+
98
+ def next_otp_recovery_tokens(number = self.class.otp_recovery_tokens)
99
+ (otp_recovery_counter..otp_recovery_counter + number).inject({}) do |h, index|
100
+ h[index] = recovery_otp.at(index)
101
+ h
102
+ end
103
+ end
104
+
105
+ def validate_otp_recovery_token(token)
106
+ recovery_otp.verify(token, otp_recovery_counter).tap do
107
+ self.otp_recovery_counter += 1
108
+ save!
109
+ end
110
+ end
111
+ alias valid_otp_recovery_token? validate_otp_recovery_token
112
+
113
+ private
114
+
115
+ def validate_otp_token_with_drift(token)
116
+ # should be centered around saved drift
117
+ (-self.class.otp_drift_window..self.class.otp_drift_window).any? do |drift|
118
+ time_based_otp.verify(token, Time.now.ago(30 * drift))
119
+ end
120
+ end
121
+
122
+ def generate_otp_persistence_seed
123
+ self.otp_persistence_seed = SecureRandom.hex
124
+ end
125
+
126
+ def generate_otp_auth_secret
127
+ self.otp_auth_secret = ROTP::Base32.random_base32
128
+ self.otp_recovery_secret = ROTP::Base32.random_base32
129
+ end
130
+ end
131
+ end