devise-otp 2.0.0 → 2.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 +4 -4
- data/.github/workflows/ci.yml +4 -2
- data/Appraisals +4 -17
- data/CHANGELOG.md +17 -0
- data/Gemfile +4 -4
- data/README.md +32 -13
- data/app/controllers/devise_otp/devise/otp_persistence_controller.rb +7 -0
- data/app/views/devise/otp_tokens/recovery.html.erb +2 -0
- data/bin/appraisal +16 -0
- data/bin/erb_lint +0 -11
- data/bin/rake +16 -0
- data/bin/rubocop +0 -11
- data/config/locales/{en.yml → devise-otp.en.yml} +1 -1
- data/devise-otp.gemspec +1 -3
- data/gemfiles/rails_8.0.gemfile +5 -1
- data/gemfiles/{rails_7.1.gemfile → rails_8.1.gemfile} +7 -7
- data/lib/devise-otp/version.rb +1 -1
- data/lib/devise-otp.rb +1 -1
- data/lib/devise_otp_authenticatable/routes.rb +1 -1
- data/test/dummy/app/assets/stylesheets/application.css +15 -1
- data/test/dummy/app/views/shared/_navbar.html.erb +15 -7
- data/test/dummy/db/schema.rb +73 -73
- data/test/integration/persistence_test.rb +38 -2
- metadata +8 -21
- data/gemfiles/rails_7.2.gemfile +0 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d19fbc8b7c55dd43387e81ac13a7f1807f1a5c700b987c97d99f37985984831c
|
|
4
|
+
data.tar.gz: 8a396782dc0fd10ab4fe45a246f5a7460af6144de7b92834dc6ff755fb5afe43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e92163f98f4ffbf5ee3f649991df5fc40df6f2531fdf7d3d1081d7174e36598e4751de6b258ed10287da5853ff02ea87170c11c9b62122cc6fd5ffec35cd59d
|
|
7
|
+
data.tar.gz: f2640e640e6c72a299c78bf87e7ae6dea9137f2d0fdf9a0a9e0345edac6c4858418f6e4285357ae6266ac3bbeeb1c1c0c65fd23b3554d7b19369d2232f58b58b
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -12,17 +12,19 @@ jobs:
|
|
|
12
12
|
fail-fast: false
|
|
13
13
|
matrix:
|
|
14
14
|
ruby:
|
|
15
|
+
- '4.0'
|
|
15
16
|
- '3.4'
|
|
16
17
|
- '3.3'
|
|
17
18
|
- '3.2'
|
|
18
19
|
- 'head'
|
|
19
20
|
rails:
|
|
21
|
+
- rails_8.1
|
|
20
22
|
- rails_8.0
|
|
21
|
-
- rails_7.2
|
|
22
|
-
- rails_7.1
|
|
23
23
|
exclude:
|
|
24
24
|
- ruby: '3.1'
|
|
25
25
|
rails: 'rails_8.0'
|
|
26
|
+
- ruby: '3.1'
|
|
27
|
+
rails: 'rails_8.1'
|
|
26
28
|
|
|
27
29
|
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
|
|
28
30
|
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.rails }}.gemfile
|
data/Appraisals
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
appraise 'rails_7.1' do
|
|
4
|
-
gem 'rails', '~> 7.1.0'
|
|
5
|
-
gem 'sqlite3', '~> 1.5.0'
|
|
6
|
-
|
|
7
|
-
# Fix:
|
|
8
|
-
# warning: logger was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.5.0.
|
|
9
|
-
# Add logger to your Gemfile or gemspec.
|
|
10
|
-
install_if '-> { Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0") }' do
|
|
11
|
-
gem 'logger'
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
appraise 'rails_7.2' do
|
|
16
|
-
gem 'rails', '~> 7.2.0'
|
|
17
|
-
gem 'sqlite3', '~> 1.5.0'
|
|
18
|
-
end
|
|
19
|
-
|
|
20
3
|
appraise 'rails_8.0' do
|
|
21
4
|
gem 'rails', '~> 8.0.0'
|
|
22
5
|
end
|
|
6
|
+
|
|
7
|
+
appraise 'rails_8.1' do
|
|
8
|
+
gem 'rails', '~> 8.1.0'
|
|
9
|
+
end
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.1.0
|
|
4
|
+
|
|
5
|
+
Bug fixes
|
|
6
|
+
- Fix issue with refreshing credentials when enabling browser persistence;
|
|
7
|
+
- Add back link from "View recovery codes" page;
|
|
8
|
+
|
|
9
|
+
Improvements
|
|
10
|
+
- Simplify "View recovery codes" verbiage;
|
|
11
|
+
- Move locale file to devise-otp.en.yml;
|
|
12
|
+
|
|
13
|
+
Maintenance
|
|
14
|
+
- Disable tests for Rails 7.1 (EOL on 10/2025) and 7.2 (EOL on 8/9/2026)
|
|
15
|
+
|
|
16
|
+
Documentation
|
|
17
|
+
- Switch to native CSS import in README for Propshaft compatibility;
|
|
18
|
+
- Add Getting Started section to README with sample link to OTP Token page;
|
|
19
|
+
|
|
3
20
|
## v2.0.0
|
|
4
21
|
|
|
5
22
|
Bug fixes:
|
data/Gemfile
CHANGED
|
@@ -6,6 +6,7 @@ gemspec
|
|
|
6
6
|
gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
|
|
7
7
|
|
|
8
8
|
gem "capybara"
|
|
9
|
+
gem "debug"
|
|
9
10
|
gem "minitest-reporters", ">= 0.5.0"
|
|
10
11
|
gem "puma"
|
|
11
12
|
gem "rake"
|
|
@@ -13,10 +14,9 @@ gem "rdoc"
|
|
|
13
14
|
gem "shoulda"
|
|
14
15
|
gem "sprockets-rails"
|
|
15
16
|
gem "sqlite3", "~> 2.1"
|
|
17
|
+
gem "timecop", "~> 0.9.10"
|
|
16
18
|
|
|
17
19
|
# Formatting gems
|
|
18
|
-
gem "
|
|
20
|
+
gem "erb_lint", require: false
|
|
21
|
+
gem "standardrb", require: false
|
|
19
22
|
gem "rubocop-rails-omakase", require: false
|
|
20
|
-
gem "erb_lint", require: false
|
|
21
|
-
|
|
22
|
-
gem "debug"
|
data/README.md
CHANGED
|
@@ -50,23 +50,31 @@ Run the following generator to add the necessary configuration options to Devise
|
|
|
50
50
|
|
|
51
51
|
rails g devise_otp:install
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
## Getting Started
|
|
54
|
+
|
|
55
|
+
Create your Devise user model if you have not done so
|
|
56
|
+
|
|
57
|
+
rails g devise MODEL
|
|
58
|
+
|
|
59
|
+
Add Devise OTP to the model
|
|
54
60
|
|
|
55
61
|
rails g devise_otp MODEL
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
Add a link to the OTP Token settings to your navigation menu (or equivalent) for the user model
|
|
58
64
|
|
|
59
|
-
|
|
65
|
+
<%= link_to "OTP Token", user_otp_token_path %>
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
NOTE: replace "user" with the lowercase name of the model (same as the main devise gem)
|
|
62
68
|
|
|
63
|
-
|
|
69
|
+
## Default CSS
|
|
64
70
|
|
|
65
|
-
|
|
71
|
+
To use the default CSS for devise-otp, just import the devise-otp.css file into your application.css file (or equivalent):
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
@import url("devise-otp.css");
|
|
68
74
|
|
|
69
|
-
|
|
75
|
+
NOTE: You can also copy/paste the styles from [devise-otp.css](https://github.com/wmlele/devise-otp/blob/master/app/assets/stylesheets/devise-otp.css) into your stylesheet.
|
|
76
|
+
|
|
77
|
+
## Custom views
|
|
70
78
|
|
|
71
79
|
If you want to customise your views, you can use the following generator to eject the default view files:
|
|
72
80
|
|
|
@@ -74,11 +82,11 @@ If you want to customise your views, you can use the following generator to ejec
|
|
|
74
82
|
|
|
75
83
|
By default, the files live within the Devise namespace (`app/views/devise`, but if you want to move them or want to match the Devise configuration, set `config.otp_controller_path` in your initializers.
|
|
76
84
|
|
|
77
|
-
|
|
85
|
+
## I18n
|
|
78
86
|
|
|
79
87
|
The install generator also installs an english copy of a Devise OTP i18n file. This can be modified (or used to create other language versions) and is located at: _config/locales/devise.otp.en.yml_
|
|
80
88
|
|
|
81
|
-
|
|
89
|
+
## QR codes
|
|
82
90
|
|
|
83
91
|
Devise OTP generates QR Codes directly as SVG's via the [rqrcode](https://github.com/whomwah/rqrcode), so there are no JavaScript (or Sprockets) dependencies.
|
|
84
92
|
|
|
@@ -110,10 +118,21 @@ To run the devise-otp dummy application in the development environment:
|
|
|
110
118
|
- Run the rails console or server (e.g. "rails c")
|
|
111
119
|
|
|
112
120
|
To run the tests for devise-otp against your current Ruby/Rails configuration:
|
|
113
|
-
- Navigate to the dummy app directory
|
|
114
|
-
|
|
121
|
+
- Navigate to the dummy app directory
|
|
122
|
+
|
|
123
|
+
cd test/dummy
|
|
124
|
+
|
|
125
|
+
- Create and migrate the database for the test environment
|
|
126
|
+
|
|
127
|
+
RAILS\_ENV=test rails db:drop
|
|
128
|
+
RAILS\_ENV=test rails db:create
|
|
129
|
+
RAILS\_ENV=test rails db:migrate
|
|
130
|
+
|
|
115
131
|
- Return to the root directory of devise-otp
|
|
116
|
-
|
|
132
|
+
|
|
133
|
+
- Execute the tests
|
|
134
|
+
|
|
135
|
+
rake test
|
|
117
136
|
|
|
118
137
|
To check Ruby syntax and formatting:
|
|
119
138
|
- Rubocop: "./bin/rubocop"
|
|
@@ -6,6 +6,13 @@ module DeviseOtp
|
|
|
6
6
|
prepend_before_action :ensure_credentials_refresh
|
|
7
7
|
prepend_before_action :authenticate_scope!
|
|
8
8
|
|
|
9
|
+
#
|
|
10
|
+
# fallback in case of redirect after authentication
|
|
11
|
+
#
|
|
12
|
+
def show
|
|
13
|
+
redirect_to otp_token_path_for(resource)
|
|
14
|
+
end
|
|
15
|
+
|
|
9
16
|
#
|
|
10
17
|
# makes the current browser persistent
|
|
11
18
|
#
|
data/bin/appraisal
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# This file was generated by Bundler.
|
|
6
|
+
#
|
|
7
|
+
# The application 'appraisal' is installed as part of a gem, and
|
|
8
|
+
# this file is here to facilitate running it.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
|
12
|
+
|
|
13
|
+
require "rubygems"
|
|
14
|
+
require "bundler/setup"
|
|
15
|
+
|
|
16
|
+
load Gem.bin_path("appraisal", "appraisal")
|
data/bin/erb_lint
CHANGED
|
@@ -10,17 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
|
12
12
|
|
|
13
|
-
bundle_binstub = File.expand_path("bundle", __dir__)
|
|
14
|
-
|
|
15
|
-
if File.file?(bundle_binstub)
|
|
16
|
-
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
|
|
17
|
-
load(bundle_binstub)
|
|
18
|
-
else
|
|
19
|
-
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
|
20
|
-
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
13
|
require "rubygems"
|
|
25
14
|
require "bundler/setup"
|
|
26
15
|
|
data/bin/rake
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# This file was generated by Bundler.
|
|
6
|
+
#
|
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
|
8
|
+
# this file is here to facilitate running it.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
|
12
|
+
|
|
13
|
+
require "rubygems"
|
|
14
|
+
require "bundler/setup"
|
|
15
|
+
|
|
16
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/rubocop
CHANGED
|
@@ -10,17 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
|
12
12
|
|
|
13
|
-
bundle_binstub = File.expand_path("bundle", __dir__)
|
|
14
|
-
|
|
15
|
-
if File.file?(bundle_binstub)
|
|
16
|
-
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
|
|
17
|
-
load(bundle_binstub)
|
|
18
|
-
else
|
|
19
|
-
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
|
20
|
-
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
13
|
require "rubygems"
|
|
25
14
|
require "bundler/setup"
|
|
26
15
|
|
|
@@ -43,7 +43,7 @@ en:
|
|
|
43
43
|
explain: 'Take note or print these recovery codes. They will allow you to log in if your token device is lost, stolen, or unavailable.'
|
|
44
44
|
sequence: 'Sequence'
|
|
45
45
|
code: 'Recovery Code'
|
|
46
|
-
codes_list: '
|
|
46
|
+
codes_list: 'View recovery codes'
|
|
47
47
|
download_codes: 'Download recovery codes'
|
|
48
48
|
otp_persistence:
|
|
49
49
|
successfully_set_persistence: 'Your device is now trusted.'
|
data/devise-otp.gemspec
CHANGED
|
@@ -18,9 +18,7 @@ Gem::Specification.new do |gem|
|
|
|
18
18
|
gem.required_ruby_version = ">= 3.2.0"
|
|
19
19
|
|
|
20
20
|
gem.add_dependency "rails", ">= 7.1"
|
|
21
|
-
gem.add_dependency "devise", ">= 4.8.0", "<
|
|
21
|
+
gem.add_dependency "devise", ">= 4.8.0", "< 6.0"
|
|
22
22
|
gem.add_dependency "rotp", ">= 2.0.0"
|
|
23
23
|
gem.add_dependency "rqrcode", "~> 3.0"
|
|
24
|
-
|
|
25
|
-
gem.add_development_dependency "timecop", "~> 0.9.10"
|
|
26
24
|
end
|
data/gemfiles/rails_8.0.gemfile
CHANGED
|
@@ -11,7 +11,11 @@ gem "rdoc"
|
|
|
11
11
|
gem "shoulda"
|
|
12
12
|
gem "sprockets-rails"
|
|
13
13
|
gem "sqlite3", "~> 2.1"
|
|
14
|
-
gem "
|
|
14
|
+
gem "timecop", "~> 0.9.10"
|
|
15
|
+
gem "standardrb", require: false
|
|
16
|
+
gem "rubocop-rails-omakase", require: false
|
|
17
|
+
gem "erb_lint", require: false
|
|
18
|
+
gem "debug"
|
|
15
19
|
gem "rails", "~> 8.0.0"
|
|
16
20
|
|
|
17
21
|
gemspec path: "../"
|
|
@@ -10,12 +10,12 @@ gem "rake"
|
|
|
10
10
|
gem "rdoc"
|
|
11
11
|
gem "shoulda"
|
|
12
12
|
gem "sprockets-rails"
|
|
13
|
-
gem "sqlite3", "~> 1
|
|
14
|
-
gem "
|
|
15
|
-
gem "
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
gem "sqlite3", "~> 2.1"
|
|
14
|
+
gem "timecop", "~> 0.9.10"
|
|
15
|
+
gem "standardrb", require: false
|
|
16
|
+
gem "rubocop-rails-omakase", require: false
|
|
17
|
+
gem "erb_lint", require: false
|
|
18
|
+
gem "debug"
|
|
19
|
+
gem "rails", "~> 8.1.0"
|
|
20
20
|
|
|
21
21
|
gemspec path: "../"
|
data/lib/devise-otp/version.rb
CHANGED
data/lib/devise-otp.rb
CHANGED
|
@@ -11,7 +11,7 @@ module ActionDispatch::Routing
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
if Devise.otp_trust_persistence
|
|
14
|
-
resource :persistence, :
|
|
14
|
+
resource :persistence, only: [:show, :create, :destroy],
|
|
15
15
|
path: mapping.path_names[:persistence], controller: controllers[:otp_persistence] do
|
|
16
16
|
delete :reset
|
|
17
17
|
end
|
|
@@ -8,7 +8,21 @@
|
|
|
8
8
|
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
|
9
9
|
* compiled file, but it's generally better to create a new file per style scope.
|
|
10
10
|
*
|
|
11
|
-
*= require devise-otp
|
|
12
11
|
*= require_self
|
|
13
12
|
*= require_tree .
|
|
14
13
|
*/
|
|
14
|
+
|
|
15
|
+
@import url("devise-otp.css");
|
|
16
|
+
|
|
17
|
+
nav {
|
|
18
|
+
ul {
|
|
19
|
+
float: right;
|
|
20
|
+
margin: 0;
|
|
21
|
+
display: flex;
|
|
22
|
+
}
|
|
23
|
+
li {
|
|
24
|
+
list-style: none;
|
|
25
|
+
padding: 0.25rem 0.5rem;;
|
|
26
|
+
margin: 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
<nav>
|
|
2
|
-
<
|
|
2
|
+
<ul style="float: right;">
|
|
3
|
+
<li><%= link_to "Home", root_path %></li>
|
|
3
4
|
<% if current_user.present? %>
|
|
4
|
-
|
|
5
|
+
<li><%= link_to "Posts", posts_path %></li>
|
|
6
|
+
<li><%= link_to "OTP Token", user_otp_token_path %></li>
|
|
7
|
+
<li><%= button_to "Sign Out", destroy_user_session_path, method: :delete, data: { turbo_method: :delete } %></li>
|
|
5
8
|
<% elsif current_admin.present? %>
|
|
6
|
-
|
|
9
|
+
<li><%= link_to "Posts", admin_posts_path %></li>
|
|
10
|
+
<li><%= link_to "OTP Token", admin_otp_token_path %></li>
|
|
11
|
+
<li><%= button_to "Sign Out", destroy_admin_session_path, method: :delete, data: { turbo_method: :delete } %></li>
|
|
7
12
|
<% elsif current_lockable_user.present? %>
|
|
8
|
-
|
|
13
|
+
<li><%= link_to "OTP Token", lockable_user_otp_token_path %></li>
|
|
14
|
+
<li><%= button_to "Sign Out", destroy_lockable_user_session_path, method: :delete, data: { turbo_method: :delete } %></li>
|
|
9
15
|
<% elsif current_non_otp_user.present? %>
|
|
10
|
-
|
|
16
|
+
<li><%= link_to "Posts", admin_posts_path %></li>
|
|
17
|
+
<li><%= button_to "Sign Out", destroy_non_otp_user_session_path, method: :delete, data: { turbo_method: :delete } %></li>
|
|
11
18
|
<% elsif current_rememberable_user.present? %>
|
|
12
|
-
|
|
19
|
+
<li><%= link_to "OTP Token", rememberable_user_otp_token_path %></li>
|
|
20
|
+
<li><%= button_to "Sign Out", destroy_rememberable_user_session_path, method: :delete, data: { turbo_method: :delete } %></li>
|
|
13
21
|
<% end %>
|
|
14
|
-
</
|
|
22
|
+
</ul >
|
|
15
23
|
</nav>
|
data/test/dummy/db/schema.rb
CHANGED
|
@@ -12,34 +12,34 @@
|
|
|
12
12
|
|
|
13
13
|
ActiveRecord::Schema[8.0].define(version: 2025_08_18_030305) do
|
|
14
14
|
create_table "admins", force: :cascade do |t|
|
|
15
|
-
t.string "
|
|
15
|
+
t.string "authentication_token"
|
|
16
16
|
t.datetime "created_at", precision: nil, null: false
|
|
17
|
-
t.datetime "
|
|
17
|
+
t.datetime "current_sign_in_at", precision: nil
|
|
18
|
+
t.string "current_sign_in_ip"
|
|
18
19
|
t.string "email", default: "", null: false
|
|
19
20
|
t.string "encrypted_password", default: "", null: false
|
|
20
|
-
t.
|
|
21
|
-
t.datetime "reset_password_sent_at", precision: nil
|
|
22
|
-
t.datetime "remember_created_at", precision: nil
|
|
23
|
-
t.integer "sign_in_count", default: 0
|
|
24
|
-
t.datetime "current_sign_in_at", precision: nil
|
|
21
|
+
t.integer "failed_attempts", default: 0
|
|
25
22
|
t.datetime "last_sign_in_at", precision: nil
|
|
26
|
-
t.string "current_sign_in_ip"
|
|
27
23
|
t.string "last_sign_in_ip"
|
|
28
|
-
t.integer "failed_attempts", default: 0
|
|
29
|
-
t.string "unlock_token"
|
|
30
24
|
t.datetime "locked_at", precision: nil
|
|
31
|
-
t.string "
|
|
25
|
+
t.string "name"
|
|
32
26
|
t.string "otp_auth_secret"
|
|
33
|
-
t.
|
|
27
|
+
t.datetime "otp_challenge_expires", precision: nil
|
|
34
28
|
t.boolean "otp_enabled", default: false, null: false
|
|
35
|
-
t.boolean "otp_mandatory", default: false, null: false
|
|
36
29
|
t.datetime "otp_enabled_on", precision: nil
|
|
37
|
-
t.integer "otp_time_drift", default: 0, null: false
|
|
38
30
|
t.integer "otp_failed_attempts", default: 0, null: false
|
|
39
|
-
t.
|
|
31
|
+
t.boolean "otp_mandatory", default: false, null: false
|
|
40
32
|
t.string "otp_persistence_seed"
|
|
33
|
+
t.integer "otp_recovery_counter", default: 0, null: false
|
|
34
|
+
t.string "otp_recovery_secret"
|
|
41
35
|
t.string "otp_session_challenge"
|
|
42
|
-
t.
|
|
36
|
+
t.integer "otp_time_drift", default: 0, null: false
|
|
37
|
+
t.datetime "remember_created_at", precision: nil
|
|
38
|
+
t.datetime "reset_password_sent_at", precision: nil
|
|
39
|
+
t.string "reset_password_token"
|
|
40
|
+
t.integer "sign_in_count", default: 0
|
|
41
|
+
t.string "unlock_token"
|
|
42
|
+
t.datetime "updated_at", precision: nil, null: false
|
|
43
43
|
t.index ["authentication_token"], name: "index_admins_on_authentication_token", unique: true
|
|
44
44
|
t.index ["email"], name: "index_admins_on_email", unique: true
|
|
45
45
|
t.index ["otp_challenge_expires"], name: "index_admins_on_otp_challenge_expires"
|
|
@@ -49,34 +49,34 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_18_030305) do
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
create_table "lockable_users", force: :cascade do |t|
|
|
52
|
-
t.string "
|
|
52
|
+
t.string "authentication_token"
|
|
53
53
|
t.datetime "created_at", precision: nil, null: false
|
|
54
|
-
t.datetime "
|
|
54
|
+
t.datetime "current_sign_in_at", precision: nil
|
|
55
|
+
t.string "current_sign_in_ip"
|
|
55
56
|
t.string "email", default: "", null: false
|
|
56
57
|
t.string "encrypted_password", default: "", null: false
|
|
57
|
-
t.
|
|
58
|
-
t.datetime "reset_password_sent_at", precision: nil
|
|
59
|
-
t.datetime "remember_created_at", precision: nil
|
|
60
|
-
t.integer "sign_in_count", default: 0
|
|
61
|
-
t.datetime "current_sign_in_at", precision: nil
|
|
58
|
+
t.integer "failed_attempts", default: 0
|
|
62
59
|
t.datetime "last_sign_in_at", precision: nil
|
|
63
|
-
t.string "current_sign_in_ip"
|
|
64
60
|
t.string "last_sign_in_ip"
|
|
65
|
-
t.integer "failed_attempts", default: 0
|
|
66
|
-
t.string "unlock_token"
|
|
67
61
|
t.datetime "locked_at", precision: nil
|
|
68
|
-
t.string "
|
|
62
|
+
t.string "name"
|
|
69
63
|
t.string "otp_auth_secret"
|
|
70
|
-
t.
|
|
64
|
+
t.datetime "otp_challenge_expires", precision: nil
|
|
71
65
|
t.boolean "otp_enabled", default: false, null: false
|
|
72
|
-
t.boolean "otp_mandatory", default: false, null: false
|
|
73
66
|
t.datetime "otp_enabled_on", precision: nil
|
|
74
|
-
t.integer "otp_time_drift", default: 0, null: false
|
|
75
67
|
t.integer "otp_failed_attempts", default: 0, null: false
|
|
76
|
-
t.
|
|
68
|
+
t.boolean "otp_mandatory", default: false, null: false
|
|
77
69
|
t.string "otp_persistence_seed"
|
|
70
|
+
t.integer "otp_recovery_counter", default: 0, null: false
|
|
71
|
+
t.string "otp_recovery_secret"
|
|
78
72
|
t.string "otp_session_challenge"
|
|
79
|
-
t.
|
|
73
|
+
t.integer "otp_time_drift", default: 0, null: false
|
|
74
|
+
t.datetime "remember_created_at", precision: nil
|
|
75
|
+
t.datetime "reset_password_sent_at", precision: nil
|
|
76
|
+
t.string "reset_password_token"
|
|
77
|
+
t.integer "sign_in_count", default: 0
|
|
78
|
+
t.string "unlock_token"
|
|
79
|
+
t.datetime "updated_at", precision: nil, null: false
|
|
80
80
|
t.index ["authentication_token"], name: "index_lockable_users_on_authentication_token", unique: true
|
|
81
81
|
t.index ["email"], name: "index_lockable_users_on_email", unique: true
|
|
82
82
|
t.index ["otp_challenge_expires"], name: "index_lockable_users_on_otp_challenge_expires"
|
|
@@ -86,23 +86,23 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_18_030305) do
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
create_table "non_otp_users", force: :cascade do |t|
|
|
89
|
-
t.string "
|
|
90
|
-
t.datetime "created_at", null: false
|
|
91
|
-
t.datetime "
|
|
89
|
+
t.string "authentication_token"
|
|
90
|
+
t.datetime "created_at", precision: nil, null: false
|
|
91
|
+
t.datetime "current_sign_in_at", precision: nil
|
|
92
|
+
t.string "current_sign_in_ip"
|
|
92
93
|
t.string "email", default: "", null: false
|
|
93
94
|
t.string "encrypted_password", default: "", null: false
|
|
95
|
+
t.integer "failed_attempts", default: 0
|
|
96
|
+
t.datetime "last_sign_in_at", precision: nil
|
|
97
|
+
t.string "last_sign_in_ip"
|
|
98
|
+
t.datetime "locked_at", precision: nil
|
|
99
|
+
t.string "name"
|
|
100
|
+
t.datetime "remember_created_at", precision: nil
|
|
101
|
+
t.datetime "reset_password_sent_at", precision: nil
|
|
94
102
|
t.string "reset_password_token"
|
|
95
|
-
t.datetime "reset_password_sent_at"
|
|
96
|
-
t.datetime "remember_created_at"
|
|
97
103
|
t.integer "sign_in_count", default: 0
|
|
98
|
-
t.datetime "current_sign_in_at"
|
|
99
|
-
t.datetime "last_sign_in_at"
|
|
100
|
-
t.string "current_sign_in_ip"
|
|
101
|
-
t.string "last_sign_in_ip"
|
|
102
|
-
t.integer "failed_attempts", default: 0
|
|
103
104
|
t.string "unlock_token"
|
|
104
|
-
t.datetime "
|
|
105
|
-
t.string "authentication_token"
|
|
105
|
+
t.datetime "updated_at", precision: nil, null: false
|
|
106
106
|
t.index ["authentication_token"], name: "index_non_otp_users_on_authentication_token", unique: true
|
|
107
107
|
t.index ["email"], name: "index_non_otp_users_on_email", unique: true
|
|
108
108
|
t.index ["reset_password_token"], name: "index_non_otp_users_on_reset_password_token", unique: true
|
|
@@ -110,35 +110,35 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_18_030305) do
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
create_table "posts", force: :cascade do |t|
|
|
113
|
-
t.string "title"
|
|
114
113
|
t.text "body"
|
|
115
114
|
t.datetime "created_at", precision: nil, null: false
|
|
115
|
+
t.string "title"
|
|
116
116
|
t.datetime "updated_at", precision: nil, null: false
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
create_table "rememberable_users", force: :cascade do |t|
|
|
120
|
-
t.
|
|
120
|
+
t.datetime "current_sign_in_at"
|
|
121
|
+
t.string "current_sign_in_ip"
|
|
121
122
|
t.string "email", default: "", null: false
|
|
122
123
|
t.string "encrypted_password", default: "", null: false
|
|
123
|
-
t.string "reset_password_token"
|
|
124
|
-
t.datetime "reset_password_sent_at"
|
|
125
|
-
t.datetime "remember_created_at"
|
|
126
|
-
t.integer "sign_in_count", default: 0
|
|
127
|
-
t.datetime "current_sign_in_at"
|
|
128
124
|
t.datetime "last_sign_in_at"
|
|
129
|
-
t.string "current_sign_in_ip"
|
|
130
125
|
t.string "last_sign_in_ip"
|
|
126
|
+
t.string "name"
|
|
131
127
|
t.string "otp_auth_secret"
|
|
132
|
-
t.
|
|
128
|
+
t.datetime "otp_challenge_expires"
|
|
133
129
|
t.boolean "otp_enabled", default: false, null: false
|
|
134
|
-
t.boolean "otp_mandatory", default: false, null: false
|
|
135
130
|
t.datetime "otp_enabled_on"
|
|
136
|
-
t.integer "otp_time_drift", default: 0, null: false
|
|
137
131
|
t.integer "otp_failed_attempts", default: 0, null: false
|
|
138
|
-
t.
|
|
132
|
+
t.boolean "otp_mandatory", default: false, null: false
|
|
139
133
|
t.string "otp_persistence_seed"
|
|
134
|
+
t.integer "otp_recovery_counter", default: 0, null: false
|
|
135
|
+
t.string "otp_recovery_secret"
|
|
140
136
|
t.string "otp_session_challenge"
|
|
141
|
-
t.
|
|
137
|
+
t.integer "otp_time_drift", default: 0, null: false
|
|
138
|
+
t.datetime "remember_created_at"
|
|
139
|
+
t.datetime "reset_password_sent_at"
|
|
140
|
+
t.string "reset_password_token"
|
|
141
|
+
t.integer "sign_in_count", default: 0
|
|
142
142
|
t.index ["email"], name: "index_rememberable_users_on_email", unique: true
|
|
143
143
|
t.index ["otp_challenge_expires"], name: "index_rememberable_users_on_otp_challenge_expires"
|
|
144
144
|
t.index ["otp_session_challenge"], name: "index_rememberable_users_on_otp_session_challenge", unique: true
|
|
@@ -146,34 +146,34 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_18_030305) do
|
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
create_table "users", force: :cascade do |t|
|
|
149
|
-
t.string "
|
|
149
|
+
t.string "authentication_token"
|
|
150
150
|
t.datetime "created_at", precision: nil, null: false
|
|
151
|
-
t.datetime "
|
|
151
|
+
t.datetime "current_sign_in_at", precision: nil
|
|
152
|
+
t.string "current_sign_in_ip"
|
|
152
153
|
t.string "email", default: "", null: false
|
|
153
154
|
t.string "encrypted_password", default: "", null: false
|
|
154
|
-
t.
|
|
155
|
-
t.datetime "reset_password_sent_at", precision: nil
|
|
156
|
-
t.datetime "remember_created_at", precision: nil
|
|
157
|
-
t.integer "sign_in_count", default: 0
|
|
158
|
-
t.datetime "current_sign_in_at", precision: nil
|
|
155
|
+
t.integer "failed_attempts", default: 0
|
|
159
156
|
t.datetime "last_sign_in_at", precision: nil
|
|
160
|
-
t.string "current_sign_in_ip"
|
|
161
157
|
t.string "last_sign_in_ip"
|
|
162
|
-
t.integer "failed_attempts", default: 0
|
|
163
|
-
t.string "unlock_token"
|
|
164
158
|
t.datetime "locked_at", precision: nil
|
|
165
|
-
t.string "
|
|
159
|
+
t.string "name"
|
|
166
160
|
t.string "otp_auth_secret"
|
|
167
|
-
t.
|
|
161
|
+
t.datetime "otp_challenge_expires", precision: nil
|
|
168
162
|
t.boolean "otp_enabled", default: false, null: false
|
|
169
|
-
t.boolean "otp_mandatory", default: false, null: false
|
|
170
163
|
t.datetime "otp_enabled_on", precision: nil
|
|
171
|
-
t.integer "otp_time_drift", default: 0, null: false
|
|
172
164
|
t.integer "otp_failed_attempts", default: 0, null: false
|
|
173
|
-
t.
|
|
165
|
+
t.boolean "otp_mandatory", default: false, null: false
|
|
174
166
|
t.string "otp_persistence_seed"
|
|
167
|
+
t.integer "otp_recovery_counter", default: 0, null: false
|
|
168
|
+
t.string "otp_recovery_secret"
|
|
175
169
|
t.string "otp_session_challenge"
|
|
176
|
-
t.
|
|
170
|
+
t.integer "otp_time_drift", default: 0, null: false
|
|
171
|
+
t.datetime "remember_created_at", precision: nil
|
|
172
|
+
t.datetime "reset_password_sent_at", precision: nil
|
|
173
|
+
t.string "reset_password_token"
|
|
174
|
+
t.integer "sign_in_count", default: 0
|
|
175
|
+
t.string "unlock_token"
|
|
176
|
+
t.datetime "updated_at", precision: nil, null: false
|
|
177
177
|
t.index ["authentication_token"], name: "index_users_on_authentication_token", unique: true
|
|
178
178
|
t.index ["email"], name: "index_users_on_email", unique: true
|
|
179
179
|
t.index ["otp_challenge_expires"], name: "index_users_on_otp_challenge_expires"
|
|
@@ -4,7 +4,6 @@ require "integration_tests_helper"
|
|
|
4
4
|
class PersistenceTest < ActionDispatch::IntegrationTest
|
|
5
5
|
def setup
|
|
6
6
|
@old_persistence = User.otp_trust_persistence
|
|
7
|
-
User.otp_trust_persistence = 3.seconds
|
|
8
7
|
end
|
|
9
8
|
|
|
10
9
|
def teardown
|
|
@@ -101,7 +100,25 @@ class PersistenceTest < ActionDispatch::IntegrationTest
|
|
|
101
100
|
assert page.body.match?(user.next_otp_recovery_tokens.values.join("\n"))
|
|
102
101
|
end
|
|
103
102
|
|
|
104
|
-
test "trusted status should
|
|
103
|
+
test "trusted status should last until the expiration date" do
|
|
104
|
+
# log in 1fa
|
|
105
|
+
user = enable_otp_and_sign_in
|
|
106
|
+
otp_challenge_for user
|
|
107
|
+
|
|
108
|
+
visit user_otp_token_path
|
|
109
|
+
assert_equal user_otp_token_path, current_path
|
|
110
|
+
|
|
111
|
+
click_button("Trust this browser")
|
|
112
|
+
assert_text "Your browser is trusted."
|
|
113
|
+
sign_out
|
|
114
|
+
|
|
115
|
+
Timecop.travel(Time.now + 29.days)
|
|
116
|
+
|
|
117
|
+
sign_user_in
|
|
118
|
+
assert_equal root_path, current_path
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
test "trusted status should expire after the expiration date" do
|
|
105
122
|
# log in 1fa
|
|
106
123
|
user = enable_otp_and_sign_in
|
|
107
124
|
otp_challenge_for user
|
|
@@ -132,4 +149,23 @@ class PersistenceTest < ActionDispatch::IntegrationTest
|
|
|
132
149
|
click_button("Trust this browser")
|
|
133
150
|
assert_equal refresh_user_otp_credential_path, current_path
|
|
134
151
|
end
|
|
152
|
+
|
|
153
|
+
test "a user should be redirected to the fallback (OTP Token page) after refreshing their credentials for trusting their browser" do
|
|
154
|
+
# log in 1fa
|
|
155
|
+
user = enable_otp_and_sign_in
|
|
156
|
+
otp_challenge_for user
|
|
157
|
+
|
|
158
|
+
visit user_otp_token_path
|
|
159
|
+
assert_equal user_otp_token_path, current_path
|
|
160
|
+
|
|
161
|
+
Timecop.travel(Time.now + 15.minutes)
|
|
162
|
+
|
|
163
|
+
click_button("Trust this browser")
|
|
164
|
+
assert_equal refresh_user_otp_credential_path, current_path
|
|
165
|
+
|
|
166
|
+
fill_in "user_refresh_password", with: "12345678"
|
|
167
|
+
click_button "Continue..."
|
|
168
|
+
|
|
169
|
+
assert_equal user_otp_token_path, current_path
|
|
170
|
+
end
|
|
135
171
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: devise-otp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lele Forzani
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date:
|
|
13
|
+
date: 2026-08-22 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: rails
|
|
@@ -35,7 +35,7 @@ dependencies:
|
|
|
35
35
|
version: 4.8.0
|
|
36
36
|
- - "<"
|
|
37
37
|
- !ruby/object:Gem::Version
|
|
38
|
-
version: '
|
|
38
|
+
version: '6.0'
|
|
39
39
|
type: :runtime
|
|
40
40
|
prerelease: false
|
|
41
41
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -45,7 +45,7 @@ dependencies:
|
|
|
45
45
|
version: 4.8.0
|
|
46
46
|
- - "<"
|
|
47
47
|
- !ruby/object:Gem::Version
|
|
48
|
-
version: '
|
|
48
|
+
version: '6.0'
|
|
49
49
|
- !ruby/object:Gem::Dependency
|
|
50
50
|
name: rotp
|
|
51
51
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -74,20 +74,6 @@ dependencies:
|
|
|
74
74
|
- - "~>"
|
|
75
75
|
- !ruby/object:Gem::Version
|
|
76
76
|
version: '3.0'
|
|
77
|
-
- !ruby/object:Gem::Dependency
|
|
78
|
-
name: timecop
|
|
79
|
-
requirement: !ruby/object:Gem::Requirement
|
|
80
|
-
requirements:
|
|
81
|
-
- - "~>"
|
|
82
|
-
- !ruby/object:Gem::Version
|
|
83
|
-
version: 0.9.10
|
|
84
|
-
type: :development
|
|
85
|
-
prerelease: false
|
|
86
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
87
|
-
requirements:
|
|
88
|
-
- - "~>"
|
|
89
|
-
- !ruby/object:Gem::Version
|
|
90
|
-
version: 0.9.10
|
|
91
77
|
description: OTP authentication for Devise
|
|
92
78
|
email:
|
|
93
79
|
- lele@windmill.it
|
|
@@ -119,13 +105,14 @@ files:
|
|
|
119
105
|
- app/views/devise/otp_tokens/recovery.html.erb
|
|
120
106
|
- app/views/devise/otp_tokens/recovery_codes.text.erb
|
|
121
107
|
- app/views/devise/otp_tokens/show.html.erb
|
|
108
|
+
- bin/appraisal
|
|
122
109
|
- bin/erb_lint
|
|
110
|
+
- bin/rake
|
|
123
111
|
- bin/rubocop
|
|
124
|
-
- config/locales/en.yml
|
|
112
|
+
- config/locales/devise-otp.en.yml
|
|
125
113
|
- devise-otp.gemspec
|
|
126
|
-
- gemfiles/rails_7.1.gemfile
|
|
127
|
-
- gemfiles/rails_7.2.gemfile
|
|
128
114
|
- gemfiles/rails_8.0.gemfile
|
|
115
|
+
- gemfiles/rails_8.1.gemfile
|
|
129
116
|
- lib/devise-otp.rb
|
|
130
117
|
- lib/devise-otp/version.rb
|
|
131
118
|
- lib/devise/strategies/database_authenticatable.rb
|
data/gemfiles/rails_7.2.gemfile
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# This file was generated by Appraisal
|
|
2
|
-
|
|
3
|
-
source "https://rubygems.org"
|
|
4
|
-
|
|
5
|
-
gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
|
|
6
|
-
gem "capybara"
|
|
7
|
-
gem "minitest-reporters", ">= 0.5.0"
|
|
8
|
-
gem "puma"
|
|
9
|
-
gem "rake"
|
|
10
|
-
gem "rdoc"
|
|
11
|
-
gem "shoulda"
|
|
12
|
-
gem "sprockets-rails"
|
|
13
|
-
gem "sqlite3", "~> 1.5.0"
|
|
14
|
-
gem "standardrb"
|
|
15
|
-
gem "rails", "~> 7.2.0"
|
|
16
|
-
|
|
17
|
-
gemspec path: "../"
|