devise-multi-factor 3.1.5
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/.codeclimate.yml +21 -0
- data/.github/workflows/gem-push.yml +42 -0
- data/.gitignore +23 -0
- data/.rubocop.yml +295 -0
- data/.travis.yml +28 -0
- data/CHANGELOG.md +119 -0
- data/Gemfile +32 -0
- data/LICENSE +19 -0
- data/README.md +322 -0
- data/Rakefile +12 -0
- data/app/controllers/devise/totp_controller.rb +79 -0
- data/app/controllers/devise/two_factor_authentication_controller.rb +84 -0
- data/app/views/devise/two_factor_authentication/max_login_attempts_reached.html.erb +3 -0
- data/app/views/devise/two_factor_authentication/new.html.erb +14 -0
- data/app/views/devise/two_factor_authentication/show.html.erb +19 -0
- data/config/locales/de.yml +8 -0
- data/config/locales/en.yml +8 -0
- data/config/locales/es.yml +8 -0
- data/config/locales/fr.yml +8 -0
- data/config/locales/ru.yml +8 -0
- data/devise-multi-factor.gemspec +40 -0
- data/lib/devise-multi-factor.rb +1 -0
- data/lib/devise_multi_factor.rb +56 -0
- data/lib/devise_multi_factor/controllers/helpers.rb +57 -0
- data/lib/devise_multi_factor/hooks/two_factor_authenticatable.rb +17 -0
- data/lib/devise_multi_factor/models/totp_enrollable.rb +7 -0
- data/lib/devise_multi_factor/models/two_factor_authenticatable.rb +142 -0
- data/lib/devise_multi_factor/orm/active_record.rb +14 -0
- data/lib/devise_multi_factor/rails.rb +7 -0
- data/lib/devise_multi_factor/routes.rb +15 -0
- data/lib/devise_multi_factor/schema.rb +23 -0
- data/lib/devise_multi_factor/version.rb +3 -0
- data/lib/generators/active_record/devise_multi_factor_generator.rb +13 -0
- data/lib/generators/active_record/templates/migration.rb +11 -0
- data/lib/generators/devise_multi_factor/devise_multi_factor_generator.rb +17 -0
- data/spec/controllers/two_factor_authentication_controller_spec.rb +41 -0
- data/spec/features/two_factor_authenticatable_spec.rb +237 -0
- data/spec/generators/active_record/devise_multi_factor_generator_spec.rb +34 -0
- data/spec/lib/devise_multi_factor/models/two_factor_authenticatable_spec.rb +282 -0
- data/spec/rails_app/.gitignore +3 -0
- data/spec/rails_app/README.md +3 -0
- data/spec/rails_app/Rakefile +7 -0
- data/spec/rails_app/app/assets/config/manifest.js +2 -0
- data/spec/rails_app/app/assets/javascripts/application.js +1 -0
- data/spec/rails_app/app/assets/stylesheets/application.css +4 -0
- data/spec/rails_app/app/controllers/application_controller.rb +3 -0
- data/spec/rails_app/app/controllers/home_controller.rb +10 -0
- data/spec/rails_app/app/helpers/application_helper.rb +8 -0
- data/spec/rails_app/app/mailers/.gitkeep +0 -0
- data/spec/rails_app/app/models/.gitkeep +0 -0
- data/spec/rails_app/app/models/admin.rb +6 -0
- data/spec/rails_app/app/models/encrypted_user.rb +7 -0
- data/spec/rails_app/app/models/guest_user.rb +7 -0
- data/spec/rails_app/app/models/test_user.rb +38 -0
- data/spec/rails_app/app/models/user.rb +18 -0
- data/spec/rails_app/app/views/home/dashboard.html.erb +11 -0
- data/spec/rails_app/app/views/home/index.html.erb +3 -0
- data/spec/rails_app/app/views/layouts/application.html.erb +20 -0
- data/spec/rails_app/config.ru +4 -0
- data/spec/rails_app/config/application.rb +61 -0
- data/spec/rails_app/config/boot.rb +10 -0
- data/spec/rails_app/config/database.yml +19 -0
- data/spec/rails_app/config/environment.rb +5 -0
- data/spec/rails_app/config/environments/development.rb +28 -0
- data/spec/rails_app/config/environments/production.rb +68 -0
- data/spec/rails_app/config/environments/test.rb +41 -0
- data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails_app/config/initializers/cookies_serializer.rb +3 -0
- data/spec/rails_app/config/initializers/devise.rb +258 -0
- data/spec/rails_app/config/initializers/inflections.rb +15 -0
- data/spec/rails_app/config/initializers/mime_types.rb +5 -0
- data/spec/rails_app/config/initializers/secret_token.rb +7 -0
- data/spec/rails_app/config/initializers/session_store.rb +8 -0
- data/spec/rails_app/config/initializers/wrap_parameters.rb +14 -0
- data/spec/rails_app/config/locales/devise.en.yml +59 -0
- data/spec/rails_app/config/locales/en.yml +5 -0
- data/spec/rails_app/config/routes.rb +65 -0
- data/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb +42 -0
- data/spec/rails_app/db/migrate/20140407172619_two_factor_authentication_add_to_users.rb +17 -0
- data/spec/rails_app/db/migrate/20140407215513_add_nickanme_to_users.rb +7 -0
- data/spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb +7 -0
- data/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb +19 -0
- data/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb +5 -0
- data/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb +42 -0
- data/spec/rails_app/db/schema.rb +55 -0
- data/spec/rails_app/lib/assets/.gitkeep +0 -0
- data/spec/rails_app/lib/sms_provider.rb +17 -0
- data/spec/rails_app/public/404.html +26 -0
- data/spec/rails_app/public/422.html +26 -0
- data/spec/rails_app/public/500.html +25 -0
- data/spec/rails_app/public/favicon.ico +0 -0
- data/spec/rails_app/script/rails +6 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/authenticated_model_helper.rb +29 -0
- data/spec/support/capybara.rb +3 -0
- data/spec/support/controller_helper.rb +16 -0
- data/spec/support/features_spec_helper.rb +42 -0
- data/spec/support/sms_provider.rb +5 -0
- data/spec/support/totp_helper.rb +11 -0
- metadata +315 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require 'devise/version'
|
|
2
|
+
|
|
3
|
+
class Devise::TwoFactorAuthenticationController < DeviseController
|
|
4
|
+
prepend_before_action :authenticate_scope!
|
|
5
|
+
before_action :prepare_and_validate, :handle_two_factor_authentication
|
|
6
|
+
|
|
7
|
+
def show
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def update
|
|
11
|
+
render :show and return if params[:code].nil?
|
|
12
|
+
|
|
13
|
+
if resource.authenticate_otp(params[:code])
|
|
14
|
+
after_two_factor_success_for(resource)
|
|
15
|
+
else
|
|
16
|
+
after_two_factor_fail_for(resource)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def resend_code
|
|
21
|
+
resource.send_new_otp
|
|
22
|
+
redirect_to send("#{resource_name}_two_factor_authentication_path"), notice: I18n.t('devise.two_factor_authentication.code_has_been_sent')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def after_two_factor_success_for(resource)
|
|
28
|
+
set_remember_two_factor_cookie(resource)
|
|
29
|
+
|
|
30
|
+
warden.session(resource_name)[DeviseMultiFactor::NEED_AUTHENTICATION] = false
|
|
31
|
+
# For compatability with devise versions below v4.2.0
|
|
32
|
+
# https://github.com/plataformatec/devise/commit/2044fffa25d781fcbaf090e7728b48b65c854ccb
|
|
33
|
+
if respond_to?(:bypass_sign_in)
|
|
34
|
+
bypass_sign_in(resource, scope: resource_name)
|
|
35
|
+
else
|
|
36
|
+
sign_in(resource_name, resource, bypass: true)
|
|
37
|
+
end
|
|
38
|
+
set_flash_message :notice, :success
|
|
39
|
+
resource.update_attribute(:second_factor_attempts_count, 0)
|
|
40
|
+
|
|
41
|
+
redirect_to after_two_factor_success_path_for(resource)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def set_remember_two_factor_cookie(resource)
|
|
45
|
+
expires_seconds = resource.class.remember_otp_session_for_seconds
|
|
46
|
+
|
|
47
|
+
if expires_seconds && expires_seconds > 0
|
|
48
|
+
cookies.signed[DeviseMultiFactor::REMEMBER_TFA_COOKIE_NAME] = {
|
|
49
|
+
value: "#{resource.class}-#{resource.public_send(Devise.second_factor_resource_id)}",
|
|
50
|
+
expires: expires_seconds.seconds.from_now
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def after_two_factor_success_path_for(resource)
|
|
56
|
+
stored_location_for(resource_name) || :root
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def after_two_factor_fail_for(resource)
|
|
60
|
+
resource.second_factor_attempts_count += 1
|
|
61
|
+
resource.save
|
|
62
|
+
set_flash_message :alert, :attempt_failed, now: true
|
|
63
|
+
|
|
64
|
+
if resource.max_login_attempts?
|
|
65
|
+
sign_out(resource)
|
|
66
|
+
render :max_login_attempts_reached
|
|
67
|
+
else
|
|
68
|
+
render :show
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def authenticate_scope!
|
|
73
|
+
self.resource = send("current_#{resource_name}")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def prepare_and_validate
|
|
77
|
+
redirect_to :root and return if resource.nil?
|
|
78
|
+
@limit = resource.max_login_attempts
|
|
79
|
+
if resource.max_login_attempts?
|
|
80
|
+
sign_out(resource)
|
|
81
|
+
render :max_login_attempts_reached and return
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Two Factor Authentication Setup
|
|
2
|
+
|
|
3
|
+
<p><%= flash[:error] %></p>
|
|
4
|
+
|
|
5
|
+
<%= image_tag(@qr_code) if @qr_code.present? %>
|
|
6
|
+
|
|
7
|
+
<p>Authenticator Secret: <%= @otp_secret %>
|
|
8
|
+
|
|
9
|
+
<%= form_for resource, url: send("#{resource_name}_two_factor_authentication_path"), method: :post do |f| %>
|
|
10
|
+
<label>Authenticator Code:</label>
|
|
11
|
+
<%= hidden_field_tag :otp_secret_signature, @otp_secret_signature %>
|
|
12
|
+
<%= text_field_tag :otp_attempt, '' %>
|
|
13
|
+
<%= f.submit %>
|
|
14
|
+
<% end %>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<% if resource.direct_otp %>
|
|
2
|
+
<h2>Enter the code that was sent to you</h2>
|
|
3
|
+
<% else %>
|
|
4
|
+
<h2>Enter the code from your authenticator app</h2>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<p><%= flash[:notice] %></p>
|
|
8
|
+
|
|
9
|
+
<%= form_tag([resource_name, :two_factor_authentication], :method => :put) do %>
|
|
10
|
+
<%= text_field_tag :code, '', autofocus: true %>
|
|
11
|
+
<%= submit_tag "Submit" %>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<% if resource.direct_otp %>
|
|
15
|
+
<%= link_to "Resend Code", send("resend_code_#{resource_name}_two_factor_authentication_path"), action: :get %>
|
|
16
|
+
<% else %>
|
|
17
|
+
<%= link_to "Send me a code instead", send("resend_code_#{resource_name}_two_factor_authentication_path"), action: :get %>
|
|
18
|
+
<% end %>
|
|
19
|
+
<%= link_to "Sign out", send("destroy_#{resource_name}_session_path"), :method => :delete %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
de:
|
|
2
|
+
devise:
|
|
3
|
+
two_factor_authentication:
|
|
4
|
+
success: "Ihre Zwei-Faktor-Authentifizierung war erfolgreich."
|
|
5
|
+
attempt_failed: "Authentifizierungsversuch fehlgeschlagen."
|
|
6
|
+
max_login_attempts_reached: "Ihr Zugang wurde ganz verweigert, da Sie Ihr Versuchslimit erreicht haben."
|
|
7
|
+
contact_administrator: "Kontaktieren Sie bitte einen Ihrer Administratoren."
|
|
8
|
+
code_has_been_sent: "Ihr Einmal-Passwort wurde verschickt."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
en:
|
|
2
|
+
devise:
|
|
3
|
+
two_factor_authentication:
|
|
4
|
+
success: "Two factor authentication successful."
|
|
5
|
+
attempt_failed: "Attempt failed."
|
|
6
|
+
max_login_attempts_reached: "Access completely denied as you have reached your attempts limit"
|
|
7
|
+
contact_administrator: "Please contact your system administrator."
|
|
8
|
+
code_has_been_sent: "Your authentication code has been sent."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
es:
|
|
2
|
+
devise:
|
|
3
|
+
two_factor_authentication:
|
|
4
|
+
success: "Autenticación multi-factor realizada exitosamente."
|
|
5
|
+
attempt_failed: "La autenticación ha fallado."
|
|
6
|
+
max_login_attempts_reached: "Has llegado al límite de intentos fallidos, acceso denegado."
|
|
7
|
+
contact_administrator: "Contacte a su administrador de sistema."
|
|
8
|
+
code_has_been_sent: "El código de autenticación ha sido enviado."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
fr:
|
|
2
|
+
devise:
|
|
3
|
+
two_factor_authentication:
|
|
4
|
+
success: "Validation en deux étapes effectuée avec succès."
|
|
5
|
+
attempt_failed: "La connexion a échoué."
|
|
6
|
+
max_login_attempts_reached: "Limite de tentatives atteinte, accès refusé."
|
|
7
|
+
contact_administrator: "Merci de contacter votre administrateur système."
|
|
8
|
+
code_has_been_sent: "Votre code de validation envoyé."
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ru:
|
|
2
|
+
devise:
|
|
3
|
+
two_factor_authentication:
|
|
4
|
+
success: "Двухфакторная авторизация успешно пройдена."
|
|
5
|
+
attempt_failed: "Неверный код."
|
|
6
|
+
max_login_attempts_reached: "Доступ заблокирован. Превышено число попыток авторизации"
|
|
7
|
+
contact_administrator: "Пожалуйста, свяжитесь с системным администратором."
|
|
8
|
+
code_has_been_sent: "Ваш персональный код был отправлен."
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "devise_multi_factor/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "devise-multi-factor"
|
|
7
|
+
s.version = DeviseMultiFactor::VERSION.dup
|
|
8
|
+
s.authors = ["Dmitrii Golub", "Alex Santos"]
|
|
9
|
+
s.email = ["hello@alexcsantos.com"]
|
|
10
|
+
s.homepage = "https://github.com/Colex/devise_multi_factor"
|
|
11
|
+
s.summary = %q{Two factor authentication plugin for devise}
|
|
12
|
+
s.description = <<-EOF
|
|
13
|
+
### Features ###
|
|
14
|
+
* control sms code pattern
|
|
15
|
+
* configure max login attempts
|
|
16
|
+
* per user level control if he really need two factor authentication
|
|
17
|
+
* your own sms logic
|
|
18
|
+
EOF
|
|
19
|
+
|
|
20
|
+
s.rubyforge_project = "devise_multi_factor"
|
|
21
|
+
|
|
22
|
+
s.files = `git ls-files`.split("\n")
|
|
23
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
24
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
25
|
+
s.require_paths = ["lib"]
|
|
26
|
+
|
|
27
|
+
s.add_runtime_dependency 'rails', '>= 3.1.1'
|
|
28
|
+
s.add_runtime_dependency 'devise'
|
|
29
|
+
s.add_runtime_dependency 'randexp'
|
|
30
|
+
s.add_runtime_dependency 'rotp', '>= 4.0.0'
|
|
31
|
+
s.add_runtime_dependency 'encryptor'
|
|
32
|
+
|
|
33
|
+
s.add_development_dependency 'bundler'
|
|
34
|
+
s.add_development_dependency 'rake'
|
|
35
|
+
s.add_development_dependency 'rspec-rails', '>= 3.0.1'
|
|
36
|
+
s.add_development_dependency 'capybara', '~> 2.5'
|
|
37
|
+
s.add_development_dependency 'pry'
|
|
38
|
+
s.add_development_dependency 'rubocop'
|
|
39
|
+
s.add_development_dependency 'timecop'
|
|
40
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'devise_multi_factor'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require 'devise_multi_factor/version'
|
|
2
|
+
require 'devise'
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
require "active_model"
|
|
5
|
+
require "active_support/core_ext/class/attribute_accessors"
|
|
6
|
+
require "cgi"
|
|
7
|
+
|
|
8
|
+
module Devise
|
|
9
|
+
mattr_accessor :max_login_attempts
|
|
10
|
+
@@max_login_attempts = 3
|
|
11
|
+
|
|
12
|
+
mattr_accessor :allowed_otp_drift_seconds
|
|
13
|
+
@@allowed_otp_drift_seconds = 30
|
|
14
|
+
|
|
15
|
+
mattr_accessor :otp_issuer
|
|
16
|
+
@@otp_issuer = nil
|
|
17
|
+
|
|
18
|
+
mattr_accessor :otp_length
|
|
19
|
+
@@otp_length = 6
|
|
20
|
+
|
|
21
|
+
mattr_accessor :direct_otp_length
|
|
22
|
+
@@direct_otp_length = 6
|
|
23
|
+
|
|
24
|
+
mattr_accessor :direct_otp_valid_for
|
|
25
|
+
@@direct_otp_valid_for = 5.minutes
|
|
26
|
+
|
|
27
|
+
mattr_accessor :remember_otp_session_for_seconds
|
|
28
|
+
@@remember_otp_session_for_seconds = 0
|
|
29
|
+
|
|
30
|
+
mattr_accessor :otp_secret_encryption_key
|
|
31
|
+
@@otp_secret_encryption_key = nil
|
|
32
|
+
|
|
33
|
+
mattr_accessor :second_factor_resource_id
|
|
34
|
+
@@second_factor_resource_id = 'id'
|
|
35
|
+
|
|
36
|
+
mattr_accessor :delete_cookie_on_logout
|
|
37
|
+
@@delete_cookie_on_logout = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module DeviseMultiFactor
|
|
41
|
+
NEED_AUTHENTICATION = 'need_two_factor_authentication'
|
|
42
|
+
REMEMBER_TFA_COOKIE_NAME = "remember_tfa"
|
|
43
|
+
|
|
44
|
+
autoload :Schema, 'devise_multi_factor/schema'
|
|
45
|
+
module Controllers
|
|
46
|
+
autoload :Helpers, 'devise_multi_factor/controllers/helpers'
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Devise.add_module :two_factor_authenticatable, :model => 'devise_multi_factor/models/two_factor_authenticatable', :controller => :two_factor_authentication, :route => :two_factor_authentication
|
|
51
|
+
Devise.add_module :totp_enrollable, model: 'devise_multi_factor/models/totp_enrollable', controller: :totp, route: :totp
|
|
52
|
+
|
|
53
|
+
require 'devise_multi_factor/orm/active_record' if defined?(ActiveRecord::Base)
|
|
54
|
+
require 'devise_multi_factor/routes'
|
|
55
|
+
require 'devise_multi_factor/models/two_factor_authenticatable'
|
|
56
|
+
require 'devise_multi_factor/rails'
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module DeviseMultiFactor
|
|
2
|
+
module Controllers
|
|
3
|
+
module Helpers
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
before_action :handle_two_factor_authentication
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def two_factor_authenticate!
|
|
13
|
+
Devise.mappings.keys.flatten.any? do |scope|
|
|
14
|
+
if signed_in?(scope) and warden.session(scope)[DeviseMultiFactor::NEED_AUTHENTICATION]
|
|
15
|
+
handle_failed_second_factor(scope)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def handle_two_factor_authentication
|
|
21
|
+
unless devise_controller?
|
|
22
|
+
two_factor_authenticate!
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def handle_failed_second_factor(scope)
|
|
27
|
+
if request.format.present?
|
|
28
|
+
if request.format.html?
|
|
29
|
+
session["#{scope}_return_to"] = request.original_fullpath if request.get?
|
|
30
|
+
redirect_to two_factor_authentication_path_for(scope)
|
|
31
|
+
elsif request.format.json?
|
|
32
|
+
session["#{scope}_return_to"] = root_path(format: :html)
|
|
33
|
+
render json: { redirect_to: two_factor_authentication_path_for(scope) }, status: :unauthorized
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
head :unauthorized
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def two_factor_authentication_path_for(resource_or_scope = nil)
|
|
41
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
|
42
|
+
change_path = "#{scope}_two_factor_authentication_path"
|
|
43
|
+
send(change_path)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module Devise
|
|
50
|
+
module Controllers
|
|
51
|
+
module Helpers
|
|
52
|
+
def is_fully_authenticated?
|
|
53
|
+
!session["warden.user.user.session"].try(:[], DeviseMultiFactor::NEED_AUTHENTICATION)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Warden::Manager.after_authentication do |resource, auth, options|
|
|
2
|
+
if auth.env["action_dispatch.cookies"]
|
|
3
|
+
expected_cookie_value = "#{resource.class}-#{resource.public_send(Devise.second_factor_resource_id)}"
|
|
4
|
+
actual_cookie_value = auth.env["action_dispatch.cookies"].signed[DeviseMultiFactor::REMEMBER_TFA_COOKIE_NAME]
|
|
5
|
+
bypass_by_cookie = actual_cookie_value == expected_cookie_value
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
if resource.respond_to?(:need_two_factor_authentication?) && !bypass_by_cookie
|
|
9
|
+
if auth.session(options[:scope])[DeviseMultiFactor::NEED_AUTHENTICATION] = resource.need_two_factor_authentication?(auth.request)
|
|
10
|
+
resource.send_new_otp if resource.send_new_otp_after_login?
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Warden::Manager.before_logout do |resource, auth, _options|
|
|
16
|
+
auth.cookies.delete DeviseMultiFactor::REMEMBER_TFA_COOKIE_NAME if Devise.delete_cookie_on_logout
|
|
17
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
require 'devise_multi_factor/hooks/two_factor_authenticatable'
|
|
2
|
+
require 'rotp'
|
|
3
|
+
|
|
4
|
+
module Devise
|
|
5
|
+
module Models
|
|
6
|
+
module TwoFactorAuthenticatable
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def has_one_time_password(options = {})
|
|
11
|
+
include InstanceMethodsOnActivation
|
|
12
|
+
|
|
13
|
+
encrypt_options = {
|
|
14
|
+
key: otp_secret_encryption_key,
|
|
15
|
+
encrypted_attribute: 'encrypted_otp_secret_key',
|
|
16
|
+
}.compact
|
|
17
|
+
encrypt_options = encrypt_options.merge(options[:encrypt]) if options[:encrypt].is_a?(Hash)
|
|
18
|
+
encrypts(:otp_secret_key, encrypt_options || {})
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def generate_totp_secret
|
|
22
|
+
# ROTP gem since version 5 to version 5.1
|
|
23
|
+
# at version 5.1 ROTP gem reinstates.
|
|
24
|
+
# Details: https://github.com/mdp/rotp/blob/master/CHANGELOG.md#510
|
|
25
|
+
ROTP::Base32.try(:random) || ROTP::Base32.random_base32
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
::Devise::Models.config(
|
|
29
|
+
self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_issuer, :otp_length,
|
|
30
|
+
:remember_otp_session_for_seconds, :otp_secret_encryption_key,
|
|
31
|
+
:direct_otp_length, :direct_otp_valid_for, :totp_timestamp, :delete_cookie_on_logout
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module InstanceMethodsOnActivation
|
|
36
|
+
def authenticate_otp(code, options = {})
|
|
37
|
+
return true if direct_otp && authenticate_direct_otp(code)
|
|
38
|
+
return true if totp_enabled? && authenticate_totp(code, options)
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def authenticate_direct_otp(code)
|
|
43
|
+
return false if direct_otp.nil? || direct_otp != code || direct_otp_expired?
|
|
44
|
+
|
|
45
|
+
clear_direct_otp
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def authenticate_totp(code, options = {})
|
|
50
|
+
totp_secret = options[:otp_secret_key] || otp_secret_key
|
|
51
|
+
digits = options[:otp_length] || self.class.otp_length
|
|
52
|
+
drift = options[:drift] || self.class.allowed_otp_drift_seconds
|
|
53
|
+
raise "authenticate_totp called with no otp_secret_key set" if totp_secret.nil?
|
|
54
|
+
|
|
55
|
+
totp = ROTP::TOTP.new(totp_secret, digits: digits)
|
|
56
|
+
new_timestamp = totp.verify(
|
|
57
|
+
without_spaces(code),
|
|
58
|
+
drift_ahead: drift, drift_behind: drift, after: totp_timestamp
|
|
59
|
+
)
|
|
60
|
+
return false unless new_timestamp
|
|
61
|
+
|
|
62
|
+
self.totp_timestamp = new_timestamp
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def provisioning_uri(account = nil, options = {})
|
|
67
|
+
totp_secret = options[:otp_secret_key] || otp_secret_key
|
|
68
|
+
options[:digits] ||= options[:otp_length] || self.class.otp_length
|
|
69
|
+
raise "provisioning_uri called with no otp_secret_key set" if totp_secret.nil?
|
|
70
|
+
account ||= email if respond_to?(:email)
|
|
71
|
+
ROTP::TOTP.new(totp_secret, options).provisioning_uri(account)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def enroll_totp!(otp_secret_key, code)
|
|
75
|
+
return false unless authenticate_totp(code, { otp_secret_key: otp_secret_key })
|
|
76
|
+
|
|
77
|
+
update_columns(totp_timestamp: totp_timestamp, otp_secret_key: otp_secret_key)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def need_two_factor_authentication?(request)
|
|
81
|
+
totp_enabled?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def send_new_otp(options = {})
|
|
85
|
+
create_direct_otp options
|
|
86
|
+
send_two_factor_authentication_code(direct_otp)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def send_new_otp_after_login?
|
|
90
|
+
!totp_enabled?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def send_two_factor_authentication_code(code)
|
|
94
|
+
raise NotImplementedError.new("No default implementation - please define in your class.")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def max_login_attempts?
|
|
98
|
+
second_factor_attempts_count.to_i >= max_login_attempts.to_i
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def max_login_attempts
|
|
102
|
+
self.class.max_login_attempts
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def totp_enabled?
|
|
106
|
+
respond_to?(:otp_secret_key) && !otp_secret_key.nil?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def generate_totp_secret
|
|
110
|
+
self.class.generate_totp_secret
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def create_direct_otp(options = {})
|
|
114
|
+
# Create a new random OTP and store it in the database
|
|
115
|
+
digits = options[:length] || self.class.direct_otp_length || 6
|
|
116
|
+
update_columns(
|
|
117
|
+
direct_otp: random_base10(digits),
|
|
118
|
+
direct_otp_sent_at: Time.now.utc
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def without_spaces(code)
|
|
125
|
+
code.gsub(/[[:space:]]/, '')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def random_base10(digits)
|
|
129
|
+
SecureRandom.random_number(10**digits).to_s.rjust(digits, '0')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def direct_otp_expired?
|
|
133
|
+
Time.now.utc > direct_otp_sent_at + self.class.direct_otp_valid_for
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def clear_direct_otp
|
|
137
|
+
update_columns(direct_otp: nil, direct_otp_sent_at: nil)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|