devise-otp 0.2.0 → 0.3.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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +36 -0
- data/.gitignore +0 -0
- data/Gemfile +1 -22
- data/LICENSE.txt +0 -0
- data/README.md +43 -66
- data/Rakefile +0 -0
- data/app/assets/javascripts/devise-otp.js +1 -0
- data/app/assets/javascripts/qrcode.js +609 -0
- data/app/controllers/devise_otp/devise/credentials_controller.rb +102 -0
- data/app/controllers/devise_otp/devise/tokens_controller.rb +112 -0
- data/app/views/devise/credentials/refresh.html.erb +19 -0
- data/app/views/devise/credentials/show.html.erb +31 -0
- data/app/views/devise/tokens/_token_secret.html.erb +23 -0
- data/app/views/devise/tokens/_trusted_devices.html.erb +12 -0
- data/app/views/devise/tokens/recovery.html.erb +21 -0
- data/app/views/devise/tokens/recovery_codes.text.erb +3 -0
- data/app/views/devise/tokens/show.html.erb +21 -0
- data/config/locales/en.yml +10 -10
- data/devise-otp.gemspec +14 -9
- data/docs/QR_CODES.md +48 -0
- data/lib/devise-otp/version.rb +1 -1
- data/lib/devise-otp.rb +22 -14
- data/lib/devise_otp_authenticatable/controllers/helpers.rb +29 -16
- data/lib/devise_otp_authenticatable/controllers/url_helpers.rb +6 -9
- data/lib/devise_otp_authenticatable/engine.rb +22 -13
- data/lib/devise_otp_authenticatable/hooks/sessions.rb +8 -7
- data/lib/devise_otp_authenticatable/hooks.rb +1 -1
- data/lib/devise_otp_authenticatable/models/otp_authenticatable.rb +28 -28
- data/lib/devise_otp_authenticatable/routes.rb +9 -10
- data/lib/generators/active_record/devise_otp_generator.rb +1 -1
- data/lib/generators/active_record/templates/migration.rb +1 -2
- data/lib/generators/devise_otp/devise_otp_generator.rb +0 -0
- data/lib/generators/devise_otp/install_generator.rb +30 -5
- data/lib/generators/devise_otp/views_generator.rb +2 -3
- data/test/dummy/README.rdoc +0 -0
- data/test/dummy/Rakefile +0 -0
- data/test/dummy/app/assets/config/manifest.js +2 -0
- data/test/dummy/app/assets/javascripts/application.js +1 -0
- data/test/dummy/app/assets/stylesheets/application.css +0 -0
- data/test/dummy/app/controllers/application_controller.rb +1 -1
- data/test/dummy/app/controllers/posts_controller.rb +2 -0
- data/test/dummy/app/helpers/application_helper.rb +0 -0
- data/test/dummy/app/helpers/posts_helper.rb +0 -0
- data/test/dummy/app/mailers/.gitkeep +0 -0
- data/test/dummy/app/models/post.rb +0 -0
- data/test/dummy/app/models/user.rb +1 -1
- data/test/dummy/app/views/layouts/application.html.erb +0 -0
- data/test/dummy/app/views/posts/_form.html.erb +0 -0
- data/test/dummy/app/views/posts/edit.html.erb +0 -0
- data/test/dummy/app/views/posts/index.html.erb +0 -0
- data/test/dummy/app/views/posts/new.html.erb +0 -0
- data/test/dummy/app/views/posts/show.html.erb +0 -0
- data/test/dummy/config/application.rb +2 -1
- data/test/dummy/config/boot.rb +0 -0
- data/test/dummy/config/database.yml +1 -1
- data/test/dummy/config/environment.rb +0 -0
- data/test/dummy/config/environments/development.rb +0 -7
- data/test/dummy/config/environments/production.rb +0 -4
- data/test/dummy/config/environments/test.rb +0 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -0
- data/test/dummy/config/initializers/devise.rb +0 -0
- data/test/dummy/config/initializers/inflections.rb +0 -0
- data/test/dummy/config/initializers/mime_types.rb +0 -0
- data/test/dummy/config/initializers/secret_token.rb +0 -0
- data/test/dummy/config/initializers/session_store.rb +0 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -0
- data/test/dummy/config/locales/en.yml +0 -0
- data/test/dummy/config/routes.rb +0 -0
- data/test/dummy/config.ru +0 -0
- data/test/dummy/db/migrate/20130125101430_create_users.rb +1 -1
- data/test/dummy/db/migrate/20130131092406_add_devise_to_users.rb +1 -1
- data/test/dummy/db/migrate/20130131142320_create_posts.rb +1 -1
- data/test/dummy/db/migrate/20130131160351_devise_otp_add_to_users.rb +2 -2
- data/test/dummy/db/test.sqlite3-journal +0 -0
- data/test/dummy/lib/assets/.gitkeep +0 -0
- data/test/dummy/public/404.html +0 -0
- data/test/dummy/public/422.html +0 -0
- data/test/dummy/public/500.html +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/integration/persistence_test.rb +81 -0
- data/test/integration/refresh_test.rb +2 -18
- data/test/integration/sign_in_test.rb +14 -4
- data/test/integration/token_test.rb +31 -0
- data/test/integration_tests_helper.rb +19 -2
- data/test/model_tests_helper.rb +0 -0
- data/test/models/otp_authenticatable_test.rb +14 -9
- data/test/orm/active_record.rb +3 -1
- data/test/test_helper.rb +71 -2
- metadata +135 -24
- data/.travis.yml +0 -11
- data/app/controllers/devise_otp/credentials_controller.rb +0 -106
- data/app/controllers/devise_otp/tokens_controller.rb +0 -105
- data/app/views/devise_otp/credentials/refresh.html.erb +0 -20
- data/app/views/devise_otp/credentials/show.html.erb +0 -23
- data/app/views/devise_otp/tokens/_token_secret.html.erb +0 -17
- data/app/views/devise_otp/tokens/recovery.html.erb +0 -21
- data/app/views/devise_otp/tokens/show.html.erb +0 -31
- data/lib/devise_otp_authenticatable/mapping.rb +0 -19
|
@@ -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,17 +16,19 @@ 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
|
|
|
26
|
+
def trusted_devices_enabled?
|
|
27
|
+
resource.class.otp_trust_persistence && (resource.class.otp_trust_persistence > 0)
|
|
28
|
+
end
|
|
29
29
|
|
|
30
30
|
def recovery_enabled?
|
|
31
|
-
resource_class.
|
|
31
|
+
resource_class.otp_recovery_tokens && (resource_class.otp_recovery_tokens > 0)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
#
|
|
@@ -40,9 +40,7 @@ module DeviseOtpAuthenticatable
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
|
|
44
43
|
# fixme do cookies and persistence need to be scoped? probably
|
|
45
|
-
|
|
46
44
|
#
|
|
47
45
|
# check if the resource needs a credentials refresh. IE, they need to be asked a password again to access
|
|
48
46
|
# this resource.
|
|
@@ -62,15 +60,14 @@ module DeviseOtpAuthenticatable
|
|
|
62
60
|
session[otp_scoped_refresh_property] = (Time.now + resource.class.otp_credentials_refresh)
|
|
63
61
|
end
|
|
64
62
|
|
|
65
|
-
|
|
66
63
|
#
|
|
67
64
|
# is the current browser trusted?
|
|
68
65
|
#
|
|
69
|
-
def
|
|
66
|
+
def is_otp_trusted_browser_for?(resource)
|
|
67
|
+
return false unless resource.class.otp_trust_persistence
|
|
70
68
|
if cookies[otp_scoped_persistence_cookie].present?
|
|
71
69
|
cookies.signed[otp_scoped_persistence_cookie] ==
|
|
72
|
-
[resource.
|
|
73
|
-
end
|
|
70
|
+
[resource.to_key, resource.authenticatable_salt, resource.otp_persistence_seed]
|
|
74
71
|
else
|
|
75
72
|
false
|
|
76
73
|
end
|
|
@@ -80,10 +77,11 @@ module DeviseOtpAuthenticatable
|
|
|
80
77
|
# make the current browser trusted
|
|
81
78
|
#
|
|
82
79
|
def otp_set_trusted_device_for(resource)
|
|
80
|
+
return unless resource.class.otp_trust_persistence
|
|
83
81
|
cookies.signed[otp_scoped_persistence_cookie] = {
|
|
84
82
|
:httponly => true,
|
|
85
|
-
:expires =>
|
|
86
|
-
:value => [resource.
|
|
83
|
+
:expires => Time.now + resource.class.otp_trust_persistence,
|
|
84
|
+
:value => [resource.to_key, resource.authenticatable_salt, resource.otp_persistence_seed]
|
|
87
85
|
}
|
|
88
86
|
end
|
|
89
87
|
|
|
@@ -115,7 +113,6 @@ module DeviseOtpAuthenticatable
|
|
|
115
113
|
cookies.delete(otp_scoped_persistence_cookie)
|
|
116
114
|
end
|
|
117
115
|
|
|
118
|
-
|
|
119
116
|
#
|
|
120
117
|
# clears the persistence list for this kind of resource
|
|
121
118
|
#
|
|
@@ -128,11 +125,27 @@ module DeviseOtpAuthenticatable
|
|
|
128
125
|
# returns the URL for the QR Code to initialize the Authenticator device
|
|
129
126
|
#
|
|
130
127
|
def otp_authenticator_token_image(resource)
|
|
131
|
-
|
|
128
|
+
otp_authenticator_token_image_js(resource.otp_provisioning_uri)
|
|
132
129
|
end
|
|
133
130
|
|
|
134
131
|
private
|
|
135
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
|
+
|
|
136
149
|
def otp_authenticator_token_image_google(otp_url)
|
|
137
150
|
otp_url = Rack::Utils.escape(otp_url)
|
|
138
151
|
url = "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=#{otp_url}"
|
|
@@ -141,4 +154,4 @@ module DeviseOtpAuthenticatable
|
|
|
141
154
|
|
|
142
155
|
end
|
|
143
156
|
end
|
|
144
|
-
end
|
|
157
|
+
end
|
|
@@ -1,35 +1,32 @@
|
|
|
1
1
|
module DeviseOtpAuthenticatable
|
|
2
2
|
module Controllers
|
|
3
|
-
|
|
4
3
|
module UrlHelpers
|
|
5
4
|
|
|
6
|
-
|
|
7
5
|
def recovery_otp_token_for(resource_or_scope, opts = {})
|
|
8
|
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
|
6
|
+
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
|
|
9
7
|
send("recovery_#{scope}_otp_token_path", opts)
|
|
10
8
|
end
|
|
11
9
|
|
|
12
|
-
|
|
13
10
|
def refresh_otp_credential_path_for(resource_or_scope, opts = {})
|
|
14
|
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
|
11
|
+
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
|
|
15
12
|
send("refresh_#{scope}_otp_credential_path", opts)
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
def persistence_otp_token_path_for(resource_or_scope, opts = {})
|
|
19
|
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
|
16
|
+
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
|
|
20
17
|
send("persistence_#{scope}_otp_token_path", opts)
|
|
21
18
|
end
|
|
22
19
|
|
|
23
20
|
def otp_token_path_for(resource_or_scope, opts = {})
|
|
24
|
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
|
21
|
+
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
|
|
25
22
|
send("#{scope}_otp_token_path", opts)
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
def otp_credential_path_for(resource_or_scope, opts = {})
|
|
29
|
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
|
26
|
+
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
|
|
30
27
|
send("#{scope}_otp_credential_path", opts)
|
|
31
28
|
end
|
|
32
29
|
|
|
33
30
|
end
|
|
34
31
|
end
|
|
35
|
-
end
|
|
32
|
+
end
|
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
module DeviseOtpAuthenticatable
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
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
4
|
# We use to_prepare instead of after_initialize here because Devise is a Rails engine;
|
|
14
5
|
config.to_prepare do
|
|
15
6
|
DeviseOtpAuthenticatable::Hooks.apply
|
|
16
7
|
end
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
initializer "devise-otp", group: :all do |app|
|
|
10
|
+
ActiveSupport.on_load(:devise_controller) do
|
|
11
|
+
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
|
|
12
|
+
include DeviseOtpAuthenticatable::Controllers::Helpers
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
ActiveSupport.on_load(:action_view) do
|
|
16
|
+
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
|
|
17
|
+
include DeviseOtpAuthenticatable::Controllers::Helpers
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# See: https://guides.rubyonrails.org/engines.html#separate-assets-and-precompiling
|
|
21
|
+
# check if Rails api mode
|
|
22
|
+
if app.config.respond_to?(:assets)
|
|
23
|
+
if defined?(Sprockets) && Sprockets::VERSION >= "4"
|
|
24
|
+
app.config.assets.precompile << "devise-otp.js"
|
|
25
|
+
else
|
|
26
|
+
# use a proc instead of a string
|
|
27
|
+
app.config.assets.precompile << proc { |path| path == "devise-otp.js" }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
21
30
|
end
|
|
22
31
|
end
|
|
23
|
-
end
|
|
32
|
+
end
|
|
@@ -4,36 +4,36 @@ module DeviseOtpAuthenticatable::Hooks
|
|
|
4
4
|
include DeviseOtpAuthenticatable::Controllers::UrlHelpers
|
|
5
5
|
|
|
6
6
|
included do
|
|
7
|
-
|
|
7
|
+
alias_method :create, :create_with_otp
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
#
|
|
11
11
|
# replaces Devise::SessionsController#create
|
|
12
12
|
#
|
|
13
13
|
def create_with_otp
|
|
14
|
-
|
|
15
14
|
resource = warden.authenticate!(auth_options)
|
|
16
15
|
|
|
16
|
+
devise_stored_location = stored_location_for(resource) # Grab the current stored location before it gets lost by warden.logout
|
|
17
|
+
store_location_for(resource, devise_stored_location) # Restore it since #stored_location_for removes it
|
|
18
|
+
|
|
17
19
|
otp_refresh_credentials_for(resource)
|
|
18
20
|
|
|
19
21
|
if otp_challenge_required_on?(resource)
|
|
20
22
|
challenge = resource.generate_otp_challenge!
|
|
21
23
|
warden.logout
|
|
24
|
+
store_location_for(resource, devise_stored_location) # restore the stored location
|
|
22
25
|
respond_with resource, :location => otp_credential_path_for(resource, {:challenge => challenge})
|
|
23
|
-
|
|
24
26
|
elsif otp_mandatory_on?(resource) # if mandatory, log in user but send him to the must activate otp
|
|
25
27
|
set_flash_message(:notice, :signed_in_but_otp) if is_navigational_format?
|
|
26
28
|
sign_in(resource_name, resource)
|
|
27
29
|
respond_with resource, :location => otp_token_path_for(resource)
|
|
28
30
|
else
|
|
29
|
-
|
|
30
31
|
set_flash_message(:notice, :signed_in) if is_navigational_format?
|
|
31
32
|
sign_in(resource_name, resource)
|
|
32
33
|
respond_with resource, :location => after_sign_in_path_for(resource)
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
|
|
37
37
|
private
|
|
38
38
|
|
|
39
39
|
#
|
|
@@ -41,7 +41,8 @@ module DeviseOtpAuthenticatable::Hooks
|
|
|
41
41
|
#
|
|
42
42
|
def otp_challenge_required_on?(resource)
|
|
43
43
|
return false unless resource.respond_to?(:otp_enabled) && resource.respond_to?(:otp_auth_secret)
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
resource.otp_enabled && !is_otp_trusted_browser_for?(resource)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
#
|
|
@@ -54,4 +55,4 @@ module DeviseOtpAuthenticatable::Hooks
|
|
|
54
55
|
resource.otp_mandatory && !resource.otp_enabled
|
|
55
56
|
end
|
|
56
57
|
end
|
|
57
|
-
end
|
|
58
|
+
end
|
|
@@ -11,8 +11,9 @@ module Devise::Models
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
module ClassMethods
|
|
14
|
-
::Devise::Models.config(self, :otp_authentication_timeout, :otp_drift_window,
|
|
15
|
-
:otp_mandatory, :otp_credentials_refresh, :
|
|
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
|
+
:otp_controller_path)
|
|
16
17
|
|
|
17
18
|
def find_valid_otp_challenge(challenge)
|
|
18
19
|
with_valid_otp_challenge(Time.now).where(:otp_session_challenge => challenge).first
|
|
@@ -20,7 +21,7 @@ module Devise::Models
|
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def time_based_otp
|
|
23
|
-
@time_based_otp ||= ROTP::TOTP.new(otp_auth_secret)
|
|
24
|
+
@time_based_otp ||= ROTP::TOTP.new(otp_auth_secret, issuer: "#{self.class.otp_issuer || Rails.application.class.module_parent_name}")
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def recovery_otp
|
|
@@ -32,7 +33,7 @@ module Devise::Models
|
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def otp_provisioning_identifier
|
|
35
|
-
|
|
36
|
+
email
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
|
|
@@ -41,9 +42,9 @@ module Devise::Models
|
|
|
41
42
|
@recovery_otp = nil
|
|
42
43
|
generate_otp_auth_secret
|
|
43
44
|
reset_otp_persistence
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
update!(:otp_enabled => false,
|
|
46
|
+
:otp_session_challenge => nil, :otp_challenge_expires => nil,
|
|
47
|
+
:otp_recovery_counter => 0)
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def reset_otp_credentials!
|
|
@@ -51,7 +52,6 @@ module Devise::Models
|
|
|
51
52
|
save!
|
|
52
53
|
end
|
|
53
54
|
|
|
54
|
-
|
|
55
55
|
def reset_otp_persistence
|
|
56
56
|
generate_otp_persistence_seed
|
|
57
57
|
end
|
|
@@ -61,9 +61,21 @@ module Devise::Models
|
|
|
61
61
|
save!
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
def enable_otp!
|
|
65
|
+
if otp_persistence_seed.nil?
|
|
66
|
+
reset_otp_credentials!
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
update!(:otp_enabled => true, :otp_enabled_on => Time.now)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def disable_otp!
|
|
73
|
+
update!(:otp_enabled => false, :otp_enabled_on => nil)
|
|
74
|
+
end
|
|
75
|
+
|
|
64
76
|
def generate_otp_challenge!(expires = nil)
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
update!(:otp_session_challenge => SecureRandom.hex,
|
|
78
|
+
:otp_challenge_expires => DateTime.now + (expires || self.class.otp_authentication_timeout))
|
|
67
79
|
otp_session_challenge
|
|
68
80
|
end
|
|
69
81
|
|
|
@@ -82,16 +94,12 @@ module Devise::Models
|
|
|
82
94
|
alias_method :valid_otp_token?, :validate_otp_token
|
|
83
95
|
|
|
84
96
|
def validate_otp_time_token(token)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
true
|
|
88
|
-
else
|
|
89
|
-
false
|
|
90
|
-
end
|
|
97
|
+
return false if token.blank?
|
|
98
|
+
validate_otp_token_with_drift(token)
|
|
91
99
|
end
|
|
92
100
|
alias_method :valid_otp_time_token?, :validate_otp_time_token
|
|
93
101
|
|
|
94
|
-
def next_otp_recovery_tokens(number =
|
|
102
|
+
def next_otp_recovery_tokens(number = self.class.otp_recovery_tokens)
|
|
95
103
|
(otp_recovery_counter..otp_recovery_counter + number).inject({}) do |h, index|
|
|
96
104
|
h[index] = recovery_otp.at(index)
|
|
97
105
|
h
|
|
@@ -108,21 +116,13 @@ module Devise::Models
|
|
|
108
116
|
|
|
109
117
|
|
|
110
118
|
|
|
111
|
-
|
|
112
|
-
|
|
113
119
|
private
|
|
114
120
|
|
|
115
|
-
#
|
|
116
|
-
# refactor me, I suck
|
|
117
|
-
#
|
|
118
121
|
def validate_otp_token_with_drift(token)
|
|
119
|
-
# valid_vals << ROTP::TOTP.new(otp_auth_secret).at(Time.now)
|
|
120
122
|
|
|
121
123
|
# should be centered around saved drift
|
|
122
|
-
(-self.class.otp_drift_window..self.class.otp_drift_window).
|
|
123
|
-
|
|
124
|
-
end
|
|
125
|
-
false
|
|
124
|
+
(-self.class.otp_drift_window..self.class.otp_drift_window).any? {|drift|
|
|
125
|
+
(time_based_otp.verify(token, at: Time.now.ago(30 * drift))) }
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
def generate_otp_persistence_seed
|
|
@@ -135,4 +135,4 @@ module Devise::Models
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
end
|
|
138
|
-
end
|
|
138
|
+
end
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
module ActionDispatch::Routing
|
|
2
2
|
class Mapper
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
protected
|
|
6
|
-
#########
|
|
7
5
|
|
|
8
6
|
def devise_otp(mapping, controllers)
|
|
9
|
-
|
|
10
7
|
namespace :otp, :module => :devise_otp do
|
|
11
8
|
resource :token, :only => [:show, :update, :destroy],
|
|
12
|
-
:path => mapping.path_names[:token], :controller => controllers[:
|
|
9
|
+
:path => mapping.path_names[:token], :controller => controllers[:tokens] do
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
if Devise.otp_trust_persistence
|
|
12
|
+
get :persistence, :action => 'get_persistence'
|
|
13
|
+
post :persistence, :action => 'clear_persistence'
|
|
14
|
+
delete :persistence, :action => 'delete_persistence'
|
|
15
|
+
end
|
|
17
16
|
|
|
18
17
|
get :recovery
|
|
19
18
|
end
|
|
20
19
|
|
|
21
20
|
resource :credential, :only => [:show, :update],
|
|
22
|
-
:path => mapping.path_names[:credentials], :controller => controllers[:
|
|
21
|
+
:path => mapping.path_names[:credentials], :controller => controllers[:credentials] do
|
|
23
22
|
|
|
24
|
-
get
|
|
23
|
+
get :refresh, :action => 'get_refresh'
|
|
25
24
|
put :refresh, :action => 'set_refresh'
|
|
26
25
|
end
|
|
27
26
|
end
|
|
28
27
|
end
|
|
29
28
|
end
|
|
30
|
-
end
|
|
29
|
+
end
|
|
@@ -6,7 +6,7 @@ module ActiveRecord
|
|
|
6
6
|
source_root File.expand_path("../templates", __FILE__)
|
|
7
7
|
|
|
8
8
|
def copy_devise_migration
|
|
9
|
-
migration_template "migration.rb", "db/migrate/devise_otp_add_to_#{table_name}"
|
|
9
|
+
migration_template "migration.rb", "db/migrate/devise_otp_add_to_#{table_name}.rb"
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
end
|
|
@@ -6,7 +6,6 @@ class DeviseOtpAddTo<%= table_name.camelize %> < ActiveRecord::Migration
|
|
|
6
6
|
t.boolean :otp_enabled, :default => false, :null => false
|
|
7
7
|
t.boolean :otp_mandatory, :default => false, :null => false
|
|
8
8
|
t.datetime :otp_enabled_on
|
|
9
|
-
t.integer :otp_time_drift, :default => 0, :null => false
|
|
10
9
|
t.integer :otp_failed_attempts, :default => 0, :null => false
|
|
11
10
|
t.integer :otp_recovery_counter, :default => 0, :null => false
|
|
12
11
|
t.string :otp_persistence_seed
|
|
@@ -21,7 +20,7 @@ class DeviseOtpAddTo<%= table_name.camelize %> < ActiveRecord::Migration
|
|
|
21
20
|
def self.down
|
|
22
21
|
change_table :<%= table_name %> do |t|
|
|
23
22
|
t.remove :otp_auth_secret, :otp_recovery_secret, :otp_enabled, :otp_mandatory, :otp_enabled_on, :otp_session_challenge,
|
|
24
|
-
:otp_challenge_expires, :
|
|
23
|
+
:otp_challenge_expires, :otp_failed_attempts, :otp_recovery_counter, :otp_persistence_seed
|
|
25
24
|
|
|
26
25
|
end
|
|
27
26
|
end
|
|
File without changes
|
|
@@ -13,11 +13,36 @@ content = <<-CONTENT
|
|
|
13
13
|
# ==> Devise OTP Extension
|
|
14
14
|
# Configure OTP extension for devise
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
#
|
|
16
|
+
# OTP is mandatory, users are going to be asked to
|
|
17
|
+
# enroll OTP the next time they sign in, before they can successfully complete the session establishment.
|
|
18
|
+
# This is the global value, can also be set on each user.
|
|
19
|
+
#config.otp_mandatory = false
|
|
20
|
+
|
|
21
|
+
# Drift: a window which provides allowance for drift between a user's token device clock
|
|
22
|
+
# (and therefore their OTP tokens) and the authentication server's clock.
|
|
23
|
+
# Expressed in minutes centered at the current time. (Note: it's a number, *NOT* 3.minutes )
|
|
24
|
+
#config.otp_drift_window = 3
|
|
25
|
+
|
|
26
|
+
# Users that have logged in longer than this time ago, are going to be asked their password
|
|
27
|
+
# (and an OTP challenge, if enabled) before they can see or change their otp informations.
|
|
28
|
+
#config.otp_credentials_refresh = 15.minutes
|
|
29
|
+
|
|
30
|
+
# Users are given a list of one-time recovery tokens, for emergency access
|
|
31
|
+
# set to false to disable giving recovery tokens.
|
|
32
|
+
#config.otp_recovery_tokens = 10
|
|
33
|
+
|
|
34
|
+
# The user is allowed to set his browser as "trusted", no more OTP challenges will be
|
|
35
|
+
# asked for that browser, for a limited time.
|
|
36
|
+
# set to false to disable setting the browser as trusted
|
|
37
|
+
#config.otp_trust_persistence = 1.month
|
|
38
|
+
|
|
39
|
+
# The name of the token issuer, to be added to the provisioning
|
|
40
|
+
# url. Display will vary based on token application. (defaults to the Rails application class)
|
|
41
|
+
#config.otp_issuer = 'my_application'
|
|
42
|
+
|
|
43
|
+
# Custom view path for Devise OTP controllers
|
|
44
|
+
#config.otp_controller_path = 'devise'
|
|
18
45
|
|
|
19
|
-
# Change time drift settings for valid token values. To change the default, uncomment and change the below:
|
|
20
|
-
#config.otp_authentication_time_drift = 3
|
|
21
46
|
CONTENT
|
|
22
47
|
|
|
23
48
|
inject_into_file "config/initializers/devise.rb", content, :before => /end[ |\n|]+\Z/
|
|
@@ -28,4 +53,4 @@ CONTENT
|
|
|
28
53
|
end
|
|
29
54
|
end
|
|
30
55
|
end
|
|
31
|
-
end
|
|
56
|
+
end
|
|
@@ -9,10 +9,9 @@ module DeviseOtp
|
|
|
9
9
|
:desc => "The scope to copy views to"
|
|
10
10
|
|
|
11
11
|
include ::Devise::Generators::ViewPathTemplates
|
|
12
|
-
source_root File.expand_path("../../../../app/views
|
|
12
|
+
source_root File.expand_path("../../../../app/views", __FILE__)
|
|
13
13
|
def copy_views
|
|
14
|
-
view_directory :
|
|
15
|
-
view_directory :tokens, 'app/views/devise_otp/tokens'
|
|
14
|
+
view_directory :devise, 'app/views/devise'
|
|
16
15
|
end
|
|
17
16
|
end
|
|
18
17
|
end
|
data/test/dummy/README.rdoc
CHANGED
|
File without changes
|
data/test/dummy/Rakefile
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -12,7 +12,7 @@ class User < PARENT_MODEL_CLASS
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
devise :otp_authenticatable, :database_authenticatable, :registerable,
|
|
15
|
-
:
|
|
15
|
+
:trackable, :validatable
|
|
16
16
|
|
|
17
17
|
# Setup accessible (or protected) attributes for your model
|
|
18
18
|
#attr_accessible :otp_enabled, :otp_mandatory, :as => :otp_privileged
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -53,6 +53,8 @@ module Dummy
|
|
|
53
53
|
# Enable escaping HTML in JSON.
|
|
54
54
|
config.active_support.escape_html_entities_in_json = true
|
|
55
55
|
|
|
56
|
+
config.active_record.legacy_connection_handling = false
|
|
57
|
+
|
|
56
58
|
# Use SQL instead of Active Record's schema dumper when creating the database.
|
|
57
59
|
# This is necessary if your schema can't be completely dumped by the schema dumper,
|
|
58
60
|
# like if you have constraints or database-specific column types
|
|
@@ -65,4 +67,3 @@ module Dummy
|
|
|
65
67
|
config.assets.version = '1.0'
|
|
66
68
|
end
|
|
67
69
|
end
|
|
68
|
-
|
data/test/dummy/config/boot.rb
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -22,13 +22,6 @@ Dummy::Application.configure do
|
|
|
22
22
|
# Only use best-standards-support built into browsers
|
|
23
23
|
config.action_dispatch.best_standards_support = :builtin
|
|
24
24
|
|
|
25
|
-
# Raise exception on mass assignment protection for Active Record models
|
|
26
|
-
config.active_record.mass_assignment_sanitizer = :strict
|
|
27
|
-
|
|
28
|
-
# Log the query plan for queries taking more than this (works
|
|
29
|
-
# with SQLite, MySQL, and PostgreSQL)
|
|
30
|
-
config.active_record.auto_explain_threshold_in_seconds = 0.5
|
|
31
|
-
|
|
32
25
|
# Do not compress assets
|
|
33
26
|
config.assets.compress = false
|
|
34
27
|
|
|
@@ -66,8 +66,4 @@ Dummy::Application.configure do
|
|
|
66
66
|
|
|
67
67
|
# Send deprecation notices to registered listeners
|
|
68
68
|
config.active_support.deprecation = :notify
|
|
69
|
-
|
|
70
|
-
# Log the query plan for queries taking more than this (works
|
|
71
|
-
# with SQLite, MySQL, and PostgreSQL)
|
|
72
|
-
# config.active_record.auto_explain_threshold_in_seconds = 0.5
|
|
73
69
|
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|