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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afd51a7a5178b6da891987b166c3f838833ec6c6c506ceb457c49c743b925244
4
- data.tar.gz: 20e043c52ed71c4d6e8c5b9dd309badbf656d6d0d386cdae1269d8440a91d09e
3
+ metadata.gz: d19fbc8b7c55dd43387e81ac13a7f1807f1a5c700b987c97d99f37985984831c
4
+ data.tar.gz: 8a396782dc0fd10ab4fe45a246f5a7460af6144de7b92834dc6ff755fb5afe43
5
5
  SHA512:
6
- metadata.gz: faee3c01bdf932a07843ea991f87cffa40081a1339647960382e7ad6532302aeb02cd0ac39de8a643a5b3a33f87a134a3bf11a30969b3c5a3d64c6bea3cb0a6f
7
- data.tar.gz: f08c0a495128b23816751a3386082bf250d3791ce2dc25c0f4e4facd6e911053900aad3664f75938d3ab67c3757811f189f9a22f5b535c8095a679de3f901c60
6
+ metadata.gz: 5e92163f98f4ffbf5ee3f649991df5fc40df6f2531fdf7d3d1081d7174e36598e4751de6b258ed10287da5853ff02ea87170c11c9b62122cc6fd5ffec35cd59d
7
+ data.tar.gz: f2640e640e6c72a299c78bf87e7ae6dea9137f2d0fdf9a0a9e0345edac6c4858418f6e4285357ae6266ac3bbeeb1c1c0c65fd23b3554d7b19369d2232f58b58b
@@ -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 "standardrb", require: false
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
- After you've created your Devise user models (which is usually done with a `rails g devise MODEL`), set up your Devise OTP additions:
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
- Don't forget to migrate:
63
+ Add a link to the OTP Token settings to your navigation menu (or equivalent) for the user model
58
64
 
59
- rake db:migrate
65
+ <%= link_to "OTP Token", user_otp_token_path %>
60
66
 
61
- ### Default CSS
67
+ NOTE: replace "user" with the lowercase name of the model (same as the main devise gem)
62
68
 
63
- To use the default CSS for devise-otp, just require the devise-otp.css file as usual in your application.css file (or equivalent):
69
+ ## Default CSS
64
70
 
65
- *= require devise-otp
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
- It might be even easier to just copy the styles to your project.
73
+ @import url("devise-otp.css");
68
74
 
69
- ### Custom views
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
- ### I18n
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
- ### QR codes
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 ("cd test/dummy")
114
- - Create and migrate the database for the test environment ("RAILS\_ENV=test rails db:drop db:create db:migrate")
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
- - Run "rake test"
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
  #
@@ -19,3 +19,5 @@
19
19
  </tbody>
20
20
  </caption>
21
21
  </table>
22
+
23
+ <%= link_to "Back", otp_token_path_for(resource) %>
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: 'Here is the list of your recovery codes'
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", "< 5.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
@@ -11,7 +11,11 @@ gem "rdoc"
11
11
  gem "shoulda"
12
12
  gem "sprockets-rails"
13
13
  gem "sqlite3", "~> 2.1"
14
- gem "standardrb"
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.5.0"
14
- gem "standardrb"
15
- gem "rails", "~> 7.1.0"
16
-
17
- install_if -> { Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0") } do
18
- gem "logger"
19
- end
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: "../"
@@ -1,5 +1,5 @@
1
1
  module Devise
2
2
  module OTP
3
- VERSION = "2.0.0"
3
+ VERSION = "2.1.0"
4
4
  end
5
5
  end
data/lib/devise-otp.rb CHANGED
@@ -45,7 +45,7 @@ module Devise
45
45
  @@otp_trust_persistence = 30.days
46
46
 
47
47
  mattr_accessor :otp_drift_window
48
- @@otp_drift_window = 3 # in minutes
48
+ @@otp_drift_window = 3
49
49
 
50
50
  #
51
51
  # if the user wants to change Otp settings,
@@ -11,7 +11,7 @@ module ActionDispatch::Routing
11
11
  end
12
12
 
13
13
  if Devise.otp_trust_persistence
14
- resource :persistence, :only => [:create, :destroy],
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
- <div style="float: right;">
2
+ <ul style="float: right;">
3
+ <li><%= link_to "Home", root_path %></li>
3
4
  <% if current_user.present? %>
4
- <%= button_to "Sign Out", destroy_user_session_path, method: :delete, data: { turbo_method: :delete } %>
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
- <%= button_to "Sign Out", destroy_admin_session_path, method: :delete, data: { turbo_method: :delete } %>
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
- <%= button_to "Sign Out", destroy_lockable_user_session_path, method: :delete, data: { turbo_method: :delete } %>
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
- <%= button_to "Sign Out", destroy_non_otp_user_session_path, method: :delete, data: { turbo_method: :delete } %>
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
- <%= button_to "Sign Out", destroy_rememberable_user_session_path, method: :delete, data: { turbo_method: :delete } %>
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
- </div>
22
+ </ul >
15
23
  </nav>
@@ -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 "name"
15
+ t.string "authentication_token"
16
16
  t.datetime "created_at", precision: nil, null: false
17
- t.datetime "updated_at", precision: nil, null: false
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.string "reset_password_token"
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 "authentication_token"
25
+ t.string "name"
32
26
  t.string "otp_auth_secret"
33
- t.string "otp_recovery_secret"
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.integer "otp_recovery_counter", default: 0, null: false
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.datetime "otp_challenge_expires", precision: nil
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 "name"
52
+ t.string "authentication_token"
53
53
  t.datetime "created_at", precision: nil, null: false
54
- t.datetime "updated_at", precision: nil, null: false
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.string "reset_password_token"
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 "authentication_token"
62
+ t.string "name"
69
63
  t.string "otp_auth_secret"
70
- t.string "otp_recovery_secret"
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.integer "otp_recovery_counter", default: 0, null: false
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.datetime "otp_challenge_expires", precision: nil
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 "name"
90
- t.datetime "created_at", null: false
91
- t.datetime "updated_at", null: false
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 "locked_at"
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.string "name"
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.string "otp_recovery_secret"
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.integer "otp_recovery_counter", default: 0, null: false
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.datetime "otp_challenge_expires"
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 "name"
149
+ t.string "authentication_token"
150
150
  t.datetime "created_at", precision: nil, null: false
151
- t.datetime "updated_at", precision: nil, null: false
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.string "reset_password_token"
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 "authentication_token"
159
+ t.string "name"
166
160
  t.string "otp_auth_secret"
167
- t.string "otp_recovery_secret"
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.integer "otp_recovery_counter", default: 0, null: false
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.datetime "otp_challenge_expires", precision: nil
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 expire" do
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.0.0
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: 2025-10-21 00:00:00.000000000 Z
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: '5.0'
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: '5.0'
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
@@ -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: "../"