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