quo_vadis 2.1.2 → 2.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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +2 -4
- data/app/controllers/quo_vadis/password_resets_controller.rb +1 -1
- data/app/controllers/quo_vadis/sessions_controller.rb +3 -3
- data/app/controllers/quo_vadis/twofas_controller.rb +1 -1
- data/app/mailers/quo_vadis/mailer.rb +16 -16
- data/app/models/quo_vadis/session.rb +1 -0
- data/app/views/quo_vadis/sessions/index.html.erb +6 -0
- data/lib/quo_vadis/controller.rb +3 -3
- data/lib/quo_vadis/version.rb +1 -1
- data/lib/quo_vadis.rb +6 -4
- data/quo_vadis.gemspec +1 -1
- data/test/mailers/mailer_test.rb +49 -32
- data/test/models/account_test.rb +8 -2
- data/test/models/model_test.rb +4 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d01da21fdfe58d00e4f073036f53df4e0da212a9b202c31bf9fe9ed162e2e2d5
|
4
|
+
data.tar.gz: f9b7b777065c7b4e1c83ed54330bc08608cc6020f55f1b72d316de5a1e4e06f5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d3daf72be8a7e1a4545bc8dc7acd77553c62933dd5837e78679a7581cf44356822fbecf4036d5cd8cac5648636a5077e1edd5cb8d6e804ea7c9d55c10b36dc6e
|
7
|
+
data.tar.gz: eee0046f0ae978f537f85946946cb4fe0267fd866216108f226411a3df479cb1b7e9c5c334d120a9107298ce9c30f3b8b6736dcaf6248a9c4a5e23a34962bc2e
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,28 @@
|
|
4
4
|
## HEAD
|
5
5
|
|
6
6
|
|
7
|
+
## 2.1.5 (27 May 2022)
|
8
|
+
|
9
|
+
* Order sessions list and display more information.
|
10
|
+
* Set status 303 See Other on destroy redirects.
|
11
|
+
* Streamline bundler instructions.
|
12
|
+
|
13
|
+
|
14
|
+
## 2.1.4 (2 October 2021)
|
15
|
+
|
16
|
+
* Allow metadata for login log.
|
17
|
+
|
18
|
+
|
19
|
+
## 2.1.3 (30 September 2021)
|
20
|
+
|
21
|
+
* Pass IP and timestamp as paramenters to mailer.
|
22
|
+
|
23
|
+
|
24
|
+
## 2.1.2 (30 September 2021)
|
25
|
+
|
26
|
+
* Delete existing recovery codes when generating new ones.
|
27
|
+
|
28
|
+
|
7
29
|
## 2.1.1 (8 July 2021)
|
8
30
|
|
9
31
|
* Remove unnecessary route names.
|
data/README.md
CHANGED
@@ -37,11 +37,9 @@ Simple to integrate into your application. The main task is customising the exa
|
|
37
37
|
Add the gem to your Gemfile:
|
38
38
|
|
39
39
|
```ruby
|
40
|
-
|
40
|
+
bundle add 'quo_vadis'
|
41
41
|
```
|
42
42
|
|
43
|
-
Then run `bundle install`.
|
44
|
-
|
45
43
|
Next, add the database tables:
|
46
44
|
|
47
45
|
```
|
@@ -119,7 +117,7 @@ end
|
|
119
117
|
|
120
118
|
__`login(model, browser_session = true)`__
|
121
119
|
|
122
|
-
To log in a user who has authenticated with a password, call `#login(model, browser_session = true)`. For the `browser_session` argument, pass `true` to log in for the duration of the browser session, or `false` to log in for `QuoVadis.session_lifetime` (which could be the browser session anyway).
|
120
|
+
To log in a user who has authenticated with a password, call `#login(model, browser_session = true, metadata: {})`. For the `browser_session` argument, optionally pass `true` to log in for the duration of the browser session, or `false` to log in for `QuoVadis.session_lifetime` (which could be the browser session anyway). Any metadata are stored in the log entry for the login.
|
123
121
|
|
124
122
|
__`request_confirmation(model)`__
|
125
123
|
|
@@ -17,7 +17,7 @@ module QuoVadis
|
|
17
17
|
|
18
18
|
if account
|
19
19
|
token = QuoVadis::PasswordResetToken.generate account
|
20
|
-
QuoVadis.deliver :reset_password, email: account.model.email, url: quo_vadis.password_reset_url(token)
|
20
|
+
QuoVadis.deliver :reset_password, {email: account.model.email, url: quo_vadis.password_reset_url(token)}
|
21
21
|
end
|
22
22
|
|
23
23
|
redirect_to password_resets_path, notice: QuoVadis.translate('flash.password_reset.create')
|
@@ -9,7 +9,7 @@ module QuoVadis
|
|
9
9
|
|
10
10
|
def index
|
11
11
|
@qv_session = qv.session
|
12
|
-
@qv_sessions = @qv_session.account.sessions
|
12
|
+
@qv_sessions = @qv_session.account.sessions.new_to_old
|
13
13
|
end
|
14
14
|
|
15
15
|
|
@@ -58,12 +58,12 @@ module QuoVadis
|
|
58
58
|
current_qv_session.account.sessions.destroy params[:id]
|
59
59
|
qv.log current_qv_session.account, Log::LOGOUT_OTHER
|
60
60
|
flash[:notice] = QuoVadis.translate 'flash.logout.other'
|
61
|
-
redirect_to action: :index
|
61
|
+
redirect_to action: :index, status: :see_other
|
62
62
|
else # this session
|
63
63
|
qv.log authenticated_model.qv_account, Log::LOGOUT
|
64
64
|
qv.logout
|
65
65
|
flash[:notice] = QuoVadis.translate 'flash.logout.self'
|
66
|
-
redirect_to main_app.root_path
|
66
|
+
redirect_to main_app.root_path, status: :see_other
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
@@ -14,7 +14,7 @@ module QuoVadis
|
|
14
14
|
account.sessions.each &:reset_authenticated_with_second_factor # OWASP ASV v4.0, 2.8.6
|
15
15
|
qv.log account, Log::TWOFA_DEACTIVATED
|
16
16
|
QuoVadis.notify :twofa_deactivated_notification, email: authenticated_model.email
|
17
|
-
redirect_to twofa_path, notice: QuoVadis.translate('flash.2fa.invalidated')
|
17
|
+
redirect_to twofa_path, notice: QuoVadis.translate('flash.2fa.invalidated'), status: :see_other
|
18
18
|
end
|
19
19
|
|
20
20
|
private
|
@@ -14,52 +14,52 @@ module QuoVadis
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def email_change_notification
|
17
|
-
@timestamp =
|
18
|
-
@ip =
|
17
|
+
@timestamp = params[:timestamp]
|
18
|
+
@ip = params[:ip]
|
19
19
|
_mail params[:email], QuoVadis.translate('mailer.notification.email_change')
|
20
20
|
end
|
21
21
|
|
22
22
|
def identifier_change_notification
|
23
|
-
@timestamp =
|
23
|
+
@timestamp = params[:timestamp]
|
24
24
|
@identifier = params[:identifier]
|
25
|
-
@ip =
|
25
|
+
@ip = params[:ip]
|
26
26
|
_mail params[:email], QuoVadis.translate('mailer.notification.identifier_change',
|
27
27
|
identifier: params[:identifier])
|
28
28
|
end
|
29
29
|
|
30
30
|
def password_change_notification
|
31
|
-
@timestamp =
|
32
|
-
@ip =
|
31
|
+
@timestamp = params[:timestamp]
|
32
|
+
@ip = params[:ip]
|
33
33
|
_mail params[:email], QuoVadis.translate('mailer.notification.password_change')
|
34
34
|
end
|
35
35
|
|
36
36
|
def password_reset_notification
|
37
|
-
@timestamp =
|
38
|
-
@ip =
|
37
|
+
@timestamp = params[:timestamp]
|
38
|
+
@ip = params[:ip]
|
39
39
|
_mail params[:email], QuoVadis.translate('mailer.notification.password_reset')
|
40
40
|
end
|
41
41
|
|
42
42
|
def totp_setup_notification
|
43
|
-
@timestamp =
|
44
|
-
@ip =
|
43
|
+
@timestamp = params[:timestamp]
|
44
|
+
@ip = params[:ip]
|
45
45
|
_mail params[:email], QuoVadis.translate('mailer.notification.totp_setup')
|
46
46
|
end
|
47
47
|
|
48
48
|
def totp_reuse_notification
|
49
|
-
@timestamp =
|
50
|
-
@ip =
|
49
|
+
@timestamp = params[:timestamp]
|
50
|
+
@ip = params[:ip]
|
51
51
|
_mail params[:email], QuoVadis.translate('mailer.notification.totp_reuse')
|
52
52
|
end
|
53
53
|
|
54
54
|
def twofa_deactivated_notification
|
55
|
-
@timestamp =
|
56
|
-
@ip =
|
55
|
+
@timestamp = params[:timestamp]
|
56
|
+
@ip = params[:ip]
|
57
57
|
_mail params[:email], QuoVadis.translate('mailer.notification.twofa_deactivated')
|
58
58
|
end
|
59
59
|
|
60
60
|
def recovery_codes_generation_notification
|
61
|
-
@timestamp =
|
62
|
-
@ip =
|
61
|
+
@timestamp = params[:timestamp]
|
62
|
+
@ip = params[:ip]
|
63
63
|
_mail params[:email], QuoVadis.translate('mailer.notification.recovery_codes_generation')
|
64
64
|
end
|
65
65
|
|
@@ -3,6 +3,9 @@
|
|
3
3
|
<table>
|
4
4
|
<thead>
|
5
5
|
<tr>
|
6
|
+
<th>Signed in</th>
|
7
|
+
<th>Last seen</th>
|
8
|
+
<th>2FA used</th>
|
6
9
|
<th>IP</th>
|
7
10
|
<th>User agent</th>
|
8
11
|
<th></th>
|
@@ -11,6 +14,9 @@
|
|
11
14
|
<tbody>
|
12
15
|
<% @qv_sessions.each do |sess| %>
|
13
16
|
<tr>
|
17
|
+
<td><time datetime="<%= sess.created_at.to_formatted_s(:iso_8601) %>"><%= sess.created_at.to_formatted_s('%-d %B %Y') %></time></td>
|
18
|
+
<td><time datetime="<%= sess.last_seen_at.to_formatted_s(:iso_8601) %>"><%= sess.last_seen_at.to_formatted_s('%-d %B %Y') %></time></td>
|
19
|
+
<td><%= sess.second_factor_authenticated? ? 'Yes' : 'No' %></td>
|
14
20
|
<td><%= sess.ip %></td>
|
15
21
|
<td><%= sess.user_agent %></td>
|
16
22
|
<td>
|
data/lib/quo_vadis/controller.rb
CHANGED
@@ -36,8 +36,8 @@ module QuoVadis
|
|
36
36
|
#
|
37
37
|
# browser_session - true: login only for duration of browser session
|
38
38
|
# false: login for QuoVadis.session_lifetime (which may be browser session anyway)
|
39
|
-
def login(model, browser_session = true)
|
40
|
-
qv.log model.qv_account, Log::LOGIN_SUCCESS
|
39
|
+
def login(model, browser_session = true, metadata: {})
|
40
|
+
qv.log model.qv_account, Log::LOGIN_SUCCESS, metadata
|
41
41
|
|
42
42
|
qv.prevent_rails_session_fixation
|
43
43
|
|
@@ -87,7 +87,7 @@ module QuoVadis
|
|
87
87
|
|
88
88
|
def request_confirmation(model)
|
89
89
|
token = QuoVadis::AccountConfirmationToken.generate model.qv_account
|
90
|
-
QuoVadis.deliver :account_confirmation, email: model.email, url: quo_vadis.confirmation_url(token)
|
90
|
+
QuoVadis.deliver :account_confirmation, {email: model.email, url: quo_vadis.confirmation_url(token)}
|
91
91
|
session[:account_pending_confirmation] = model.qv_account.id
|
92
92
|
|
93
93
|
flash[:notice] = QuoVadis.translate 'flash.confirmation.create'
|
data/lib/quo_vadis/version.rb
CHANGED
data/lib/quo_vadis.rb
CHANGED
@@ -73,12 +73,14 @@ module QuoVadis
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def notify(action, params)
|
76
|
-
|
76
|
+
deliver(action, params, later: true)
|
77
77
|
end
|
78
78
|
|
79
|
-
def deliver(action, params)
|
80
|
-
mail = QuoVadis::Mailer
|
81
|
-
|
79
|
+
def deliver(action, params, later: QuoVadis.enqueue_transactional_emails)
|
80
|
+
mail = QuoVadis::Mailer
|
81
|
+
.with(params.merge(ip: QuoVadis::CurrentRequestDetails.ip, timestamp: Time.now))
|
82
|
+
.send(action)
|
83
|
+
later ?
|
82
84
|
mail.deliver_later :
|
83
85
|
mail.deliver_now
|
84
86
|
end
|
data/quo_vadis.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.authors = ['Andy Stewart']
|
9
9
|
spec.email = ['boss@airbladesoftware.com']
|
10
10
|
|
11
|
-
spec.summary = 'Multifactor authentication for Rails 6.'
|
11
|
+
spec.summary = 'Multifactor authentication for Rails 6 and 7.'
|
12
12
|
spec.homepage = 'https://github.com/airblade/quo_vadis'
|
13
13
|
spec.license = 'MIT'
|
14
14
|
|
data/test/mailers/mailer_test.rb
CHANGED
@@ -44,14 +44,16 @@ class MailerTest < ActionMailer::TestCase
|
|
44
44
|
|
45
45
|
|
46
46
|
test 'email change notification' do
|
47
|
-
email = QuoVadis::Mailer.with(
|
47
|
+
email = QuoVadis::Mailer.with(
|
48
|
+
email: 'Foo <foo@example.com>',
|
49
|
+
ip: '1.2.3.4',
|
50
|
+
timestamp: Time.now
|
51
|
+
).email_change_notification
|
48
52
|
|
49
53
|
# freeze_time
|
50
54
|
|
51
55
|
assert_emails 1 do
|
52
|
-
|
53
|
-
email.deliver_now
|
54
|
-
end
|
56
|
+
email.deliver_now
|
55
57
|
end
|
56
58
|
|
57
59
|
assert_equal ['foo@example.com'], email.to
|
@@ -62,14 +64,17 @@ class MailerTest < ActionMailer::TestCase
|
|
62
64
|
|
63
65
|
|
64
66
|
test 'identifier change notification' do
|
65
|
-
email = QuoVadis::Mailer.with(
|
67
|
+
email = QuoVadis::Mailer.with(
|
68
|
+
email: 'Foo <foo@example.com>',
|
69
|
+
identifier: 'email',
|
70
|
+
ip: '1.2.3.4',
|
71
|
+
timestamp: Time.now
|
72
|
+
).identifier_change_notification
|
66
73
|
|
67
74
|
# freeze_time
|
68
75
|
|
69
76
|
assert_emails 1 do
|
70
|
-
|
71
|
-
email.deliver_now
|
72
|
-
end
|
77
|
+
email.deliver_now
|
73
78
|
end
|
74
79
|
|
75
80
|
assert_equal ['foo@example.com'], email.to
|
@@ -80,14 +85,16 @@ class MailerTest < ActionMailer::TestCase
|
|
80
85
|
|
81
86
|
|
82
87
|
test 'password change notification' do
|
83
|
-
email = QuoVadis::Mailer.with(
|
88
|
+
email = QuoVadis::Mailer.with(
|
89
|
+
email: 'Foo <foo@example.com>',
|
90
|
+
ip: '1.2.3.4',
|
91
|
+
timestamp: Time.now
|
92
|
+
).password_change_notification
|
84
93
|
|
85
94
|
# freeze_time
|
86
95
|
|
87
96
|
assert_emails 1 do
|
88
|
-
|
89
|
-
email.deliver_now
|
90
|
-
end
|
97
|
+
email.deliver_now
|
91
98
|
end
|
92
99
|
|
93
100
|
assert_equal ['foo@example.com'], email.to
|
@@ -98,14 +105,16 @@ class MailerTest < ActionMailer::TestCase
|
|
98
105
|
|
99
106
|
|
100
107
|
test 'password reset notification' do
|
101
|
-
email = QuoVadis::Mailer.with(
|
108
|
+
email = QuoVadis::Mailer.with(
|
109
|
+
email: 'Foo <foo@example.com>',
|
110
|
+
ip: '1.2.3.4',
|
111
|
+
timestamp: Time.now
|
112
|
+
).password_reset_notification
|
102
113
|
|
103
114
|
# freeze_time
|
104
115
|
|
105
116
|
assert_emails 1 do
|
106
|
-
|
107
|
-
email.deliver_now
|
108
|
-
end
|
117
|
+
email.deliver_now
|
109
118
|
end
|
110
119
|
|
111
120
|
assert_equal ['foo@example.com'], email.to
|
@@ -116,14 +125,16 @@ class MailerTest < ActionMailer::TestCase
|
|
116
125
|
|
117
126
|
|
118
127
|
test 'totp setup notification' do
|
119
|
-
email = QuoVadis::Mailer.with(
|
128
|
+
email = QuoVadis::Mailer.with(
|
129
|
+
email: 'Foo <foo@example.com>',
|
130
|
+
ip: '1.2.3.4',
|
131
|
+
timestamp: Time.now
|
132
|
+
).totp_setup_notification
|
120
133
|
|
121
134
|
# freeze_time
|
122
135
|
|
123
136
|
assert_emails 1 do
|
124
|
-
|
125
|
-
email.deliver_now
|
126
|
-
end
|
137
|
+
email.deliver_now
|
127
138
|
end
|
128
139
|
|
129
140
|
assert_equal ['foo@example.com'], email.to
|
@@ -134,14 +145,16 @@ class MailerTest < ActionMailer::TestCase
|
|
134
145
|
|
135
146
|
|
136
147
|
test 'totp reuse notification' do
|
137
|
-
email = QuoVadis::Mailer.with(
|
148
|
+
email = QuoVadis::Mailer.with(
|
149
|
+
email: 'Foo <foo@example.com>',
|
150
|
+
ip: '1.2.3.4',
|
151
|
+
timestamp: Time.now
|
152
|
+
).totp_reuse_notification
|
138
153
|
|
139
154
|
# freeze_time
|
140
155
|
|
141
156
|
assert_emails 1 do
|
142
|
-
|
143
|
-
email.deliver_now
|
144
|
-
end
|
157
|
+
email.deliver_now
|
145
158
|
end
|
146
159
|
|
147
160
|
assert_equal ['foo@example.com'], email.to
|
@@ -152,14 +165,16 @@ class MailerTest < ActionMailer::TestCase
|
|
152
165
|
|
153
166
|
|
154
167
|
test '2fa deactivated notification' do
|
155
|
-
email = QuoVadis::Mailer.with(
|
168
|
+
email = QuoVadis::Mailer.with(
|
169
|
+
email: 'Foo <foo@example.com>',
|
170
|
+
ip: '1.2.3.4',
|
171
|
+
timestamp: Time.now
|
172
|
+
).twofa_deactivated_notification
|
156
173
|
|
157
174
|
# freeze_time
|
158
175
|
|
159
176
|
assert_emails 1 do
|
160
|
-
|
161
|
-
email.deliver_now
|
162
|
-
end
|
177
|
+
email.deliver_now
|
163
178
|
end
|
164
179
|
|
165
180
|
assert_equal ['foo@example.com'], email.to
|
@@ -170,14 +185,16 @@ class MailerTest < ActionMailer::TestCase
|
|
170
185
|
|
171
186
|
|
172
187
|
test 'recovery codes generation notification' do
|
173
|
-
email = QuoVadis::Mailer.with(
|
188
|
+
email = QuoVadis::Mailer.with(
|
189
|
+
email: 'Foo <foo@example.com>',
|
190
|
+
ip: '1.2.3.4',
|
191
|
+
timestamp: Time.now
|
192
|
+
).recovery_codes_generation_notification
|
174
193
|
|
175
194
|
# freeze_time
|
176
195
|
|
177
196
|
assert_emails 1 do
|
178
|
-
|
179
|
-
email.deliver_now
|
180
|
-
end
|
197
|
+
email.deliver_now
|
181
198
|
end
|
182
199
|
|
183
200
|
assert_equal ['foo@example.com'], email.to
|
data/test/models/account_test.rb
CHANGED
@@ -13,8 +13,11 @@ class AccountTest < ActiveSupport::TestCase
|
|
13
13
|
|
14
14
|
|
15
15
|
test 'notifies on identifier change when notifier is not email' do
|
16
|
+
freeze_time
|
16
17
|
p = Person.create! username: 'bob', email: 'bob@example.com', password: 'secretsecret'
|
17
|
-
assert_enqueued_email_with QuoVadis::Mailer,
|
18
|
+
assert_enqueued_email_with QuoVadis::Mailer,
|
19
|
+
:identifier_change_notification,
|
20
|
+
args: {email: 'bob@example.com', identifier: 'username', ip: nil, timestamp: Time.now} do
|
18
21
|
assert_enqueued_emails 1 do
|
19
22
|
p.update username: 'robert@example.com'
|
20
23
|
end
|
@@ -23,8 +26,11 @@ class AccountTest < ActiveSupport::TestCase
|
|
23
26
|
|
24
27
|
|
25
28
|
test 'does not notify on identifier change when notifier is email' do
|
29
|
+
freeze_time
|
26
30
|
u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
|
27
|
-
assert_enqueued_email_with QuoVadis::Mailer,
|
31
|
+
assert_enqueued_email_with QuoVadis::Mailer,
|
32
|
+
:email_change_notification,
|
33
|
+
args: {email: 'bob@example.com', ip: nil, timestamp: Time.now} do
|
28
34
|
assert_enqueued_emails 1 do
|
29
35
|
u.update email: 'robert@example.com'
|
30
36
|
end
|
data/test/models/model_test.rb
CHANGED
@@ -58,8 +58,11 @@ class ModelTest < ActiveSupport::TestCase
|
|
58
58
|
|
59
59
|
|
60
60
|
test 'notifies on email change' do
|
61
|
+
freeze_time
|
61
62
|
u = User.create! name: 'bob', email: 'bob@example.com', password: '123456789abc'
|
62
|
-
assert_enqueued_email_with QuoVadis::Mailer,
|
63
|
+
assert_enqueued_email_with QuoVadis::Mailer,
|
64
|
+
:email_change_notification,
|
65
|
+
args: {email: 'bob@example.com', ip: nil, timestamp: Time.now} do
|
63
66
|
u.update email: 'robert@example.com'
|
64
67
|
end
|
65
68
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quo_vadis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1.
|
4
|
+
version: 2.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Stewart
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-05-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -224,8 +224,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
224
224
|
- !ruby/object:Gem::Version
|
225
225
|
version: '0'
|
226
226
|
requirements: []
|
227
|
-
rubygems_version: 3.
|
227
|
+
rubygems_version: 3.2.22
|
228
228
|
signing_key:
|
229
229
|
specification_version: 4
|
230
|
-
summary: Multifactor authentication for Rails 6.
|
230
|
+
summary: Multifactor authentication for Rails 6 and 7.
|
231
231
|
test_files: []
|