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