devise-two-factor 3.0.2 → 4.0.1
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/.github/workflows/ci.yml +47 -0
- data/Appraisals +19 -4
- data/CHANGELOG.md +22 -0
- data/LICENSE +1 -1
- data/README.md +40 -8
- data/certs/tinfoilsecurity-gems-cert.pem +33 -33
- data/devise-two-factor.gemspec +3 -7
- data/gemfiles/{rails_4_1.gemfile → rails_4.1.gemfile} +0 -0
- data/gemfiles/{rails_4_2.gemfile → rails_4.2.gemfile} +0 -0
- data/gemfiles/{rails_5_0.gemfile → rails_5.0.gemfile} +0 -0
- data/gemfiles/{rails_5_1.gemfile → rails_5.1.gemfile} +0 -0
- data/gemfiles/rails_5.2.gemfile +8 -0
- data/gemfiles/rails_6.0.gemfile +8 -0
- data/gemfiles/rails_6.1.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 +18 -11
- data/lib/devise_two_factor/spec_helpers.rb +6 -0
- 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 +0 -1
- data.tar.gz.sig +0 -0
- metadata +56 -68
- metadata.gz.sig +0 -0
- data/.travis.yml +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: deebbcf66a27a576c35731a7079f7aa31dead706b97ef36d8a492e9842da2d99
|
4
|
+
data.tar.gz: 9659a191445fd665979f42c8a81be6282906dd90f5ebdd1ad714ba106e746a01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea90eaac55d11f619fb76709a576d8df42d413f7eaf4ceedfa72767531fe2d740a819192846cb12070fe32ab0dcb3e92b3a971bef17f86304d76f10eb2d8ba9a
|
7
|
+
data.tar.gz: 55aba2b1a2bae479246b8d579ca4397db1e983857cc75565a1232dfed7a4eb4e5c57363e982ea5d9a433de5e0ab2ad17aee51a5ee79387df1bc7bb15e17a71ba
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
@@ -0,0 +1,47 @@
|
|
1
|
+
name: CI
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
branches:
|
5
|
+
- master
|
6
|
+
pull_request:
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
tests:
|
10
|
+
runs-on: ubuntu-latest
|
11
|
+
strategy:
|
12
|
+
fail-fast: false
|
13
|
+
matrix:
|
14
|
+
# Due to https://github.com/actions/runner/issues/849, we should quote versions
|
15
|
+
ruby: ['2.3', '2.4', '2.5', '2.6', '2.7', '3.0', 'truffleruby-head']
|
16
|
+
rails: ['4.1', '4.2', '5.0', '5.1', '5.2', '6.0', '6.1']
|
17
|
+
exclude:
|
18
|
+
- {ruby: '2.3', rails: '6.0'}
|
19
|
+
- {ruby: '2.3', rails: '6.1'}
|
20
|
+
- {ruby: '2.4', rails: '6.0'}
|
21
|
+
- {ruby: '2.4', rails: '6.1'}
|
22
|
+
- {ruby: '2.7', rails: '4.1'}
|
23
|
+
- {ruby: '2.7', rails: '4.2'}
|
24
|
+
- {ruby: '3.0', rails: '4.1'}
|
25
|
+
- {ruby: '3.0', rails: '4.2'}
|
26
|
+
- {ruby: 'truffleruby-head', rails: '4.1'}
|
27
|
+
- {ruby: 'truffleruby-head', rails: '4.2'}
|
28
|
+
|
29
|
+
name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}
|
30
|
+
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
|
31
|
+
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/rails_${{ matrix.rails }}.gemfile
|
32
|
+
steps:
|
33
|
+
- uses: actions/checkout@v2
|
34
|
+
- name: Set up Ruby
|
35
|
+
uses: ruby/setup-ruby@v1
|
36
|
+
with:
|
37
|
+
ruby-version: ${{ matrix.ruby }}
|
38
|
+
bundler-cache: true
|
39
|
+
- name: Print versions
|
40
|
+
continue-on-error: true
|
41
|
+
run: |
|
42
|
+
ruby --version
|
43
|
+
bundle --version
|
44
|
+
echo "RubyGems version `gem --version`"
|
45
|
+
bundle exec rails --version
|
46
|
+
- name: Run tests
|
47
|
+
run: bundle exec rake
|
data/Appraisals
CHANGED
@@ -1,19 +1,34 @@
|
|
1
|
-
appraise "rails-4
|
1
|
+
appraise "rails-4.1" do
|
2
2
|
gem 'railties', '~> 4.1'
|
3
3
|
gem 'activesupport', '~> 4.1'
|
4
4
|
end
|
5
5
|
|
6
|
-
appraise "rails-4
|
6
|
+
appraise "rails-4.2" do
|
7
7
|
gem 'railties', '~> 4.2'
|
8
8
|
gem 'activesupport', '~> 4.2'
|
9
9
|
end
|
10
10
|
|
11
|
-
appraise "rails-5
|
11
|
+
appraise "rails-5.0" do
|
12
12
|
gem 'railties', '~> 5.0'
|
13
13
|
gem 'activesupport', '~> 5.0'
|
14
14
|
end
|
15
15
|
|
16
|
-
appraise "rails-5
|
16
|
+
appraise "rails-5.1" do
|
17
17
|
gem 'railties', '~> 5.1'
|
18
18
|
gem 'activesupport', '~> 5.1'
|
19
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
|
30
|
+
|
31
|
+
appraise "rails-6.1" do
|
32
|
+
gem 'railties', '~> 6.1'
|
33
|
+
gem 'activesupport', '~> 6.1'
|
34
|
+
end
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,28 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## 4.0.1
|
6
|
+
- Convert CI from Travis CI to Github Actions ([#198](https://github.com/tinfoil/devise-two-factor/pull/198))
|
7
|
+
- Fix ActiveSupport::Testing::TimeHelpers require in shared examples ([#191](https://github.com/tinfoil/devise-two-factor/pull/191))
|
8
|
+
- Accept whitespace in provided codes ([#195](https://github.com/tinfoil/devise-two-factor/pull/195))
|
9
|
+
- Add Truffleruby head to CI ([#200](https://github.com/tinfoil/devise-two-factor/pull/200))
|
10
|
+
|
11
|
+
## 4.0.0
|
12
|
+
- [breaking] Drop support for Ruby <= 2.2
|
13
|
+
- Update ROTP
|
14
|
+
- Add Rails 6.1 support
|
15
|
+
- Remove timecop dependency
|
16
|
+
- Clarify changes in project ownership
|
17
|
+
- Bugfixes & cleanup
|
18
|
+
|
19
|
+
## 3.1.0
|
20
|
+
- Add Rails 6.0 support
|
21
|
+
- New gem signing certificate
|
22
|
+
- Fix paranoid-mode being ignored
|
23
|
+
|
24
|
+
## 3.0.3
|
25
|
+
- Add Rails 5.2 support
|
26
|
+
|
5
27
|
## 3.0.2
|
6
28
|
- Add Rails 5.1 support
|
7
29
|
|
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://github.com/tinfoil/devise-two-factor/actions/workflows/ci.yml/badge.svg)
|
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
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
|
|
@@ -139,7 +148,13 @@ If you instead to decide to send the one-time password to the user directly, suc
|
|
139
148
|
current_user.current_otp
|
140
149
|
```
|
141
150
|
|
142
|
-
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
|
+
```
|
143
158
|
|
144
159
|
However you decide to handle enrollment, there are a few important considerations to be made:
|
145
160
|
|
@@ -224,3 +239,20 @@ require 'devise_two_factor/spec_helpers'
|
|
224
239
|
it_behaves_like "two_factor_authenticatable"
|
225
240
|
it_behaves_like "two_factor_backupable"
|
226
241
|
```
|
242
|
+
|
243
|
+
## Troubleshooting
|
244
|
+
If you are using Rails 4.x and Ruby >= 2.7, you may get an error like
|
245
|
+
|
246
|
+
```
|
247
|
+
An error occurred while loading ./spec/devise/models/two_factor_authenticatable_spec.rb.
|
248
|
+
Failure/Error: require 'devise'
|
249
|
+
|
250
|
+
NoMethodError:
|
251
|
+
undefined method `new' for BigDecimal:Class
|
252
|
+
```
|
253
|
+
see https://github.com/ruby/bigdecimal#which-version-should-you-select and https://github.com/ruby/bigdecimal/issues/127
|
254
|
+
for more details, but you should be able to solve this
|
255
|
+
by explicitly requiring an older version of bigdecimal in your gemfile like
|
256
|
+
```
|
257
|
+
gem "bigdecimal", "~> 1.4"
|
258
|
+
```
|
@@ -1,35 +1,35 @@
|
|
1
1
|
-----BEGIN CERTIFICATE-----
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
2
|
+
MIIGADCCA+igAwIBAgIIP4wV6YA6CO0wDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV
|
3
|
+
BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
|
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==
|
35
35
|
-----END CERTIFICATE-----
|
data/devise-two-factor.gemspec
CHANGED
@@ -17,18 +17,15 @@ 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'
|
34
31
|
s.add_development_dependency 'appraisal'
|
@@ -36,5 +33,4 @@ Gem::Specification.new do |s|
|
|
36
33
|
s.add_development_dependency 'rspec', '> 3'
|
37
34
|
s.add_development_dependency 'simplecov'
|
38
35
|
s.add_development_dependency 'faker'
|
39
|
-
s.add_development_dependency 'timecop'
|
40
36
|
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -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.gsub(/\s+/, ""), 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,3 +1,5 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
1
3
|
RSpec.shared_examples 'two_factor_authenticatable' do
|
2
4
|
before :each do
|
3
5
|
subject.otp_secret = subject.class.generate_otp_secret
|
@@ -32,12 +34,12 @@ RSpec.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 @@ RSpec.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)
|
@@ -71,23 +73,28 @@ RSpec.shared_examples 'two_factor_authenticatable' do
|
|
71
73
|
expect(subject.validate_and_consume_otp!(otp)).to be true
|
72
74
|
end
|
73
75
|
|
76
|
+
it 'validates a precisely correct OTP with whitespace' do
|
77
|
+
otp = ROTP::TOTP.new(otp_secret).at(Time.now)
|
78
|
+
expect(subject.validate_and_consume_otp!(otp.split("").join(" "))).to be true
|
79
|
+
end
|
80
|
+
|
74
81
|
it 'fails a nil OTP value' do
|
75
82
|
otp = nil
|
76
83
|
expect(subject.validate_and_consume_otp!(otp)).to be false
|
77
84
|
end
|
78
85
|
|
79
86
|
it 'validates an OTP within the allowed drift' do
|
80
|
-
otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift
|
87
|
+
otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift)
|
81
88
|
expect(subject.validate_and_consume_otp!(otp)).to be true
|
82
89
|
end
|
83
90
|
|
84
91
|
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
|
92
|
+
otp = ROTP::TOTP.new(otp_secret).at(Time.now + subject.class.otp_allowed_drift * 2)
|
86
93
|
expect(subject.validate_and_consume_otp!(otp)).to be false
|
87
94
|
end
|
88
95
|
|
89
96
|
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
|
97
|
+
otp = ROTP::TOTP.new(otp_secret).at(Time.now - subject.class.otp_allowed_drift * 2)
|
91
98
|
expect(subject.validate_and_consume_otp!(otp)).to be false
|
92
99
|
end
|
93
100
|
end
|
@@ -95,15 +102,15 @@ RSpec.shared_examples 'two_factor_authenticatable' do
|
|
95
102
|
describe '#otp_provisioning_uri' do
|
96
103
|
let(:otp_secret_length) { subject.class.otp_secret_length }
|
97
104
|
let(:account) { Faker::Internet.email }
|
98
|
-
let(:issuer) {
|
105
|
+
let(:issuer) { 'Tinfoil' }
|
99
106
|
|
100
|
-
it
|
101
|
-
expect(subject.otp_provisioning_uri(account)).to match(%r{otpauth://totp/#{account}\?secret=\w{#{otp_secret_length}}})
|
107
|
+
it 'should return uri with specified account' do
|
108
|
+
expect(subject.otp_provisioning_uri(account)).to match(%r{otpauth://totp/#{CGI.escape(account)}\?secret=\w{#{otp_secret_length}}})
|
102
109
|
end
|
103
110
|
|
104
111
|
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}(&|$)})
|
112
|
+
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{issuer}:#{CGI.escape(account)}\?.*secret=\w{#{otp_secret_length}}(&|$)})
|
113
|
+
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{issuer}:#{CGI.escape(account)}\?.*issuer=#{issuer}(&|$)})
|
107
114
|
end
|
108
115
|
end
|
109
116
|
end
|
@@ -1,2 +1,8 @@
|
|
1
|
+
require 'active_support/testing/time_helpers'
|
2
|
+
|
1
3
|
require 'devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples'
|
2
4
|
require 'devise_two_factor/spec_helpers/two_factor_backupable_shared_examples'
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.include ActiveSupport::Testing::TimeHelpers
|
8
|
+
end
|
@@ -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
data.tar.gz.sig
CHANGED
Binary file
|
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.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shane Wilton
|
@@ -52,41 +52,41 @@ cert_chain:
|
|
52
52
|
-----END CERTIFICATE-----
|
53
53
|
- |
|
54
54
|
-----BEGIN CERTIFICATE-----
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
55
|
+
MIIGADCCA+igAwIBAgIIP4wV6YA6CO0wDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV
|
56
|
+
BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
|
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==
|
88
88
|
-----END CERTIFICATE-----
|
89
|
-
date:
|
89
|
+
date: 2021-09-01 00:00:00.000000000 Z
|
90
90
|
dependencies:
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: railties
|
@@ -94,28 +94,28 @@ dependencies:
|
|
94
94
|
requirements:
|
95
95
|
- - "<"
|
96
96
|
- !ruby/object:Gem::Version
|
97
|
-
version: '
|
97
|
+
version: '6.2'
|
98
98
|
type: :runtime
|
99
99
|
prerelease: false
|
100
100
|
version_requirements: !ruby/object:Gem::Requirement
|
101
101
|
requirements:
|
102
102
|
- - "<"
|
103
103
|
- !ruby/object:Gem::Version
|
104
|
-
version: '
|
104
|
+
version: '6.2'
|
105
105
|
- !ruby/object:Gem::Dependency
|
106
106
|
name: activesupport
|
107
107
|
requirement: !ruby/object:Gem::Requirement
|
108
108
|
requirements:
|
109
109
|
- - "<"
|
110
110
|
- !ruby/object:Gem::Version
|
111
|
-
version: '
|
111
|
+
version: '6.2'
|
112
112
|
type: :runtime
|
113
113
|
prerelease: false
|
114
114
|
version_requirements: !ruby/object:Gem::Requirement
|
115
115
|
requirements:
|
116
116
|
- - "<"
|
117
117
|
- !ruby/object:Gem::Version
|
118
|
-
version: '
|
118
|
+
version: '6.2'
|
119
119
|
- !ruby/object:Gem::Dependency
|
120
120
|
name: attr_encrypted
|
121
121
|
requirement: !ruby/object:Gem::Requirement
|
@@ -123,12 +123,12 @@ dependencies:
|
|
123
123
|
- - ">="
|
124
124
|
- !ruby/object:Gem::Version
|
125
125
|
version: '1.3'
|
126
|
-
- - "<"
|
127
|
-
- !ruby/object:Gem::Version
|
128
|
-
version: '4'
|
129
126
|
- - "!="
|
130
127
|
- !ruby/object:Gem::Version
|
131
128
|
version: '2'
|
129
|
+
- - "<"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '4'
|
132
132
|
type: :runtime
|
133
133
|
prerelease: false
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -136,12 +136,12 @@ dependencies:
|
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '1.3'
|
139
|
-
- - "<"
|
140
|
-
- !ruby/object:Gem::Version
|
141
|
-
version: '4'
|
142
139
|
- - "!="
|
143
140
|
- !ruby/object:Gem::Version
|
144
141
|
version: '2'
|
142
|
+
- - "<"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '4'
|
145
145
|
- !ruby/object:Gem::Dependency
|
146
146
|
name: devise
|
147
147
|
requirement: !ruby/object:Gem::Requirement
|
@@ -162,14 +162,14 @@ dependencies:
|
|
162
162
|
requirements:
|
163
163
|
- - "~>"
|
164
164
|
- !ruby/object:Gem::Version
|
165
|
-
version: '
|
165
|
+
version: '6.0'
|
166
166
|
type: :runtime
|
167
167
|
prerelease: false
|
168
168
|
version_requirements: !ruby/object:Gem::Requirement
|
169
169
|
requirements:
|
170
170
|
- - "~>"
|
171
171
|
- !ruby/object:Gem::Version
|
172
|
-
version: '
|
172
|
+
version: '6.0'
|
173
173
|
- !ruby/object:Gem::Dependency
|
174
174
|
name: activemodel
|
175
175
|
requirement: !ruby/object:Gem::Requirement
|
@@ -254,29 +254,15 @@ dependencies:
|
|
254
254
|
- - ">="
|
255
255
|
- !ruby/object:Gem::Version
|
256
256
|
version: '0'
|
257
|
-
- !ruby/object:Gem::Dependency
|
258
|
-
name: timecop
|
259
|
-
requirement: !ruby/object:Gem::Requirement
|
260
|
-
requirements:
|
261
|
-
- - ">="
|
262
|
-
- !ruby/object:Gem::Version
|
263
|
-
version: '0'
|
264
|
-
type: :development
|
265
|
-
prerelease: false
|
266
|
-
version_requirements: !ruby/object:Gem::Requirement
|
267
|
-
requirements:
|
268
|
-
- - ">="
|
269
|
-
- !ruby/object:Gem::Version
|
270
|
-
version: '0'
|
271
257
|
description: Barebones two-factor authentication with Devise
|
272
258
|
email: engineers@tinfoilsecurity.com
|
273
259
|
executables: []
|
274
260
|
extensions: []
|
275
261
|
extra_rdoc_files: []
|
276
262
|
files:
|
263
|
+
- ".github/workflows/ci.yml"
|
277
264
|
- ".gitignore"
|
278
265
|
- ".rspec"
|
279
|
-
- ".travis.yml"
|
280
266
|
- Appraisals
|
281
267
|
- CHANGELOG.md
|
282
268
|
- CONTRIBUTING.md
|
@@ -288,10 +274,13 @@ files:
|
|
288
274
|
- certs/tinfoil-cacert.pem
|
289
275
|
- certs/tinfoilsecurity-gems-cert.pem
|
290
276
|
- devise-two-factor.gemspec
|
291
|
-
- gemfiles/
|
292
|
-
- gemfiles/
|
293
|
-
- gemfiles/
|
294
|
-
- gemfiles/
|
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
|
283
|
+
- gemfiles/rails_6.1.gemfile
|
295
284
|
- lib/devise-two-factor.rb
|
296
285
|
- lib/devise_two_factor/models.rb
|
297
286
|
- lib/devise_two_factor/models/two_factor_authenticatable.rb
|
@@ -326,8 +315,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
326
315
|
- !ruby/object:Gem::Version
|
327
316
|
version: '0'
|
328
317
|
requirements: []
|
329
|
-
|
330
|
-
rubygems_version: 2.6.10
|
318
|
+
rubygems_version: 3.0.3
|
331
319
|
signing_key:
|
332
320
|
specification_version: 4
|
333
321
|
summary: Barebones two-factor authentication with Devise
|
metadata.gz.sig
CHANGED
Binary file
|
data/.travis.yml
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
sudo: false
|
2
|
-
language: ruby
|
3
|
-
cache: bundler
|
4
|
-
before_install:
|
5
|
-
- gem update --system
|
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
|
-
rvm:
|
14
|
-
- "2.1"
|
15
|
-
- "2.2"
|
16
|
-
- "2.3.4"
|
17
|
-
- "2.4.0"
|
18
|
-
- "2.4.1"
|
19
|
-
matrix:
|
20
|
-
exclude:
|
21
|
-
- rvm: "2.1"
|
22
|
-
gemfile: gemfiles/rails_5_0.gemfile
|
23
|
-
- rvm: "2.2"
|
24
|
-
gemfile: gemfiles/rails_5_0.gemfile
|
25
|
-
- rvm: "2.1"
|
26
|
-
gemfile: gemfiles/rails_5_1.gemfile
|
27
|
-
- rvm: "2.2"
|
28
|
-
gemfile: gemfiles/rails_5_1.gemfile
|