devise-two-factor 3.0.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of devise-two-factor might be problematic. Click here for more details.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.gitignore +3 -0
- data/.travis.yml +38 -2
- data/Appraisals +29 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +1 -1
- data/README.md +34 -8
- data/certs/tinfoilsecurity-gems-cert.pem +32 -30
- data/devise-two-factor.gemspec +4 -7
- data/gemfiles/rails_4_1.gemfile +8 -0
- data/gemfiles/rails_4_2.gemfile +8 -0
- data/gemfiles/rails_5_0.gemfile +8 -0
- data/gemfiles/rails_5_1.gemfile +8 -0
- data/gemfiles/rails_5_2.gemfile +8 -0
- data/gemfiles/rails_6_0.gemfile +8 -0
- data/lib/devise_two_factor/models/two_factor_authenticatable.rb +17 -11
- data/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb +14 -12
- data/lib/devise_two_factor/spec_helpers/two_factor_backupable_shared_examples.rb +1 -1
- data/lib/devise_two_factor/strategies/two_factor_authenticatable.rb +1 -1
- data/lib/devise_two_factor/strategies/two_factor_backupable.rb +1 -1
- data/lib/devise_two_factor/version.rb +1 -1
- data/spec/devise/models/two_factor_authenticatable_spec.rb +20 -0
- data/spec/spec_helper.rb +2 -1
- metadata +72 -64
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 33ab6513476203a5a4135af19c1f3bbddeeed83fb2ed8bf3a74c2afe2e74be9b
|
4
|
+
data.tar.gz: 16068a92b6b20aa06108cb0e3cd294dc5c98c19563311d024dcfc7e0573030ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 258cd2abf3bc9beb80c0f9fc596b33055efa24ca53177847fbab5a38b80a220e322a2739303b30128c356000dabd708bcc77e835dacecee2e3c9fe51b66c2b33
|
7
|
+
data.tar.gz: 940c49c9b2cbea4832ee8f66c39b26c1c6ad45d09dc054cf0c54d0be823219e18ad4ef7ca12dc388e2eec319534be0d880b5f9d3e2eb5fcace86ce6b9008e960
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data.tar.gz.sig
CHANGED
Binary file
|
data/.gitignore
CHANGED
@@ -9,6 +9,8 @@
|
|
9
9
|
/test/tmp/
|
10
10
|
/test/version_tmp/
|
11
11
|
/tmp/
|
12
|
+
/gemfiles/.bundle/
|
13
|
+
/gemfiles/*.gemfile.lock
|
12
14
|
|
13
15
|
## Specific to RubyMotion:
|
14
16
|
.dat*
|
@@ -42,6 +44,7 @@ build-iPhoneSimulator/
|
|
42
44
|
Gemfile.lock
|
43
45
|
.ruby-version
|
44
46
|
.ruby-gemset
|
47
|
+
.tool-versions
|
45
48
|
|
46
49
|
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
47
50
|
.rvmrc
|
data/.travis.yml
CHANGED
@@ -2,9 +2,45 @@ sudo: false
|
|
2
2
|
language: ruby
|
3
3
|
cache: bundler
|
4
4
|
before_install:
|
5
|
-
- gem update
|
5
|
+
- gem i rubygems-update -v '<3' && update_rubygems
|
6
6
|
- gem update bundler
|
7
|
+
gemfile:
|
8
|
+
- Gemfile
|
9
|
+
- gemfiles/rails_4_1.gemfile
|
10
|
+
- gemfiles/rails_4_2.gemfile
|
11
|
+
- gemfiles/rails_5_0.gemfile
|
12
|
+
- gemfiles/rails_5_1.gemfile
|
13
|
+
- gemfiles/rails_5_2.gemfile
|
14
|
+
- gemfiles/rails_6_0.gemfile
|
7
15
|
rvm:
|
8
16
|
- "2.1"
|
9
17
|
- "2.2"
|
10
|
-
- "2.3.
|
18
|
+
- "2.3.4"
|
19
|
+
- "2.4.0"
|
20
|
+
- "2.4.1"
|
21
|
+
- "2.5"
|
22
|
+
- "2.6"
|
23
|
+
matrix:
|
24
|
+
exclude:
|
25
|
+
- rvm: "2.1"
|
26
|
+
gemfile: gemfiles/rails_5_0.gemfile
|
27
|
+
- rvm: "2.2"
|
28
|
+
gemfile: gemfiles/rails_5_0.gemfile
|
29
|
+
- rvm: "2.1"
|
30
|
+
gemfile: gemfiles/rails_5_1.gemfile
|
31
|
+
- rvm: "2.2"
|
32
|
+
gemfile: gemfiles/rails_5_1.gemfile
|
33
|
+
- rvm: "2.1"
|
34
|
+
gemfile: gemfiles/rails_5_2.gemfile
|
35
|
+
- rvm: "2.2"
|
36
|
+
gemfile: gemfiles/rails_5_2.gemfile
|
37
|
+
- rvm: "2.1"
|
38
|
+
gemfile: gemfiles/rails_6_0.gemfile
|
39
|
+
- rvm: "2.2"
|
40
|
+
gemfile: gemfiles/rails_6_0.gemfile
|
41
|
+
- rvm: "2.3.4"
|
42
|
+
gemfile: gemfiles/rails_6_0.gemfile
|
43
|
+
- rvm: "2.4.0"
|
44
|
+
gemfile: gemfiles/rails_6_0.gemfile
|
45
|
+
- rvm: "2.4.1"
|
46
|
+
gemfile: gemfiles/rails_6_0.gemfile
|
data/Appraisals
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
appraise "rails-4-1" do
|
2
|
+
gem 'railties', '~> 4.1'
|
3
|
+
gem 'activesupport', '~> 4.1'
|
4
|
+
end
|
5
|
+
|
6
|
+
appraise "rails-4-2" do
|
7
|
+
gem 'railties', '~> 4.2'
|
8
|
+
gem 'activesupport', '~> 4.2'
|
9
|
+
end
|
10
|
+
|
11
|
+
appraise "rails-5-0" do
|
12
|
+
gem 'railties', '~> 5.0'
|
13
|
+
gem 'activesupport', '~> 5.0'
|
14
|
+
end
|
15
|
+
|
16
|
+
appraise "rails-5-1" do
|
17
|
+
gem 'railties', '~> 5.1'
|
18
|
+
gem 'activesupport', '~> 5.1'
|
19
|
+
end
|
20
|
+
|
21
|
+
appraise "rails-5-2" do
|
22
|
+
gem 'railties', '~> 5.2'
|
23
|
+
gem 'activesupport', '~> 5.2'
|
24
|
+
end
|
25
|
+
|
26
|
+
appraise "rails-6-0" do
|
27
|
+
gem 'railties', '~> 6.0'
|
28
|
+
gem 'activesupport', '~> 6.0'
|
29
|
+
end
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,27 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## 4.0.0
|
6
|
+
- Update ROTP
|
7
|
+
- Add Rails 6.1 support
|
8
|
+
- Remove timecop dependency
|
9
|
+
- Clarify changes in project ownership
|
10
|
+
- Bugfixes & cleanup
|
11
|
+
|
12
|
+
## 3.1.0
|
13
|
+
- Add Rails 6.0 support
|
14
|
+
- New gem signing certificate
|
15
|
+
- Fix paranoid-mode being ignored
|
16
|
+
|
17
|
+
## 3.0.3
|
18
|
+
- Add Rails 5.2 support
|
19
|
+
|
20
|
+
## 3.0.2
|
21
|
+
- Add Rails 5.1 support
|
22
|
+
|
23
|
+
## 3.0.1
|
24
|
+
- Qualify call to rspec shared_examples
|
25
|
+
|
5
26
|
## 3.0.0
|
6
27
|
See `UPGRADING.md` for specific help with breaking changes from 2.x to 3.0.0.
|
7
28
|
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
# Devise-Two-Factor Authentication
|
2
|
-
By [Tinfoil Security](
|
2
|
+
By [Tinfoil Security](https://www.tinfoilsecurity.com/) (acq. [Synopsys](https://www.synopsys.com/) 2020). Interested in [working with us](https://www.synopsys.com/careers.html)? We're hiring!
|
3
3
|
|
4
4
|
[![Build Status](https://travis-ci.org/tinfoil/devise-two-factor.svg?branch=master)](https://travis-ci.org/tinfoil/devise-two-factor)
|
5
5
|
|
6
|
-
Devise-Two-Factor is a minimalist extension to Devise which offers support for two-factor authentication, through the [TOTP](https://en.wikipedia.org/wiki/Time-based_One-
|
6
|
+
Devise-Two-Factor is a minimalist extension to Devise which offers support for two-factor authentication, through the [TOTP](https://en.wikipedia.org/wiki/Time-based_One-Time_Password) scheme. It:
|
7
7
|
|
8
8
|
* Allows you to incorporate two-factor authentication into your existing models
|
9
9
|
* Is opinionated about security, so you don't have to be
|
10
|
-
* Integrates easily with two-factor applications like Google Authenticator and Authy
|
10
|
+
* Integrates easily with two-factor applications like [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en) and [Authy](https://authy.com/)
|
11
11
|
* Is extensible, and includes two-factor backup codes as an example of how plugins can be structured
|
12
12
|
|
13
|
+
## Contributing
|
14
|
+
We welcome pull requests, bug reports, and other contributions. We're especially looking for help getting this gem fully compatible with Rails 5+ and squashing any deprecation messages.
|
15
|
+
|
13
16
|
## Example App
|
14
17
|
An example Rails 4 application is provided in the `demo` directory. It showcases a minimal example of Devise-Two-Factor in action, and can act as a reference for integrating the gem into your own application.
|
15
18
|
|
@@ -50,7 +53,13 @@ This generator will add a few columns to the specified model:
|
|
50
53
|
* consumed_timestep
|
51
54
|
* otp_required_for_login
|
52
55
|
|
53
|
-
|
56
|
+
Remember to apply the new migration.
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
bundle exec rake db:migrate
|
60
|
+
```
|
61
|
+
|
62
|
+
It also adds the `:two_factor_authenticatable` directive to your model, and sets up your encryption key. If present, it will remove `:database_authenticatable` from the model, as the two strategies are incompatible. Lastly, the generator will add a Warden config block to your Devise initializer, which enables the strategies required for two-factor authentication.
|
54
63
|
|
55
64
|
If you're running Rails 3, or do not have strong parameters enabled, the generator will also setup the required mass-assignment security options in your model.
|
56
65
|
|
@@ -82,7 +91,7 @@ def configure_permitted_parameters
|
|
82
91
|
end
|
83
92
|
```
|
84
93
|
|
85
|
-
**After running the generator, verify that
|
94
|
+
**After running the generator, verify that `:database_authenticatable` is not being loaded by your model. The generator will try to remove it, but if you have a non-standard Devise setup, this step may fail. Loading both `:database_authenticatable` and `:two_factor_authenticatable` in a model will allow users to bypass two-factor authenticatable due to the way Warden handles cascading strategies.**
|
86
95
|
|
87
96
|
## Designing Your Workflow
|
88
97
|
Devise-Two-Factor only worries about the backend, leaving the details of the integration up to you. This means that you're responsible for building the UI that drives the gem. While there is an example Rails application included in the gem, it is important to remember that this gem is intentionally very open-ended, and you should build a user experience which fits your individual application.
|
@@ -92,7 +101,7 @@ There are two key workflows you'll have to think about:
|
|
92
101
|
1. Logging in with two-factor authentication
|
93
102
|
2. Enabling two-factor authentication for a given user
|
94
103
|
|
95
|
-
We chose to keep things as simple as possible, and our implementation can be found by registering at [Tinfoil Security](https://tinfoilsecurity.com/), and enabling two-factor authentication from the [security settings page](https://www.tinfoilsecurity.com/account/security).
|
104
|
+
We chose to keep things as simple as possible, and our implementation can be found by registering at [Tinfoil Security](https://www.tinfoilsecurity.com/), and enabling two-factor authentication from the [security settings page](https://www.tinfoilsecurity.com/account/security).
|
96
105
|
|
97
106
|
|
98
107
|
### Logging In
|
@@ -105,7 +114,7 @@ Logging in with two-factor authentication works extremely similarly to regular d
|
|
105
114
|
These parameters can be submitted to the standard Devise login route, and the strategy will handle the authentication of the user for you.
|
106
115
|
|
107
116
|
### Disabling Automatic Login After Password Resets
|
108
|
-
If you use the Devise
|
117
|
+
If you use the Devise `recoverable` strategy, the default behavior after a password reset is to automatically authenticate the user and log them in. This is obviously a problem if a user has two-factor authentication enabled, as resetting the password would get around the two-factor requirement.
|
109
118
|
|
110
119
|
Because of this, you need to set `sign_in_after_reset_password` to `false` (either globally in your Devise initializer or via `devise_for`).
|
111
120
|
|
@@ -122,13 +131,30 @@ Before you can do this however, you need to decide how you're going to transmit
|
|
122
131
|
|
123
132
|
At Tinfoil Security, we opted to use the excellent [rqrcode-rails3](https://github.com/samvincent/rqrcode-rails3) gem to generate a QR-code representing the user's secret key, which can then be scanned by any mobile two-factor authentication client.
|
124
133
|
|
134
|
+
If you decide to do this you'll need to generate a URI to act as the source for the QR code. This can be done using the `User#otp_provisioning_uri` method.
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
issuer = 'Your App'
|
138
|
+
label = "#{issuer}:#{current_user.email}"
|
139
|
+
|
140
|
+
current_user.otp_provisioning_uri(label, issuer: issuer)
|
141
|
+
|
142
|
+
# > "otpauth://totp/Your%20App:user@example.com?secret=[otp_secret]&issuer=Your+App"
|
143
|
+
```
|
144
|
+
|
125
145
|
If you instead to decide to send the one-time password to the user directly, such as via SMS, you'll need a mechanism for generating the one-time password on the server:
|
126
146
|
|
127
147
|
```ruby
|
128
148
|
current_user.current_otp
|
129
149
|
```
|
130
150
|
|
131
|
-
The generated code will be valid for the duration specified by `otp_allowed_drift`.
|
151
|
+
The generated code will be valid for the duration specified by `otp_allowed_drift`. This value can be modified by adding a config in `config/initializers/devise.rb`.
|
152
|
+
```ruby
|
153
|
+
Devise.otp_allowed_drift = 240 # value in seconds
|
154
|
+
Devise.setup do |config|
|
155
|
+
...
|
156
|
+
end
|
157
|
+
```
|
132
158
|
|
133
159
|
However you decide to handle enrollment, there are a few important considerations to be made:
|
134
160
|
|
@@ -1,33 +1,35 @@
|
|
1
1
|
-----BEGIN CERTIFICATE-----
|
2
|
-
|
3
|
-
VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEfMB0GA1UE
|
4
|
-
ChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEfMB0GA1UEAxMWVGluZm9pbCBTZWN1
|
5
|
-
cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYbc3VwcG9ydEB0aW5mb2lsc2VjdXJp
|
6
|
-
dHkuY29tMB4XDTE0MDUyMDIxMTAwMFoXDTE2MDUyMDIxMTAwMFowgZwxCzAJBgNV
|
2
|
+
MIIGADCCA+igAwIBAgIIP4wV6YA6CO0wDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV
|
7
3
|
BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
4
|
+
ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
|
5
|
+
aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
|
6
|
+
eS5jb20wHhcNMjEwNDA4MTUxODAwWhcNMjExMjI0MDUwNzAwWjCBiDELMAkGA1UE
|
7
|
+
BhMCVVMxCzAJBgNVBAgTAkNBMR8wHQYDVQQKExZUaW5mb2lsIFNlY3VyaXR5LCBJ
|
8
|
+
bmMuMR0wGwYDVQQDExR0aW5mb2lsc2VjdXJpdHktZ2VtczEsMCoGCSqGSIb3DQEJ
|
9
|
+
ARYdZW5naW5lZXJzQHRpbmZvaWxzZWN1cml0eS5jb20wggIiMA0GCSqGSIb3DQEB
|
10
|
+
AQUAA4ICDwAwggIKAoICAQDNJYNH8D+8lACLt3KzjEIPs3XVBCPaMm2eD/Xk9OOT
|
11
|
+
uDV/NqgMK0icD9MRxMUtS3SCrC9QcPocXT76f2LQ3yVJuK+rBUasymEES47PIx2c
|
12
|
+
zC4n4Hga0xPPuBpioO26oaRFsobyzh9RPOIbnYfpjyqtdrbm+YyM3sPR4XzFirv9
|
13
|
+
xomT4E9T4RCLgOQHTcLKL9K9m+EN7PeVdVUXV0Pa7cVs2vJUKedsd7vnr6Lzbn8T
|
14
|
+
oPk/7J/4W931PbaeI5yg9ZuaRa9K2IaY1TkPI67NW4qKitBVepRlXw6Sb7TYcUnc
|
15
|
+
WEQ/eC5CpnOmqUrG5tfGD8cc5aGZOkitW/VXZgVj81xgCv1hk4HjErrqq4FBNAaC
|
16
|
+
SNyBfwR0TUYqg1lN1nbNjOKwfb6YRn06R2ovcFJG0tmGhsQULCr6fW8u2TfSM+U9
|
17
|
+
WFSIJx2griureY7EZPwg/MgsUiWUWMFemz3GVYXWJR3dN2pW9Uqr3rkjKZbA0bst
|
18
|
+
GWahJO9HuFdDakQxoaTPYPtTQDC+kskkO6lKG1KLIoZ1iLZzB1Ks1vEeyE7lp1im
|
19
|
+
WgpUq+q23PFkt1gIBi/4tGvzsLZye25QU2Y+XLzldCNm+DyRFXZ+Q+bK33IveUeU
|
20
|
+
WEOv4T1qTXHAOypyzmgodVRG/PrlsSMOBfE515kG1mDMGjRcCpEtlskgxUbf7qM7
|
21
|
+
hQIDAQABo1gwVjAJBgNVHRMEAjAAMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHBzOi8v
|
22
|
+
d3d3LnRpbmZvaWxzZWN1cml0eS5jb20vc2VjdXJpdHkvcmV2b2NhdGlvbl9saXN0
|
23
|
+
MA0GCSqGSIb3DQEBDQUAA4ICAQB4p1yL6e/38Dmf5HdZoSJzQ7AcM+jrD0LdMC1V
|
24
|
+
H0Y107JzZWvIB2aWH4tw4+SKGTr52OvyGFLpBv5jsWUUFssuAV971T1x41kWJSYt
|
25
|
+
tnljNguSrH6ah/pDravLxi+JGQXMBRXhkdvQKbFOfutSe9HEuZLiWUYNDYM17XJq
|
26
|
+
WmG+QhNgXliXgu4AQg+8vb1rDbu/G491GEuxbwaLyyKG8X+P5mYTBbMQbTgJkNfX
|
27
|
+
elpmFtqivFaEHs3evVGEEZRQhe8i5V0Ak2c4Or1ap/pZQf3hUIkZbw7HumyZYNWi
|
28
|
+
VJDMpObUyucv6++TNW8bAI5Oip8DGeYKibPsJ0IfYxMmRC3BmY1E3IIvAdsUHTcq
|
29
|
+
WapfQlX732+mfx/gSBpuZhdwEqjWj0xPj6l9DjQrGuhUEijfucKqyY3F280OYM1b
|
30
|
+
2zG6cwVmh5IeR9nVv0i2KNkoc2zC8tcGpjfBBuDdXZCpow54DRJU4qQ6S0lH5ojs
|
31
|
+
aQHEEIQ9/STv9TKuc4KlMUey8W6L0Zw+xFWnkLeygaMps1PhPokSbrABQsB4C10Q
|
32
|
+
QSG/Dvvw438W/2sb9aR+skGh1oNAwJiFhLNaNALfkSXRtU16gLMPBJCi2Xqyco7V
|
33
|
+
Wh4SFQHrAbuglSi0nYgFm2SxYf/r6JRKxhVkwo8wxRiV8rDZj7WmzQoZK4GHj1u6
|
34
|
+
LXXw3g==
|
33
35
|
-----END CERTIFICATE-----
|
data/devise-two-factor.gemspec
CHANGED
@@ -17,23 +17,20 @@ Gem::Specification.new do |s|
|
|
17
17
|
'certs/tinfoilsecurity-gems-cert.pem'
|
18
18
|
]
|
19
19
|
s.signing_key = File.expand_path("~/.ssh/tinfoilsecurity-gems-key.pem") if $0 =~ /gem\z/
|
20
|
-
|
21
|
-
s.rubyforge_project = 'devise-two-factor'
|
22
|
-
|
23
20
|
s.files = `git ls-files`.split("\n").delete_if { |x| x.match('demo/*') }
|
24
21
|
s.test_files = `git ls-files -- spec/*`.split("\n")
|
25
22
|
s.require_paths = ['lib']
|
26
23
|
|
27
|
-
s.add_runtime_dependency 'railties'
|
28
|
-
s.add_runtime_dependency 'activesupport'
|
24
|
+
s.add_runtime_dependency 'railties', '< 6.2'
|
25
|
+
s.add_runtime_dependency 'activesupport', '< 6.2'
|
29
26
|
s.add_runtime_dependency 'attr_encrypted', '>= 1.3', '< 4', '!= 2'
|
30
27
|
s.add_runtime_dependency 'devise', '~> 4.0'
|
31
|
-
s.add_runtime_dependency 'rotp', '~>
|
28
|
+
s.add_runtime_dependency 'rotp', '~> 6.0'
|
32
29
|
|
33
30
|
s.add_development_dependency 'activemodel'
|
31
|
+
s.add_development_dependency 'appraisal'
|
34
32
|
s.add_development_dependency 'bundler', '> 1.0'
|
35
33
|
s.add_development_dependency 'rspec', '> 3'
|
36
34
|
s.add_development_dependency 'simplecov'
|
37
35
|
s.add_development_dependency 'faker'
|
38
|
-
s.add_development_dependency 'timecop'
|
39
36
|
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'attr_encrypted'
|
2
1
|
require 'rotp'
|
3
2
|
|
4
3
|
module Devise
|
@@ -8,14 +7,18 @@ module Devise
|
|
8
7
|
include Devise::Models::DatabaseAuthenticatable
|
9
8
|
|
10
9
|
included do
|
11
|
-
unless
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
10
|
+
unless %i[otp_secret otp_secret=].all? { |attr| method_defined?(attr) }
|
11
|
+
require 'attr_encrypted'
|
12
|
+
|
13
|
+
unless singleton_class.ancestors.include?(AttrEncrypted)
|
14
|
+
extend AttrEncrypted
|
15
|
+
end
|
16
|
+
|
17
|
+
unless attr_encrypted?(:otp_secret)
|
18
|
+
attr_encrypted :otp_secret,
|
19
|
+
:key => self.otp_secret_encryption_key,
|
20
|
+
:mode => :per_attribute_iv_and_salt unless self.attr_encrypted?(:otp_secret)
|
21
|
+
end
|
19
22
|
end
|
20
23
|
|
21
24
|
attr_accessor :otp_attempt
|
@@ -31,8 +34,10 @@ module Devise
|
|
31
34
|
otp_secret = options[:otp_secret] || self.otp_secret
|
32
35
|
return false unless code.present? && otp_secret.present?
|
33
36
|
|
34
|
-
totp =
|
35
|
-
|
37
|
+
totp = otp(otp_secret)
|
38
|
+
if totp.verify(code, drift_behind: self.class.otp_allowed_drift, drift_ahead: self.class.otp_allowed_drift)
|
39
|
+
return consume_otp!
|
40
|
+
end
|
36
41
|
|
37
42
|
false
|
38
43
|
end
|
@@ -56,6 +61,7 @@ module Devise
|
|
56
61
|
end
|
57
62
|
|
58
63
|
def clean_up_passwords
|
64
|
+
super
|
59
65
|
self.otp_attempt = nil
|
60
66
|
end
|
61
67
|
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
RSpec.shared_examples 'two_factor_authenticatable' do
|
2
4
|
before :each do
|
3
5
|
subject.otp_secret = subject.class.generate_otp_secret
|
4
6
|
subject.consumed_timestep = nil
|
@@ -32,12 +34,12 @@ shared_examples 'two_factor_authenticatable' do
|
|
32
34
|
let(:otp_secret) { '2z6hxkdwi3uvrnpn' }
|
33
35
|
|
34
36
|
before :each do
|
35
|
-
|
37
|
+
travel_to(Time.now)
|
36
38
|
subject.otp_secret = otp_secret
|
37
39
|
end
|
38
40
|
|
39
41
|
after :each do
|
40
|
-
|
42
|
+
travel_back
|
41
43
|
end
|
42
44
|
|
43
45
|
context 'with a stored consumed_timestep' do
|
@@ -54,7 +56,7 @@ shared_examples 'two_factor_authenticatable' do
|
|
54
56
|
end
|
55
57
|
|
56
58
|
context 'given a previously valid OTP within the allowed drift' do
|
57
|
-
let(:consumed_otp) { ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift
|
59
|
+
let(:consumed_otp) { ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift) }
|
58
60
|
|
59
61
|
before do
|
60
62
|
subject.validate_and_consume_otp!(consumed_otp)
|
@@ -77,17 +79,17 @@ shared_examples 'two_factor_authenticatable' do
|
|
77
79
|
end
|
78
80
|
|
79
81
|
it 'validates an OTP within the allowed drift' do
|
80
|
-
otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift
|
82
|
+
otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift)
|
81
83
|
expect(subject.validate_and_consume_otp!(otp)).to be true
|
82
84
|
end
|
83
85
|
|
84
86
|
it 'does not validate an OTP above the allowed drift' do
|
85
|
-
otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift * 2
|
87
|
+
otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift * 2)
|
86
88
|
expect(subject.validate_and_consume_otp!(otp)).to be false
|
87
89
|
end
|
88
90
|
|
89
91
|
it 'does not validate an OTP below the allowed drift' do
|
90
|
-
otp = ROTP::TOTP.new(otp_secret).at(Time.now - subject.class.otp_allowed_drift * 2
|
92
|
+
otp = ROTP::TOTP.new(otp_secret).at(Time.now - subject.class.otp_allowed_drift * 2)
|
91
93
|
expect(subject.validate_and_consume_otp!(otp)).to be false
|
92
94
|
end
|
93
95
|
end
|
@@ -95,15 +97,15 @@ shared_examples 'two_factor_authenticatable' do
|
|
95
97
|
describe '#otp_provisioning_uri' do
|
96
98
|
let(:otp_secret_length) { subject.class.otp_secret_length }
|
97
99
|
let(:account) { Faker::Internet.email }
|
98
|
-
let(:issuer) {
|
100
|
+
let(:issuer) { 'Tinfoil' }
|
99
101
|
|
100
|
-
it
|
101
|
-
expect(subject.otp_provisioning_uri(account)).to match(%r{otpauth://totp/#{account}\?secret=\w{#{otp_secret_length}}})
|
102
|
+
it 'should return uri with specified account' do
|
103
|
+
expect(subject.otp_provisioning_uri(account)).to match(%r{otpauth://totp/#{CGI.escape(account)}\?secret=\w{#{otp_secret_length}}})
|
102
104
|
end
|
103
105
|
|
104
106
|
it 'should return uri with issuer option' do
|
105
|
-
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{account}\?.*secret=\w{#{otp_secret_length}}(&|$)})
|
106
|
-
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{account}\?.*issuer=#{issuer}(&|$)})
|
107
|
+
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{issuer}:#{CGI.escape(account)}\?.*secret=\w{#{otp_secret_length}}(&|$)})
|
108
|
+
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{issuer}:#{CGI.escape(account)}\?.*issuer=#{issuer}(&|$)})
|
107
109
|
end
|
108
110
|
end
|
109
111
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
shared_examples 'two_factor_backupable' do
|
1
|
+
RSpec.shared_examples 'two_factor_backupable' do
|
2
2
|
describe 'required_fields' do
|
3
3
|
it 'has the attr_encrypted fields for otp_backup_codes' do
|
4
4
|
expect(Devise::Models::TwoFactorBackupable.required_fields(subject.class)).to contain_exactly(:otp_backup_codes)
|
@@ -12,7 +12,7 @@ module Devise
|
|
12
12
|
super
|
13
13
|
end
|
14
14
|
|
15
|
-
fail(:not_found_in_database) unless resource
|
15
|
+
fail(Devise.paranoid ? :invalid : :not_found_in_database) unless resource
|
16
16
|
|
17
17
|
# We want to cascade to the next strategy if this one fails,
|
18
18
|
# but database authenticatable automatically halts on a bad password
|
@@ -12,7 +12,7 @@ module Devise
|
|
12
12
|
super
|
13
13
|
end
|
14
14
|
|
15
|
-
fail(:not_found_in_database) unless resource
|
15
|
+
fail(Devise.paranoid ? :invalid : :not_found_in_database) unless resource
|
16
16
|
|
17
17
|
# We want to cascade to the next strategy if this one fails,
|
18
18
|
# but database authenticatable automatically halts on a bad password
|
@@ -77,3 +77,23 @@ describe ::Devise::Models::TwoFactorAuthenticatable do
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
end
|
80
|
+
|
81
|
+
describe ::Devise::Models::TwoFactorAuthenticatable do
|
82
|
+
context 'When clean_up_passwords is called ' do
|
83
|
+
subject { TwoFactorAuthenticatableDouble.new }
|
84
|
+
before :each do
|
85
|
+
subject.otp_attempt = 'foo'
|
86
|
+
subject.password_confirmation = 'foo'
|
87
|
+
end
|
88
|
+
it 'otp_attempt should be nill' do
|
89
|
+
subject.clean_up_passwords
|
90
|
+
expect(subject.otp_attempt).to be_nil
|
91
|
+
end
|
92
|
+
it 'password_confirmation should be nill' do
|
93
|
+
subject.clean_up_passwords
|
94
|
+
expect(subject.password_confirmation).to be_nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
data/spec/spec_helper.rb
CHANGED
@@ -19,14 +19,15 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
19
19
|
|
20
20
|
require 'rspec'
|
21
21
|
require 'faker'
|
22
|
-
require 'timecop'
|
23
22
|
require 'devise-two-factor'
|
24
23
|
require 'devise_two_factor/spec_helpers'
|
24
|
+
require 'active_support/testing/time_helpers'
|
25
25
|
|
26
26
|
# Requires supporting files with custom matchers and macros, etc,
|
27
27
|
# in ./support/ and its subdirectories.
|
28
28
|
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
29
29
|
|
30
30
|
RSpec.configure do |config|
|
31
|
+
config.include ActiveSupport::Testing::TimeHelpers
|
31
32
|
config.order = 'random'
|
32
33
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: devise-two-factor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shane Wilton
|
@@ -52,68 +52,70 @@ cert_chain:
|
|
52
52
|
-----END CERTIFICATE-----
|
53
53
|
- |
|
54
54
|
-----BEGIN CERTIFICATE-----
|
55
|
-
|
56
|
-
VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEfMB0GA1UE
|
57
|
-
ChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEfMB0GA1UEAxMWVGluZm9pbCBTZWN1
|
58
|
-
cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYbc3VwcG9ydEB0aW5mb2lsc2VjdXJp
|
59
|
-
dHkuY29tMB4XDTE0MDUyMDIxMTAwMFoXDTE2MDUyMDIxMTAwMFowgZwxCzAJBgNV
|
55
|
+
MIIGADCCA+igAwIBAgIIP4wV6YA6CO0wDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV
|
60
56
|
BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
57
|
+
ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
|
58
|
+
aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
|
59
|
+
eS5jb20wHhcNMjEwNDA4MTUxODAwWhcNMjExMjI0MDUwNzAwWjCBiDELMAkGA1UE
|
60
|
+
BhMCVVMxCzAJBgNVBAgTAkNBMR8wHQYDVQQKExZUaW5mb2lsIFNlY3VyaXR5LCBJ
|
61
|
+
bmMuMR0wGwYDVQQDExR0aW5mb2lsc2VjdXJpdHktZ2VtczEsMCoGCSqGSIb3DQEJ
|
62
|
+
ARYdZW5naW5lZXJzQHRpbmZvaWxzZWN1cml0eS5jb20wggIiMA0GCSqGSIb3DQEB
|
63
|
+
AQUAA4ICDwAwggIKAoICAQDNJYNH8D+8lACLt3KzjEIPs3XVBCPaMm2eD/Xk9OOT
|
64
|
+
uDV/NqgMK0icD9MRxMUtS3SCrC9QcPocXT76f2LQ3yVJuK+rBUasymEES47PIx2c
|
65
|
+
zC4n4Hga0xPPuBpioO26oaRFsobyzh9RPOIbnYfpjyqtdrbm+YyM3sPR4XzFirv9
|
66
|
+
xomT4E9T4RCLgOQHTcLKL9K9m+EN7PeVdVUXV0Pa7cVs2vJUKedsd7vnr6Lzbn8T
|
67
|
+
oPk/7J/4W931PbaeI5yg9ZuaRa9K2IaY1TkPI67NW4qKitBVepRlXw6Sb7TYcUnc
|
68
|
+
WEQ/eC5CpnOmqUrG5tfGD8cc5aGZOkitW/VXZgVj81xgCv1hk4HjErrqq4FBNAaC
|
69
|
+
SNyBfwR0TUYqg1lN1nbNjOKwfb6YRn06R2ovcFJG0tmGhsQULCr6fW8u2TfSM+U9
|
70
|
+
WFSIJx2griureY7EZPwg/MgsUiWUWMFemz3GVYXWJR3dN2pW9Uqr3rkjKZbA0bst
|
71
|
+
GWahJO9HuFdDakQxoaTPYPtTQDC+kskkO6lKG1KLIoZ1iLZzB1Ks1vEeyE7lp1im
|
72
|
+
WgpUq+q23PFkt1gIBi/4tGvzsLZye25QU2Y+XLzldCNm+DyRFXZ+Q+bK33IveUeU
|
73
|
+
WEOv4T1qTXHAOypyzmgodVRG/PrlsSMOBfE515kG1mDMGjRcCpEtlskgxUbf7qM7
|
74
|
+
hQIDAQABo1gwVjAJBgNVHRMEAjAAMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHBzOi8v
|
75
|
+
d3d3LnRpbmZvaWxzZWN1cml0eS5jb20vc2VjdXJpdHkvcmV2b2NhdGlvbl9saXN0
|
76
|
+
MA0GCSqGSIb3DQEBDQUAA4ICAQB4p1yL6e/38Dmf5HdZoSJzQ7AcM+jrD0LdMC1V
|
77
|
+
H0Y107JzZWvIB2aWH4tw4+SKGTr52OvyGFLpBv5jsWUUFssuAV971T1x41kWJSYt
|
78
|
+
tnljNguSrH6ah/pDravLxi+JGQXMBRXhkdvQKbFOfutSe9HEuZLiWUYNDYM17XJq
|
79
|
+
WmG+QhNgXliXgu4AQg+8vb1rDbu/G491GEuxbwaLyyKG8X+P5mYTBbMQbTgJkNfX
|
80
|
+
elpmFtqivFaEHs3evVGEEZRQhe8i5V0Ak2c4Or1ap/pZQf3hUIkZbw7HumyZYNWi
|
81
|
+
VJDMpObUyucv6++TNW8bAI5Oip8DGeYKibPsJ0IfYxMmRC3BmY1E3IIvAdsUHTcq
|
82
|
+
WapfQlX732+mfx/gSBpuZhdwEqjWj0xPj6l9DjQrGuhUEijfucKqyY3F280OYM1b
|
83
|
+
2zG6cwVmh5IeR9nVv0i2KNkoc2zC8tcGpjfBBuDdXZCpow54DRJU4qQ6S0lH5ojs
|
84
|
+
aQHEEIQ9/STv9TKuc4KlMUey8W6L0Zw+xFWnkLeygaMps1PhPokSbrABQsB4C10Q
|
85
|
+
QSG/Dvvw438W/2sb9aR+skGh1oNAwJiFhLNaNALfkSXRtU16gLMPBJCi2Xqyco7V
|
86
|
+
Wh4SFQHrAbuglSi0nYgFm2SxYf/r6JRKxhVkwo8wxRiV8rDZj7WmzQoZK4GHj1u6
|
87
|
+
LXXw3g==
|
86
88
|
-----END CERTIFICATE-----
|
87
|
-
date:
|
89
|
+
date: 2021-04-08 00:00:00.000000000 Z
|
88
90
|
dependencies:
|
89
91
|
- !ruby/object:Gem::Dependency
|
90
92
|
name: railties
|
91
93
|
requirement: !ruby/object:Gem::Requirement
|
92
94
|
requirements:
|
93
|
-
- - "
|
95
|
+
- - "<"
|
94
96
|
- !ruby/object:Gem::Version
|
95
|
-
version: '
|
97
|
+
version: '6.2'
|
96
98
|
type: :runtime
|
97
99
|
prerelease: false
|
98
100
|
version_requirements: !ruby/object:Gem::Requirement
|
99
101
|
requirements:
|
100
|
-
- - "
|
102
|
+
- - "<"
|
101
103
|
- !ruby/object:Gem::Version
|
102
|
-
version: '
|
104
|
+
version: '6.2'
|
103
105
|
- !ruby/object:Gem::Dependency
|
104
106
|
name: activesupport
|
105
107
|
requirement: !ruby/object:Gem::Requirement
|
106
108
|
requirements:
|
107
|
-
- - "
|
109
|
+
- - "<"
|
108
110
|
- !ruby/object:Gem::Version
|
109
|
-
version: '
|
111
|
+
version: '6.2'
|
110
112
|
type: :runtime
|
111
113
|
prerelease: false
|
112
114
|
version_requirements: !ruby/object:Gem::Requirement
|
113
115
|
requirements:
|
114
|
-
- - "
|
116
|
+
- - "<"
|
115
117
|
- !ruby/object:Gem::Version
|
116
|
-
version: '
|
118
|
+
version: '6.2'
|
117
119
|
- !ruby/object:Gem::Dependency
|
118
120
|
name: attr_encrypted
|
119
121
|
requirement: !ruby/object:Gem::Requirement
|
@@ -121,12 +123,12 @@ dependencies:
|
|
121
123
|
- - ">="
|
122
124
|
- !ruby/object:Gem::Version
|
123
125
|
version: '1.3'
|
124
|
-
- - "<"
|
125
|
-
- !ruby/object:Gem::Version
|
126
|
-
version: '4'
|
127
126
|
- - "!="
|
128
127
|
- !ruby/object:Gem::Version
|
129
128
|
version: '2'
|
129
|
+
- - "<"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '4'
|
130
132
|
type: :runtime
|
131
133
|
prerelease: false
|
132
134
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -134,12 +136,12 @@ dependencies:
|
|
134
136
|
- - ">="
|
135
137
|
- !ruby/object:Gem::Version
|
136
138
|
version: '1.3'
|
137
|
-
- - "<"
|
138
|
-
- !ruby/object:Gem::Version
|
139
|
-
version: '4'
|
140
139
|
- - "!="
|
141
140
|
- !ruby/object:Gem::Version
|
142
141
|
version: '2'
|
142
|
+
- - "<"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '4'
|
143
145
|
- !ruby/object:Gem::Dependency
|
144
146
|
name: devise
|
145
147
|
requirement: !ruby/object:Gem::Requirement
|
@@ -160,14 +162,14 @@ dependencies:
|
|
160
162
|
requirements:
|
161
163
|
- - "~>"
|
162
164
|
- !ruby/object:Gem::Version
|
163
|
-
version: '
|
165
|
+
version: '6.0'
|
164
166
|
type: :runtime
|
165
167
|
prerelease: false
|
166
168
|
version_requirements: !ruby/object:Gem::Requirement
|
167
169
|
requirements:
|
168
170
|
- - "~>"
|
169
171
|
- !ruby/object:Gem::Version
|
170
|
-
version: '
|
172
|
+
version: '6.0'
|
171
173
|
- !ruby/object:Gem::Dependency
|
172
174
|
name: activemodel
|
173
175
|
requirement: !ruby/object:Gem::Requirement
|
@@ -182,6 +184,20 @@ dependencies:
|
|
182
184
|
- - ">="
|
183
185
|
- !ruby/object:Gem::Version
|
184
186
|
version: '0'
|
187
|
+
- !ruby/object:Gem::Dependency
|
188
|
+
name: appraisal
|
189
|
+
requirement: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - ">="
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '0'
|
194
|
+
type: :development
|
195
|
+
prerelease: false
|
196
|
+
version_requirements: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
185
201
|
- !ruby/object:Gem::Dependency
|
186
202
|
name: bundler
|
187
203
|
requirement: !ruby/object:Gem::Requirement
|
@@ -238,20 +254,6 @@ dependencies:
|
|
238
254
|
- - ">="
|
239
255
|
- !ruby/object:Gem::Version
|
240
256
|
version: '0'
|
241
|
-
- !ruby/object:Gem::Dependency
|
242
|
-
name: timecop
|
243
|
-
requirement: !ruby/object:Gem::Requirement
|
244
|
-
requirements:
|
245
|
-
- - ">="
|
246
|
-
- !ruby/object:Gem::Version
|
247
|
-
version: '0'
|
248
|
-
type: :development
|
249
|
-
prerelease: false
|
250
|
-
version_requirements: !ruby/object:Gem::Requirement
|
251
|
-
requirements:
|
252
|
-
- - ">="
|
253
|
-
- !ruby/object:Gem::Version
|
254
|
-
version: '0'
|
255
257
|
description: Barebones two-factor authentication with Devise
|
256
258
|
email: engineers@tinfoilsecurity.com
|
257
259
|
executables: []
|
@@ -261,6 +263,7 @@ files:
|
|
261
263
|
- ".gitignore"
|
262
264
|
- ".rspec"
|
263
265
|
- ".travis.yml"
|
266
|
+
- Appraisals
|
264
267
|
- CHANGELOG.md
|
265
268
|
- CONTRIBUTING.md
|
266
269
|
- Gemfile
|
@@ -271,6 +274,12 @@ files:
|
|
271
274
|
- certs/tinfoil-cacert.pem
|
272
275
|
- certs/tinfoilsecurity-gems-cert.pem
|
273
276
|
- devise-two-factor.gemspec
|
277
|
+
- gemfiles/rails_4_1.gemfile
|
278
|
+
- gemfiles/rails_4_2.gemfile
|
279
|
+
- gemfiles/rails_5_0.gemfile
|
280
|
+
- gemfiles/rails_5_1.gemfile
|
281
|
+
- gemfiles/rails_5_2.gemfile
|
282
|
+
- gemfiles/rails_6_0.gemfile
|
274
283
|
- lib/devise-two-factor.rb
|
275
284
|
- lib/devise_two_factor/models.rb
|
276
285
|
- lib/devise_two_factor/models/two_factor_authenticatable.rb
|
@@ -305,8 +314,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
305
314
|
- !ruby/object:Gem::Version
|
306
315
|
version: '0'
|
307
316
|
requirements: []
|
308
|
-
|
309
|
-
rubygems_version: 2.6.3
|
317
|
+
rubygems_version: 3.0.3
|
310
318
|
signing_key:
|
311
319
|
specification_version: 4
|
312
320
|
summary: Barebones two-factor authentication with Devise
|
metadata.gz.sig
CHANGED
Binary file
|