devise-otp-rails5 0.2.4
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.
- checksums.yaml +7 -0
- data/.gitignore +42 -0
- data/.travis.yml +12 -0
- data/Gemfile +25 -0
- data/LICENSE.txt +22 -0
- data/README.md +140 -0
- data/Rakefile +42 -0
- data/app/assets/javascripts/devise-otp.js +1 -0
- data/app/assets/javascripts/qrcode.js +609 -0
- data/app/controllers/devise_otp/credentials_controller.rb +106 -0
- data/app/controllers/devise_otp/tokens_controller.rb +111 -0
- data/app/views/devise_otp/credentials/refresh.html.erb +20 -0
- data/app/views/devise_otp/credentials/show.html.erb +23 -0
- data/app/views/devise_otp/tokens/_token_secret.html.erb +19 -0
- data/app/views/devise_otp/tokens/_trusted_devices.html.erb +10 -0
- data/app/views/devise_otp/tokens/recovery.html.erb +21 -0
- data/app/views/devise_otp/tokens/recovery_codes.text.erb +3 -0
- data/app/views/devise_otp/tokens/show.html.erb +19 -0
- data/config/locales/en.yml +66 -0
- data/devise-otp.gemspec +25 -0
- data/lib/devise-otp.rb +83 -0
- data/lib/devise-otp/version.rb +5 -0
- data/lib/devise_otp_authenticatable/controllers/helpers.rb +168 -0
- data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +33 -0
- data/lib/devise_otp_authenticatable/engine.rb +23 -0
- data/lib/devise_otp_authenticatable/hooks.rb +13 -0
- data/lib/devise_otp_authenticatable/hooks/sessions.rb +59 -0
- data/lib/devise_otp_authenticatable/mapping.rb +19 -0
- data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +137 -0
- data/lib/devise_otp_authenticatable/routes.rb +32 -0
- data/lib/generators/active_record/devise_otp_generator.rb +13 -0
- data/lib/generators/active_record/templates/migration.rb +27 -0
- data/lib/generators/devise_otp/devise_otp_generator.rb +17 -0
- data/lib/generators/devise_otp/install_generator.rb +53 -0
- data/lib/generators/devise_otp/views_generator.rb +19 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +4 -0
- data/test/dummy/app/controllers/posts_controller.rb +83 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/helpers/posts_helper.rb +2 -0
- data/test/dummy/app/mailers/.gitkeep +0 -0
- data/test/dummy/app/models/post.rb +2 -0
- data/test/dummy/app/models/user.rb +20 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/app/views/posts/_form.html.erb +25 -0
- data/test/dummy/app/views/posts/edit.html.erb +6 -0
- data/test/dummy/app/views/posts/index.html.erb +25 -0
- data/test/dummy/app/views/posts/new.html.erb +5 -0
- data/test/dummy/app/views/posts/show.html.erb +15 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +67 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +30 -0
- data/test/dummy/config/environments/production.rb +69 -0
- data/test/dummy/config/environments/test.rb +36 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/devise.rb +253 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +8 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +6 -0
- data/test/dummy/db/migrate/20130125101430_create_users.rb +9 -0
- data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +53 -0
- data/test/dummy/db/migrate/20130131142320_create_posts.rb +10 -0
- data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +28 -0
- data/test/dummy/lib/assets/.gitkeep +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/integration/persistence_test.rb +65 -0
- data/test/integration/refresh_test.rb +106 -0
- data/test/integration/sign_in_test.rb +87 -0
- data/test/integration/token_test.rb +34 -0
- data/test/integration_tests_helper.rb +66 -0
- data/test/model_tests_helper.rb +22 -0
- data/test/models/otp_authenticatable_test.rb +122 -0
- data/test/orm/active_record.rb +4 -0
- data/test/test_helper.rb +22 -0
- metadata +253 -0
@@ -0,0 +1,168 @@
|
|
1
|
+
module DeviseOtpAuthenticatable
|
2
|
+
|
3
|
+
module Controllers
|
4
|
+
module Helpers
|
5
|
+
|
6
|
+
|
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.otp.#{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
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def trusted_devices_enabled?
|
31
|
+
resource.class.otp_trust_persistence && (resource.class.otp_trust_persistence > 0)
|
32
|
+
end
|
33
|
+
|
34
|
+
def recovery_enabled?
|
35
|
+
resource_class.otp_recovery_tokens && (resource_class.otp_recovery_tokens > 0)
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Sanity check for resource validity
|
40
|
+
#
|
41
|
+
def ensure_resource!
|
42
|
+
if resource.nil?
|
43
|
+
raise ArgumentError, "Should not happen"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# fixme do cookies and persistence need to be scoped? probably
|
49
|
+
|
50
|
+
#
|
51
|
+
# check if the resource needs a credentials refresh. IE, they need to be asked a password again to access
|
52
|
+
# this resource.
|
53
|
+
#
|
54
|
+
def needs_credentials_refresh?(resource)
|
55
|
+
return false unless resource.class.otp_credentials_refresh
|
56
|
+
|
57
|
+
(!session[otp_scoped_refresh_property].present? ||
|
58
|
+
(session[otp_scoped_refresh_property] < DateTime.now)).tap { |need| otp_set_refresh_return_url if need }
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# credentials are refreshed
|
63
|
+
#
|
64
|
+
def otp_refresh_credentials_for(resource)
|
65
|
+
return false unless resource.class.otp_credentials_refresh
|
66
|
+
session[otp_scoped_refresh_property] = (Time.now + resource.class.otp_credentials_refresh)
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
#
|
71
|
+
# is the current browser trusted?
|
72
|
+
#
|
73
|
+
def is_otp_trusted_device_for?(resource)
|
74
|
+
return false unless resource.class.otp_trust_persistence
|
75
|
+
if cookies[otp_scoped_persistence_cookie].present?
|
76
|
+
cookies.signed[otp_scoped_persistence_cookie] ==
|
77
|
+
[resource.to_key, resource.authenticatable_salt, resource.otp_persistence_seed]
|
78
|
+
else
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# make the current browser trusted
|
85
|
+
#
|
86
|
+
def otp_set_trusted_device_for(resource)
|
87
|
+
return unless resource.class.otp_trust_persistence
|
88
|
+
cookies.signed[otp_scoped_persistence_cookie] = {
|
89
|
+
:httponly => true,
|
90
|
+
:expires => Time.now + resource.class.otp_trust_persistence,
|
91
|
+
:value => [resource.to_key, resource.authenticatable_salt, resource.otp_persistence_seed]
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
def otp_set_refresh_return_url
|
96
|
+
session[otp_scoped_refresh_return_url_property] = request.fullpath
|
97
|
+
end
|
98
|
+
|
99
|
+
def otp_fetch_refresh_return_url
|
100
|
+
session.delete(otp_scoped_refresh_return_url_property) { :root }
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
def otp_scoped_refresh_return_url_property
|
105
|
+
"otp_#{resource_name}refresh_return_url".to_sym
|
106
|
+
end
|
107
|
+
|
108
|
+
def otp_scoped_refresh_property
|
109
|
+
"otp_#{resource_name}refresh_after".to_sym
|
110
|
+
end
|
111
|
+
|
112
|
+
def otp_scoped_persistence_cookie
|
113
|
+
"otp_#{resource_name}_device_trusted"
|
114
|
+
end
|
115
|
+
|
116
|
+
#
|
117
|
+
# make the current browser NOT trusted
|
118
|
+
#
|
119
|
+
def otp_clear_trusted_device_for(resource)
|
120
|
+
cookies.delete(otp_scoped_persistence_cookie)
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
#
|
125
|
+
# clears the persistence list for this kind of resource
|
126
|
+
#
|
127
|
+
def otp_reset_persistence_for(resource)
|
128
|
+
otp_clear_trusted_device_for(resource)
|
129
|
+
resource.reset_otp_persistence!
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# returns the URL for the QR Code to initialize the Authenticator device
|
134
|
+
#
|
135
|
+
def otp_authenticator_token_image(resource)
|
136
|
+
otp_authenticator_token_image_js(resource.otp_provisioning_uri)
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def otp_authenticator_token_image_js(otp_url)
|
142
|
+
|
143
|
+
content_tag(:div, :class => 'qrcode-container') do
|
144
|
+
tag(:div, :id => 'qrcode', :class => 'qrcode') +
|
145
|
+
javascript_tag(%Q[
|
146
|
+
|
147
|
+
new QRCode("qrcode", {
|
148
|
+
text: "#{otp_url}",
|
149
|
+
width: 256,
|
150
|
+
height: 256,
|
151
|
+
colorDark : "#000000",
|
152
|
+
colorLight : "#ffffff",
|
153
|
+
correctLevel : QRCode.CorrectLevel.H
|
154
|
+
});
|
155
|
+
]) + tag("/div")
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
def otp_authenticator_token_image_google(otp_url)
|
161
|
+
otp_url = Rack::Utils.escape(otp_url)
|
162
|
+
url = "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=#{otp_url}"
|
163
|
+
image_tag(url, :alt => 'OTP Url QRCode')
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module DeviseOtpAuthenticatable
|
2
|
+
module Controllers
|
3
|
+
|
4
|
+
module UrlHelpers
|
5
|
+
|
6
|
+
def recovery_otp_token_for(resource_or_scope, opts = {})
|
7
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
8
|
+
send("recovery_#{scope}_otp_token_path", opts)
|
9
|
+
end
|
10
|
+
|
11
|
+
def refresh_otp_credential_path_for(resource_or_scope, opts = {})
|
12
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
13
|
+
send("refresh_#{scope}_otp_credential_path", opts)
|
14
|
+
end
|
15
|
+
|
16
|
+
def persistence_otp_token_path_for(resource_or_scope, opts = {})
|
17
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
18
|
+
send("persistence_#{scope}_otp_token_path", opts)
|
19
|
+
end
|
20
|
+
|
21
|
+
def otp_token_path_for(resource_or_scope, opts = {})
|
22
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
23
|
+
send("#{scope}_otp_token_path", opts)
|
24
|
+
end
|
25
|
+
|
26
|
+
def otp_credential_path_for(resource_or_scope, opts = {})
|
27
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
28
|
+
send("#{scope}_otp_credential_path", opts)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module DeviseOtpAuthenticatable
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
|
4
|
+
ActiveSupport.on_load(:action_controller) do
|
5
|
+
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
|
6
|
+
include DeviseOtpAuthenticatable::Controllers::Helpers
|
7
|
+
end
|
8
|
+
ActiveSupport.on_load(:action_view) do
|
9
|
+
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
|
10
|
+
include DeviseOtpAuthenticatable::Controllers::Helpers
|
11
|
+
end
|
12
|
+
|
13
|
+
# We use to_prepare instead of after_initialize here because Devise is a Rails engine;
|
14
|
+
config.to_prepare do
|
15
|
+
DeviseOtpAuthenticatable::Hooks.apply
|
16
|
+
end
|
17
|
+
|
18
|
+
# extend mapping with after_initialize because is not reloaded
|
19
|
+
config.after_initialize do
|
20
|
+
Devise::Mapping.send :include, DeviseOtpAuthenticatable::Mapping
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module DeviseOtpAuthenticatable::Hooks
|
2
|
+
module Sessions
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
|
5
|
+
|
6
|
+
included do
|
7
|
+
alias_method_chain :create, :otp
|
8
|
+
end
|
9
|
+
|
10
|
+
#
|
11
|
+
# replaces Devise::SessionsController#create
|
12
|
+
#
|
13
|
+
def create_with_otp
|
14
|
+
|
15
|
+
resource = warden.authenticate!(auth_options)
|
16
|
+
|
17
|
+
devise_stored_location = stored_location_for(resource) # Grab the current stored location before it gets lost by warden.logout
|
18
|
+
|
19
|
+
otp_refresh_credentials_for(resource)
|
20
|
+
|
21
|
+
if otp_challenge_required_on?(resource)
|
22
|
+
challenge = resource.generate_otp_challenge!
|
23
|
+
warden.logout
|
24
|
+
store_location_for(resource, devise_stored_location) # restore the stored location
|
25
|
+
respond_with resource, :location => otp_credential_path_for(resource, {:challenge => challenge})
|
26
|
+
elsif otp_mandatory_on?(resource) # if mandatory, log in user but send him to the must activate otp
|
27
|
+
set_flash_message(:notice, :signed_in_but_otp) if is_navigational_format?
|
28
|
+
sign_in(resource_name, resource)
|
29
|
+
respond_with resource, :location => otp_token_path_for(resource)
|
30
|
+
else
|
31
|
+
|
32
|
+
set_flash_message(:notice, :signed_in) if is_navigational_format?
|
33
|
+
sign_in(resource_name, resource)
|
34
|
+
respond_with resource, :location => after_sign_in_path_for(resource)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
#
|
42
|
+
# resource should be challenged for otp
|
43
|
+
#
|
44
|
+
def otp_challenge_required_on?(resource)
|
45
|
+
return false unless resource.respond_to?(:otp_enabled) && resource.respond_to?(:otp_auth_secret)
|
46
|
+
resource.otp_enabled && !is_otp_trusted_device_for?(resource)
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# the resource -should- have otp turned on, but it isn't
|
51
|
+
#
|
52
|
+
def otp_mandatory_on?(resource)
|
53
|
+
return true if resource.class.otp_mandatory
|
54
|
+
return false unless resource.respond_to?(:otp_mandatory)
|
55
|
+
|
56
|
+
resource.otp_mandatory && !resource.otp_enabled
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DeviseOtpAuthenticatable
|
2
|
+
|
3
|
+
module Mapping
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.alias_method_chain :default_controllers, :otp
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def default_controllers_with_otp(options)
|
11
|
+
options[:controllers] ||= {}
|
12
|
+
|
13
|
+
options[:controllers][:otp_tokens] ||= "tokens"
|
14
|
+
options[:controllers][:otp_credentials] ||= "credentials"
|
15
|
+
|
16
|
+
default_controllers_without_otp(options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'rotp'
|
2
|
+
|
3
|
+
module Devise::Models
|
4
|
+
module OtpAuthenticatable
|
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, lambda { |time| where('otp_challenge_expires > ?', time) }
|
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(Time.now).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}")
|
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
|
+
|
39
|
+
def reset_otp_credentials
|
40
|
+
@time_based_otp = nil
|
41
|
+
@recovery_otp = nil
|
42
|
+
generate_otp_auth_secret
|
43
|
+
reset_otp_persistence
|
44
|
+
update_attributes!(:otp_enabled => false,
|
45
|
+
:otp_session_challenge => nil, :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
|
+
if otp_persistence_seed.nil?
|
65
|
+
reset_otp_credentials!
|
66
|
+
end
|
67
|
+
|
68
|
+
update_attributes!(:otp_enabled => true, :otp_enabled_on => Time.now)
|
69
|
+
end
|
70
|
+
|
71
|
+
def disable_otp!
|
72
|
+
update_attributes!(:otp_enabled => false, :otp_enabled_on => nil)
|
73
|
+
end
|
74
|
+
|
75
|
+
def generate_otp_challenge!(expires = nil)
|
76
|
+
update_attributes!(:otp_session_challenge => SecureRandom.hex,
|
77
|
+
:otp_challenge_expires => DateTime.now + (expires || self.class.otp_authentication_timeout))
|
78
|
+
otp_session_challenge
|
79
|
+
end
|
80
|
+
|
81
|
+
def otp_challenge_valid?
|
82
|
+
(otp_challenge_expires.nil? || otp_challenge_expires > Time.now)
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
def validate_otp_token(token, recovery = false)
|
87
|
+
if recovery
|
88
|
+
validate_otp_recovery_token token
|
89
|
+
else
|
90
|
+
validate_otp_time_token token
|
91
|
+
end
|
92
|
+
end
|
93
|
+
alias_method :valid_otp_token?, :validate_otp_token
|
94
|
+
|
95
|
+
def validate_otp_time_token(token)
|
96
|
+
return false if token.blank?
|
97
|
+
validate_otp_token_with_drift(token)
|
98
|
+
end
|
99
|
+
alias_method :valid_otp_time_token?, :validate_otp_time_token
|
100
|
+
|
101
|
+
def next_otp_recovery_tokens(number = self.class.otp_recovery_tokens)
|
102
|
+
(otp_recovery_counter..otp_recovery_counter + number).inject({}) do |h, index|
|
103
|
+
h[index] = recovery_otp.at(index)
|
104
|
+
h
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def validate_otp_recovery_token(token)
|
109
|
+
recovery_otp.verify(token, otp_recovery_counter).tap do
|
110
|
+
self.otp_recovery_counter += 1
|
111
|
+
save!
|
112
|
+
end
|
113
|
+
end
|
114
|
+
alias_method :valid_otp_recovery_token?, :validate_otp_recovery_token
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def validate_otp_token_with_drift(token)
|
121
|
+
|
122
|
+
# should be centered around saved drift
|
123
|
+
(-self.class.otp_drift_window..self.class.otp_drift_window).any? {|drift|
|
124
|
+
(time_based_otp.verify(token, Time.now.ago(30 * drift))) }
|
125
|
+
end
|
126
|
+
|
127
|
+
def generate_otp_persistence_seed
|
128
|
+
self.otp_persistence_seed = SecureRandom.hex
|
129
|
+
end
|
130
|
+
|
131
|
+
def generate_otp_auth_secret
|
132
|
+
self.otp_auth_secret = ROTP::Base32.random_base32
|
133
|
+
self.otp_recovery_secret = ROTP::Base32.random_base32
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|