devise-two-factor 4.0.0 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci.yml +36 -0
- data/Appraisals +16 -6
- data/CHANGELOG.md +12 -0
- data/README.md +64 -46
- data/UPGRADING.md +193 -0
- data/certs/tinfoil-cacert.pem +12 -12
- data/certs/tinfoilsecurity-gems-cert.pem +14 -14
- data/devise-two-factor.gemspec +2 -3
- 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 → rails_5.2.gemfile} +0 -0
- data/gemfiles/rails_6.0.gemfile +8 -0
- data/gemfiles/rails_6.1.gemfile +8 -0
- data/gemfiles/rails_7.0.gemfile +8 -0
- data/lib/devise-two-factor.rb +5 -1
- data/lib/devise_two_factor/models/two_factor_authenticatable.rb +35 -16
- data/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb +42 -13
- data/lib/devise_two_factor/spec_helpers.rb +6 -0
- data/lib/devise_two_factor/version.rb +1 -1
- data/lib/generators/devise_two_factor/devise_two_factor_generator.rb +2 -7
- data/spec/devise/models/two_factor_authenticatable_spec.rb +7 -53
- data/spec/devise/models/two_factor_backupable_spec.rb +7 -2
- data/spec/spec_helper.rb +0 -2
- data.tar.gz.sig +0 -0
- metadata +49 -73
- metadata.gz.sig +0 -0
- data/.travis.yml +0 -46
- data/gemfiles/rails_6_0.gemfile +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0f81d936ba021c504827ebf9a6faa199f7a0a8f714fee2d9ce6d48acbde423b
|
4
|
+
data.tar.gz: 38bf04b9361f64618c84081c5ce5436f523f8476c625b91b92cfba8e56e2cd5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54b62797c0194f8a3dc04f4594db384bdf6421eaf35707b7a35a39dc3993348790f1544e24d9d013885a7569a4c2381f938037626c26bf054ca00fe02bc46026
|
7
|
+
data.tar.gz: 2c24d3d5e822151f323ba27efb915ad44a33be2a20b95b3decad88facf34c68a70bbf471b1d748bd2c2498a088b5ddc0fb333486d467eb9865dd3f6aa941694c
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
@@ -0,0 +1,36 @@
|
|
1
|
+
name: CI
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
branches:
|
5
|
+
- main
|
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.7', '3.0', '3.1', 'truffleruby-head']
|
16
|
+
rails: ['7.0']
|
17
|
+
|
18
|
+
name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}
|
19
|
+
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
|
20
|
+
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/rails_${{ matrix.rails }}.gemfile
|
21
|
+
steps:
|
22
|
+
- uses: actions/checkout@v2
|
23
|
+
- name: Set up Ruby
|
24
|
+
uses: ruby/setup-ruby@v1
|
25
|
+
with:
|
26
|
+
ruby-version: ${{ matrix.ruby }}
|
27
|
+
bundler-cache: true
|
28
|
+
- name: Print versions
|
29
|
+
continue-on-error: true
|
30
|
+
run: |
|
31
|
+
ruby --version
|
32
|
+
bundle --version
|
33
|
+
echo "RubyGems version `gem --version`"
|
34
|
+
bundle exec rails --version
|
35
|
+
- name: Run tests
|
36
|
+
run: bundle exec rake
|
data/Appraisals
CHANGED
@@ -1,29 +1,39 @@
|
|
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
20
|
|
21
|
-
appraise "rails-5
|
21
|
+
appraise "rails-5.2" do
|
22
22
|
gem 'railties', '~> 5.2'
|
23
23
|
gem 'activesupport', '~> 5.2'
|
24
24
|
end
|
25
25
|
|
26
|
-
appraise "rails-6
|
26
|
+
appraise "rails-6.0" do
|
27
27
|
gem 'railties', '~> 6.0'
|
28
28
|
gem 'activesupport', '~> 6.0'
|
29
29
|
end
|
30
|
+
|
31
|
+
appraise "rails-6.1" do
|
32
|
+
gem 'railties', '~> 6.1'
|
33
|
+
gem 'activesupport', '~> 6.1'
|
34
|
+
end
|
35
|
+
|
36
|
+
appraise "rails-7.0" do
|
37
|
+
gem 'railties', '~> 7.0'
|
38
|
+
gem 'activesupport', '~> 7.0'
|
39
|
+
end
|
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,19 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## 4.0.2
|
6
|
+
- Add Rails 7.0 support
|
7
|
+
- Renew signing certificate
|
8
|
+
- Use `after` option of TOTP#verify for additional timestamp verification
|
9
|
+
|
10
|
+
## 4.0.1
|
11
|
+
- Convert CI from Travis CI to Github Actions ([#198](https://github.com/tinfoil/devise-two-factor/pull/198))
|
12
|
+
- Fix ActiveSupport::Testing::TimeHelpers require in shared examples ([#191](https://github.com/tinfoil/devise-two-factor/pull/191))
|
13
|
+
- Accept whitespace in provided codes ([#195](https://github.com/tinfoil/devise-two-factor/pull/195))
|
14
|
+
- Add Truffleruby head to CI ([#200](https://github.com/tinfoil/devise-two-factor/pull/200))
|
15
|
+
|
5
16
|
## 4.0.0
|
17
|
+
- [breaking] Drop support for Ruby <= 2.2
|
6
18
|
- Update ROTP
|
7
19
|
- Add Rails 6.1 support
|
8
20
|
- Remove timecop dependency
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Devise-Two-Factor Authentication
|
2
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
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
|
|
@@ -19,79 +19,80 @@ An example Rails 4 application is provided in the `demo` directory. It showcases
|
|
19
19
|
For the demo app to work, create an encryption key and store it as an environment variable. One way to do this is to create a file named `local_env.yml` in the application root. Set the value of `ENCRYPTION_KEY` in the YML file. That value will be loaded into the application environment by `application.rb`.
|
20
20
|
|
21
21
|
## Getting Started
|
22
|
-
Devise-Two-Factor doesn't require much to get started, but there are a few prerequisites before you can start using it in your application.
|
23
22
|
|
24
|
-
|
23
|
+
Devise-Two-Factor doesn't require much to get started, but there are two prerequisites before you can start using it in your application:
|
25
24
|
|
26
|
-
|
25
|
+
1. A Rails application with [devise](https://github.com/heartcombo/devise) installed
|
26
|
+
1. Secrets configured for ActiveRecord encrypted attributes
|
27
27
|
|
28
|
-
|
29
|
-
gem 'devise-two-factor'
|
30
|
-
```
|
28
|
+
First, you'll need a Rails application setup with Devise. Visit the Devise [homepage](https://github.com/plataformatec/devise) for instructions.
|
31
29
|
|
32
|
-
|
30
|
+
Devise-Two-Factor uses [ActiveRecord encrypted attributes](https://edgeguides.rubyonrails.org/active_record_encryption.html) which in turn uses Rails' encrypted credentials. [The Rails encrypted attributes guide](https://edgeguides.rubyonrails.org/active_record_encryption.html) has full details of how to set these up but briefly:
|
33
31
|
|
34
|
-
```
|
35
|
-
|
36
|
-
|
32
|
+
```bash
|
33
|
+
# generate suitable encryption secrets to stdout
|
34
|
+
$ ./bin/rails db:encryption:init
|
37
35
|
|
36
|
+
# Add the output from the command above to your encrypted credentials file via
|
37
|
+
# Setting the EDITOR environment variable is optional, without it, your default editor will open
|
38
|
+
$ EDITOR=code ./bin/rails credentials:edit
|
38
39
|
```
|
39
40
|
|
40
|
-
|
41
|
+
Add Devise-Two-Factor to your Gemfile with:
|
41
42
|
|
42
43
|
```ruby
|
43
|
-
|
44
|
-
```
|
45
|
-
|
46
|
-
Where `MODEL` is the name of the model you wish to add two-factor functionality to (for example `user`), and `ENVIRONMENT_VARIABLE` is the name of the variable you're storing your encryption key in.
|
44
|
+
# Gemfile
|
47
45
|
|
48
|
-
|
49
|
-
|
50
|
-
* encrypted_otp_secret
|
51
|
-
* encrypted_otp_secret_iv
|
52
|
-
* encrypted_otp_secret_salt
|
53
|
-
* consumed_timestep
|
54
|
-
* otp_required_for_login
|
46
|
+
gem 'devise-two-factor'
|
47
|
+
```
|
55
48
|
|
56
|
-
|
49
|
+
There is a generator which automates most of the setup:
|
57
50
|
|
58
|
-
```
|
59
|
-
|
51
|
+
```bash
|
52
|
+
# MODEL is the name of the model you wish to configure devise_two_factor e.g. User or Admin
|
53
|
+
./bin/rails generate devise_two_factor MODEL
|
60
54
|
```
|
61
55
|
|
62
|
-
|
56
|
+
Where `MODEL` is the name of the model you wish to add two-factor functionality to (for example `user`)
|
63
57
|
|
64
|
-
|
58
|
+
This generator will:
|
65
59
|
|
66
|
-
|
60
|
+
1. Create a new migration which adds a few columns to the specified model:
|
61
|
+
```ruby
|
62
|
+
add_column :users, :otp_secret, :string
|
63
|
+
add_column :users, :consumed_timestep, :integer
|
64
|
+
add_column :users, :otp_required_for_login, :boolean
|
65
|
+
```
|
66
|
+
1. Edit `app/models/MODEL.rb` (where MODEL is your model name):
|
67
|
+
* add the `:two_factor_authenticatable` devise module
|
68
|
+
* remove the `:database_authenticatable` if present because it is incompatible with `:two_factor_authenticatable`
|
69
|
+
1. Add a Warden config block to your Devise initializer, which enables the strategies required for two-factor authentication.
|
67
70
|
|
68
|
-
|
69
|
-
before_action :configure_permitted_parameters, if: :devise_controller?
|
70
|
-
|
71
|
-
...
|
71
|
+
Remember to apply the new migration after you run the generator:
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
def configure_permitted_parameters
|
76
|
-
devise_parameter_sanitizer.for(:sign_in) << :otp_attempt
|
77
|
-
end
|
73
|
+
```bash
|
74
|
+
./bin/rails db:migrate
|
78
75
|
```
|
79
76
|
|
80
|
-
|
77
|
+
Next you need to whitelist `:otp_attempt` as a permitted parameter in Devise `:sign_in` controller. You can do this by adding the following to your `application_controller.rb`:
|
81
78
|
|
82
79
|
```ruby
|
83
|
-
|
80
|
+
# app/controllers/application_controller.rb
|
84
81
|
|
85
|
-
|
82
|
+
before_action :configure_permitted_parameters, if: :devise_controller?
|
86
83
|
|
87
|
-
|
84
|
+
# ...
|
88
85
|
|
89
|
-
|
90
|
-
|
91
|
-
|
86
|
+
protected
|
87
|
+
|
88
|
+
def configure_permitted_parameters
|
89
|
+
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
90
|
+
end
|
92
91
|
```
|
93
92
|
|
94
|
-
|
93
|
+
Finally you should 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.
|
94
|
+
|
95
|
+
**Loading both `:database_authenticatable` and `:two_factor_authenticatable` in a model is a security issue** It will allow users to bypass two-factor authenticatable due to the way Warden handles cascading strategies!
|
95
96
|
|
96
97
|
## Designing Your Workflow
|
97
98
|
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.
|
@@ -239,3 +240,20 @@ require 'devise_two_factor/spec_helpers'
|
|
239
240
|
it_behaves_like "two_factor_authenticatable"
|
240
241
|
it_behaves_like "two_factor_backupable"
|
241
242
|
```
|
243
|
+
|
244
|
+
## Troubleshooting
|
245
|
+
If you are using Rails 4.x and Ruby >= 2.7, you may get an error like
|
246
|
+
|
247
|
+
```
|
248
|
+
An error occurred while loading ./spec/devise/models/two_factor_authenticatable_spec.rb.
|
249
|
+
Failure/Error: require 'devise'
|
250
|
+
|
251
|
+
NoMethodError:
|
252
|
+
undefined method `new' for BigDecimal:Class
|
253
|
+
```
|
254
|
+
see https://github.com/ruby/bigdecimal#which-version-should-you-select and https://github.com/ruby/bigdecimal/issues/127
|
255
|
+
for more details, but you should be able to solve this
|
256
|
+
by explicitly requiring an older version of bigdecimal in your gemfile like
|
257
|
+
```
|
258
|
+
gem "bigdecimal", "~> 1.4"
|
259
|
+
```
|
data/UPGRADING.md
CHANGED
@@ -1,3 +1,196 @@
|
|
1
|
+
# Upgrading from 4.x to 5.x
|
2
|
+
|
3
|
+
## Background
|
4
|
+
|
5
|
+
### Database columns in version 4.x and older
|
6
|
+
|
7
|
+
Versions 4.x and older stored the OTP secret in an attribute called `encrypted_otp_secret` using the [attr_encrypted](https://github.com/attr-encrypted/attr_encrypted) gem. This gem is currently unmaintained which is part of the motivation for moving to Rails encrypted attributes. This attribute was backed by three database columns:
|
8
|
+
|
9
|
+
```
|
10
|
+
encrypted_otp_secret
|
11
|
+
encrypted_otp_secret_iv
|
12
|
+
encrypted_otp_secret_salt
|
13
|
+
```
|
14
|
+
|
15
|
+
Two other columns were also created:
|
16
|
+
|
17
|
+
```
|
18
|
+
consumed_timestep
|
19
|
+
otp_required_for_login
|
20
|
+
```
|
21
|
+
|
22
|
+
A fresh install of 4.x would create all five of the database columns above.
|
23
|
+
|
24
|
+
### Database columns in version 5.x and later
|
25
|
+
|
26
|
+
Versions 5+ of this gem uses a single [Rails 7+ encrypted attribute](https://edgeguides.rubyonrails.org/active_record_encryption.html) named `otp_secret`to store the OTP secret in the database table (usually `users` but will be whatever model you picked).
|
27
|
+
|
28
|
+
A fresh install of 5+ will add the following columns to your `users` table:
|
29
|
+
|
30
|
+
```bash
|
31
|
+
otp_secret # this replaces encrypted_otp_secret, encrypted_otp_secret_iv, encrypted_otp_secret_salt
|
32
|
+
consumed_timestep
|
33
|
+
otp_required_for_login
|
34
|
+
```
|
35
|
+
|
36
|
+
### Upgrading from 4.x to 5.x
|
37
|
+
|
38
|
+
|
39
|
+
We have attempted to make the upgrade as painless as possible but unfortunately because of the secret storage change, it cannot be as simple as `bundle update devise-two-factor` :heart:
|
40
|
+
|
41
|
+
#### Assumptions
|
42
|
+
|
43
|
+
This guide assumes you are upgrading an existing Rails 6 app (with `devise` and `devise-two-factor`) to Rails 7.
|
44
|
+
|
45
|
+
This gem must be upgraded **as part of a Rails 7 upgrade**. See [the official Rails upgrading guide](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html) for an overview of upgrading Rails.
|
46
|
+
|
47
|
+
#### Phase 1: Upgrading devise-two-factor as part of Rails 7 upgrade
|
48
|
+
|
49
|
+
1. Update the version constraint for Rails in your `Gemfile` to your desired version e.g. `gem "rails", "~> 7.0.3"`
|
50
|
+
1. Run `bundle install` and resolve any issues with dependencies.
|
51
|
+
1. Update the version constraint for `devise-two-factor` in your `Gemfile` to the the latest version (must be at least 5.x e.g. `~> 5.0`
|
52
|
+
1. Run `./bin/rails app:update` as per the [Rails upgrade guide](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html) and tweak the output as required for your app.
|
53
|
+
1. Run `./bin/rails db:migrate` to update your DB based on the changes made by `app:update`
|
54
|
+
1. Add a new `otp_secret` attribute to your user model
|
55
|
+
```bash
|
56
|
+
# TODO: replace 'User' in the migration name with the name of your user model
|
57
|
+
./bin/rails g migration AddOtpSecretToUser otp_secret:string
|
58
|
+
./bin/rails db:migrate
|
59
|
+
```
|
60
|
+
1. Add a `legacy_otp_secret` method to your user model e.g. `User`.
|
61
|
+
* This method is used by the gem to find and decode the OTP secret from the legacy database columns.
|
62
|
+
* The implementation shown below works if you set up devise-two-factor with the settings suggested in the [README](./README.md).
|
63
|
+
* If you have customised the encryption scheme used to store the OTP secret then you will need to update this method to match.
|
64
|
+
* If you are unsure, you should try the method below as is, and if you can still sign in users with OTP enabled then all is well.
|
65
|
+
```ruby
|
66
|
+
class User
|
67
|
+
# ...
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
##
|
72
|
+
# Decrypt and return the `encrypted_otp_secret` attribute which was used in
|
73
|
+
# prior versions of devise-two-factor
|
74
|
+
# @return [String] The decrypted OTP secret
|
75
|
+
def legacy_otp_secret
|
76
|
+
return nil unless self[:encrypted_otp_secret]
|
77
|
+
return nil unless self.class.otp_secret_encryption_key
|
78
|
+
|
79
|
+
hmac_iterations = 2000 # a default set by the Encryptor gem
|
80
|
+
key = self.class.otp_secret_encryption_key
|
81
|
+
salt = Base64.decode64(encrypted_otp_secret_salt)
|
82
|
+
iv = Base64.decode64(encrypted_otp_secret_iv)
|
83
|
+
|
84
|
+
raw_cipher_text = Base64.decode64(encrypted_otp_secret)
|
85
|
+
# The last 16 bytes of the ciphertext are the authentication tag - we use
|
86
|
+
# Galois Counter Mode which is an authenticated encryption mode
|
87
|
+
cipher_text = raw_cipher_text[0..-17]
|
88
|
+
auth_tag = raw_cipher_text[-16..-1]
|
89
|
+
|
90
|
+
# this alrorithm lifted from
|
91
|
+
# https://github.com/attr-encrypted/encryptor/blob/master/lib/encryptor.rb#L54
|
92
|
+
|
93
|
+
# create an OpenSSL object which will decrypt the AES cipher with 256 bit
|
94
|
+
# keys in Galois Counter Mode (GCM). See
|
95
|
+
# https://ruby.github.io/openssl/OpenSSL/Cipher.html
|
96
|
+
cipher = OpenSSL::Cipher.new('aes-256-gcm')
|
97
|
+
|
98
|
+
# tell the cipher we want to decrypt. Symmetric algorithms use a very
|
99
|
+
# similar process for encryption and decryption, hence the same object can
|
100
|
+
# do both.
|
101
|
+
cipher.decrypt
|
102
|
+
|
103
|
+
# Use a Password-Based Key Derivation Function to generate the key actually
|
104
|
+
# used for encryptoin from the key we got as input.
|
105
|
+
cipher.key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(key, salt, hmac_iterations, cipher.key_len)
|
106
|
+
|
107
|
+
# set the Initialization Vector (IV)
|
108
|
+
cipher.iv = iv
|
109
|
+
|
110
|
+
# The tag must be set after calling Cipher#decrypt, Cipher#key= and
|
111
|
+
# Cipher#iv=, but before calling Cipher#final. After all decryption is
|
112
|
+
# performed, the tag is verified automatically in the call to Cipher#final.
|
113
|
+
#
|
114
|
+
# If the auth_tag does not verify, then #final will raise OpenSSL::Cipher::CipherError
|
115
|
+
cipher.auth_tag = auth_tag
|
116
|
+
|
117
|
+
# auth_data must be set after auth_tag has been set when decrypting See
|
118
|
+
# http://ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL/Cipher.html#method-i-auth_data-3D
|
119
|
+
# we are not adding any authenticated data but OpenSSL docs say this should
|
120
|
+
# still be called.
|
121
|
+
cipher.auth_data = ''
|
122
|
+
|
123
|
+
# #update is (somewhat confusingly named) the method which actually
|
124
|
+
# performs the decryption on the given chunk of data. Our OTP secret is
|
125
|
+
# short so we only need to call it once.
|
126
|
+
#
|
127
|
+
# It is very important that we call #final because:
|
128
|
+
#
|
129
|
+
# 1. The authentication tag is checked during the call to #final
|
130
|
+
# 2. Block based cipher modes (e.g. CBC) work on fixed size chunks. We need
|
131
|
+
# to call #final to get it to process the last chunk properly. The output
|
132
|
+
# of #final should be appended to the decrypted value. This isn't
|
133
|
+
# required for streaming cipher modes but including it is a best practice
|
134
|
+
# so that your code will continue to function correctly even if you later
|
135
|
+
# change to a block cipher mode.
|
136
|
+
cipher.update(cipher_text) + cipher.final
|
137
|
+
end
|
138
|
+
end
|
139
|
+
```
|
140
|
+
2. Set up [Rails encrypted secrets](https://edgeguides.rubyonrails.org/active_record_encryption.html)
|
141
|
+
```bash
|
142
|
+
./bin/rails db:encryption:init
|
143
|
+
# capture the output and put in encrypted credentials via
|
144
|
+
./bin/rails credentials:edit
|
145
|
+
```
|
146
|
+
3. Complete your Rails 7 upgrade (making whatever other changes are required)
|
147
|
+
|
148
|
+
You can now deploy your upgraded application and devise-two-factor should work as before.
|
149
|
+
|
150
|
+
This gem will fall back to **reading** the OTP secret from the legacy columns if it cannot find one in the new `otp_secret` column. When you **write** a new OTP secret it will always be written to the new `otp_secret` column.
|
151
|
+
|
152
|
+
#### Phase 2: Clean up
|
153
|
+
|
154
|
+
This "clean up" phase can happen at the same time as your initial deployment but teams managing existing apps will likely want to do clean-up as separate, later deployments.
|
155
|
+
|
156
|
+
1. Create a rake task to copy the OTP secret for each user from the legacy column to the new `otp_secret` column. This prepares the way for us to remove the legacy columns in a later step.
|
157
|
+
```ruby
|
158
|
+
# lib/tasks/devise_two_factor_migration.rake
|
159
|
+
|
160
|
+
# Use this as a starting point for your task to migrate your user's OTP secrets.
|
161
|
+
namespace :devise_two_factor do
|
162
|
+
desc "Copy devise_two_factor OTP secret from old format to new format"
|
163
|
+
task copy_otp_secret_to_rails7_encrypted_attr: [:environment] do
|
164
|
+
# TODO: change User to your user model
|
165
|
+
User.find_each do |user| # find_each finds in batches of 1,000 by default
|
166
|
+
otp_secret = user.otp_secret # read from otp_secret column, fall back to legacy columns if new column is empty
|
167
|
+
puts "Processing #{user.email}"
|
168
|
+
user.update!(otp_secret: otp_secret)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
```
|
173
|
+
1. Remove the `#legacy_otp_secret` method from your user model (e.g. `User`) because it is no longer required.
|
174
|
+
1. Remove the now unused legacy columns from the database. This assumes you have run a rake task as in the previous step to migrate all the legacy stored secrets to the new storage.
|
175
|
+
```bash
|
176
|
+
# TODO: replace 'Users' in migration name with the name of your user model
|
177
|
+
./bin/rails g migration RemoveLegacyDeviseTwoFactorSecretsFromUsers
|
178
|
+
```
|
179
|
+
which generates
|
180
|
+
```ruby
|
181
|
+
class RemoveLegacyDeviseTwoFactorSecretsFromUsers < ActiveRecord::Migration[7.0]
|
182
|
+
def change
|
183
|
+
# TODO: change :users to whatever your users table is
|
184
|
+
|
185
|
+
# WARNING: Only run this when you are confident you have copied the OTP
|
186
|
+
# secret for ALL users from `encrypted_otp_secret` to `otp_secret`!
|
187
|
+
remove_column :users, :encrypted_otp_secret
|
188
|
+
remove_column :users, :encrypted_otp_secret_iv
|
189
|
+
remove_column :users, :encrypted_otp_secret_salt
|
190
|
+
end
|
191
|
+
end
|
192
|
+
```
|
193
|
+
|
1
194
|
# Guide to upgrading from 2.x to 3.x
|
2
195
|
|
3
196
|
Pull request #76 allows for compatibility with `attr_encrypted` 3.0, which should be used due to a security vulnerability discovered in 2.0.
|
data/certs/tinfoil-cacert.pem
CHANGED
@@ -3,7 +3,7 @@ MIIHSjCCBTKgAwIBAgIJAK2u0LojMCNgMA0GCSqGSIb3DQEBBQUAMIGcMQswCQYD
|
|
3
3
|
VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEfMB0GA1UE
|
4
4
|
ChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEfMB0GA1UEAxMWVGluZm9pbCBTZWN1
|
5
5
|
cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYbc3VwcG9ydEB0aW5mb2lsc2VjdXJp
|
6
|
-
|
6
|
+
dHkuY29tMB4XDTIxMDkwOTE4MjIwMFoXDTI1MDkwOTE4MjIwMFowgZwxCzAJBgNV
|
7
7
|
BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
|
8
8
|
ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
|
9
9
|
aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
|
@@ -27,15 +27,15 @@ c3VwcG9ydEB0aW5mb2lsc2VjdXJpdHkuY29tggkAra7QuiMwI2AwDwYDVR0TAQH/
|
|
27
27
|
BAUwAwEB/zARBglghkgBhvhCAQEEBAMCAQYwCQYDVR0SBAIwADArBglghkgBhvhC
|
28
28
|
AQ0EHhYcVGlueUNBIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAmBgNVHREEHzAdgRtz
|
29
29
|
dXBwb3J0QHRpbmZvaWxzZWN1cml0eS5jb20wDgYDVR0PAQH/BAQDAgEGMA0GCSqG
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
30
|
+
SIb3DQEBBQUAA4ICAQBZy4JJSmwLuO0nZbdr4tJeVS2P8bcGi6PzAcdzVfwzjp6n
|
31
|
+
5qf8m4O8my4lnJieom0GrWSHQoPY1Yur4hEoZbugKO9DWZL3dTiGcrgw0TbQ6Gtq
|
32
|
+
TTPatW3LA21qFJwvohSvLqPdmZuM+H9g49sdl2kNTDVI6iUyMYuNpL14aPKPGBFo
|
33
|
+
o7UjciT1h7JtJl9b/fXrbPeRHBwpZXWeipiPGv/OZW5KnOsNlUkTquS7Zj4ETkIC
|
34
|
+
6mVtmsLvq+YwFthFyMU37pXwYxcmqRmH6lX+XC6AVW5oO4GBmG+Zr/Z+h5Cih5ca
|
35
|
+
/mX88RkO+dGTjw1IdxKmxOqKL62OBATKrTDJ/scsmRptynA4TunYW+7ikOpDbPfL
|
36
|
+
l18aleLISlcgWJg/Czf2nmBqAClPLnhV8qxWsvt58MQQ/Jpoggvpl8EG1PylWiBS
|
37
|
+
Kc/4Ad/FKQFpTzXUgDg2kV07npVjYbBzA5p4ZSWSlflFu93jb9gg2+qtnRSImVCZ
|
38
|
+
nQjZdsv8hebElPAIbtJjSnoH1Kz2ucYLakdF1UMKnpp1PVREtuKPz/foU9KUHs0z
|
39
|
+
dWRALx8cWG4uKK9AIEUlVdGKfX0Wj0qFK0KGxl3f3jObud5Agwue2EPKWwUzEGUh
|
40
|
+
Iqp60gNw3vBdKHw4dh1bfcbXWnRDL+OQPuOFZeMWgu1QmeHeuggYtYtRg7V5Kg==
|
41
41
|
-----END CERTIFICATE-----
|
@@ -1,9 +1,9 @@
|
|
1
1
|
-----BEGIN CERTIFICATE-----
|
2
|
-
MIIGADCCA+
|
2
|
+
MIIGADCCA+igAwIBAgIIHIF9ta6cW3YwDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV
|
3
3
|
BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
|
4
4
|
ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
|
5
5
|
aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
|
6
|
-
|
6
|
+
eS5jb20wHhcNMjIwMzIyMjI1MzAwWhcNMjUwOTA5MTgyMjAwWjCBiDELMAkGA1UE
|
7
7
|
BhMCVVMxCzAJBgNVBAgTAkNBMR8wHQYDVQQKExZUaW5mb2lsIFNlY3VyaXR5LCBJ
|
8
8
|
bmMuMR0wGwYDVQQDExR0aW5mb2lsc2VjdXJpdHktZ2VtczEsMCoGCSqGSIb3DQEJ
|
9
9
|
ARYdZW5naW5lZXJzQHRpbmZvaWxzZWN1cml0eS5jb20wggIiMA0GCSqGSIb3DQEB
|
@@ -20,16 +20,16 @@ WgpUq+q23PFkt1gIBi/4tGvzsLZye25QU2Y+XLzldCNm+DyRFXZ+Q+bK33IveUeU
|
|
20
20
|
WEOv4T1qTXHAOypyzmgodVRG/PrlsSMOBfE515kG1mDMGjRcCpEtlskgxUbf7qM7
|
21
21
|
hQIDAQABo1gwVjAJBgNVHRMEAjAAMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHBzOi8v
|
22
22
|
d3d3LnRpbmZvaWxzZWN1cml0eS5jb20vc2VjdXJpdHkvcmV2b2NhdGlvbl9saXN0
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
23
|
+
MA0GCSqGSIb3DQEBDQUAA4ICAQAiYF/m2ny/mxFvBVxHfdYuzybhCvsEUd+TSnoe
|
24
|
+
mqOWntY3sxCOaY0aGOMB4vyg9G+oP/kT4m63sD4uQxeuU7WOjaG2smCSS5q+PSWS
|
25
|
+
v63gILqPamjSyP/Om864EA6YlvVQ7nPXhVDEaiBt3iliefJGmb0wWSbbDCmq3aMb
|
26
|
+
WTLuax/IeY6MjJi20LutIcuz+VX8OxlA1hSpgAToMz3xrhA8fPt5UkKhkDkPFYBF
|
27
|
+
5htKVipyijChWsXyt33YM2qGaavTEXzxza1I99PGNRKxUMvbSMas4YaLqkBpQSc+
|
28
|
+
mcrLWYPiXWsePGu+j08AypE2Ubp4AOSZJN9rBBGotC3gofipo+K/sBiOM9xXI76Q
|
29
|
+
0HYOxXPa7D7UQQG1R9i0rcxmf9qepIVYCldmqVkKKDizcDo5UI9lRiLFjDyQhn6l
|
30
|
+
YFY9bPQ4lKTK5Jr3M6+dV7fHxLhqXyMGs1905IUb7qvB7Bq/f0qJfC0JZuY/qdn2
|
31
|
+
lL0SeFKOVsjErtobh3u8p8j2USkc8uJgIANHpXEMEExdp899CV/eVjh3TpAR7E6T
|
32
|
+
mg7Q9Hi6Hh8z+Le9iR4I49vPEWDQEvj35IT6VfwU79UfIOlX+DkW8fFkPbaut3Se
|
33
|
+
vqIDv6JBG9I16h/HhchntKfM58MI1bNZFBSdZqYOJiL8JIjP8HNIk76Y366ppG29
|
34
|
+
EhBYYg==
|
35
35
|
-----END CERTIFICATE-----
|
data/devise-two-factor.gemspec
CHANGED
@@ -21,9 +21,8 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.test_files = `git ls-files -- spec/*`.split("\n")
|
22
22
|
s.require_paths = ['lib']
|
23
23
|
|
24
|
-
s.add_runtime_dependency 'railties', '
|
25
|
-
s.add_runtime_dependency 'activesupport', '
|
26
|
-
s.add_runtime_dependency 'attr_encrypted', '>= 1.3', '< 4', '!= 2'
|
24
|
+
s.add_runtime_dependency 'railties', '~> 7.0'
|
25
|
+
s.add_runtime_dependency 'activesupport', '~> 7.0'
|
27
26
|
s.add_runtime_dependency 'devise', '~> 4.0'
|
28
27
|
s.add_runtime_dependency 'rotp', '~> 6.0'
|
29
28
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
data/lib/devise-two-factor.rb
CHANGED
@@ -12,10 +12,14 @@ module Devise
|
|
12
12
|
mattr_accessor :otp_allowed_drift
|
13
13
|
@@otp_allowed_drift = 30
|
14
14
|
|
15
|
-
# The key used to encrypt OTP secrets in the database
|
15
|
+
# The key used to encrypt OTP secrets in the database in legacy installs.
|
16
16
|
mattr_accessor :otp_secret_encryption_key
|
17
17
|
@@otp_secret_encryption_key = nil
|
18
18
|
|
19
|
+
# These options are passed to the Rails 7+ encrypted attribute
|
20
|
+
mattr_accessor :otp_encrypted_attribute_options
|
21
|
+
@@otp_encrypted_attribute_options = {}
|
22
|
+
|
19
23
|
# The length of all generated OTP backup codes
|
20
24
|
mattr_accessor :otp_backup_code_length
|
21
25
|
@@otp_backup_code_length = 16
|
@@ -7,25 +7,28 @@ module Devise
|
|
7
7
|
include Devise::Models::DatabaseAuthenticatable
|
8
8
|
|
9
9
|
included do
|
10
|
-
|
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
|
22
|
-
end
|
23
|
-
|
10
|
+
encrypts :otp_secret, **splattable_encrypted_attr_options
|
24
11
|
attr_accessor :otp_attempt
|
25
12
|
end
|
26
13
|
|
14
|
+
def otp_secret
|
15
|
+
# return the OTP secret stored as a Rails encrypted attribute if it
|
16
|
+
# exists. Otherwise return OTP secret stored by the `attr_encrypted` gem
|
17
|
+
return self[:otp_secret] if self[:otp_secret]
|
18
|
+
|
19
|
+
legacy_otp_secret
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Decrypt and return the `encrypted_otp_secret` attribute which was used in
|
24
|
+
# prior versions of devise-two-factor
|
25
|
+
# See: # https://github.com/tinfoil/devise-two-factor/blob/main/UPGRADING.md
|
26
|
+
def legacy_otp_secret
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
27
30
|
def self.required_fields(klass)
|
28
|
-
[:
|
31
|
+
[:otp_secret, :consumed_timestep]
|
29
32
|
end
|
30
33
|
|
31
34
|
# This defaults to the model's otp_secret
|
@@ -35,7 +38,13 @@ module Devise
|
|
35
38
|
return false unless code.present? && otp_secret.present?
|
36
39
|
|
37
40
|
totp = otp(otp_secret)
|
38
|
-
|
41
|
+
|
42
|
+
if self.consumed_timestep
|
43
|
+
# reconstruct the timestamp of the last consumed timestep
|
44
|
+
after_timestamp = self.consumed_timestep * otp.interval
|
45
|
+
end
|
46
|
+
|
47
|
+
if totp.verify(code.gsub(/\s+/, ""), drift_behind: self.class.otp_allowed_drift, drift_ahead: self.class.otp_allowed_drift, after: after_timestamp)
|
39
48
|
return consume_otp!
|
40
49
|
end
|
41
50
|
|
@@ -81,11 +90,21 @@ module Devise
|
|
81
90
|
module ClassMethods
|
82
91
|
Devise::Models.config(self, :otp_secret_length,
|
83
92
|
:otp_allowed_drift,
|
93
|
+
:otp_encrypted_attribute_options,
|
84
94
|
:otp_secret_encryption_key)
|
85
95
|
|
86
96
|
def generate_otp_secret(otp_secret_length = self.otp_secret_length)
|
87
97
|
ROTP::Base32.random_base32(otp_secret_length)
|
88
98
|
end
|
99
|
+
|
100
|
+
# Return value will be splatted with ** so return a version of the
|
101
|
+
# encrypted attribute options which is always a Hash.
|
102
|
+
# @return [Hash]
|
103
|
+
def splattable_encrypted_attr_options
|
104
|
+
return {} if otp_encrypted_attribute_options.nil?
|
105
|
+
|
106
|
+
otp_encrypted_attribute_options
|
107
|
+
end
|
89
108
|
end
|
90
109
|
end
|
91
110
|
end
|
@@ -8,7 +8,7 @@ RSpec.shared_examples 'two_factor_authenticatable' do
|
|
8
8
|
|
9
9
|
describe 'required_fields' do
|
10
10
|
it 'should have the attr_encrypted fields for otp_secret' do
|
11
|
-
expect(Devise::Models::TwoFactorAuthenticatable.required_fields(subject.class)).to contain_exactly(:
|
11
|
+
expect(Devise::Models::TwoFactorAuthenticatable.required_fields(subject.class)).to contain_exactly(:otp_secret, :consumed_timestep)
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
@@ -16,18 +16,6 @@ RSpec.shared_examples 'two_factor_authenticatable' do
|
|
16
16
|
it 'should be of the configured length' do
|
17
17
|
expect(subject.otp_secret.length).to eq(subject.class.otp_secret_length)
|
18
18
|
end
|
19
|
-
|
20
|
-
it 'stores the encrypted otp_secret' do
|
21
|
-
expect(subject.encrypted_otp_secret).to_not be_nil
|
22
|
-
end
|
23
|
-
|
24
|
-
it 'stores an iv for otp_secret' do
|
25
|
-
expect(subject.encrypted_otp_secret_iv).to_not be_nil
|
26
|
-
end
|
27
|
-
|
28
|
-
it 'stores a salt for otp_secret' do
|
29
|
-
expect(subject.encrypted_otp_secret_salt).to_not be_nil
|
30
|
-
end
|
31
19
|
end
|
32
20
|
|
33
21
|
describe '#validate_and_consume_otp!' do
|
@@ -66,6 +54,42 @@ RSpec.shared_examples 'two_factor_authenticatable' do
|
|
66
54
|
expect(subject.validate_and_consume_otp!(consumed_otp)).to be false
|
67
55
|
end
|
68
56
|
end
|
57
|
+
|
58
|
+
context 'given a valid OTP used multiple times within the allowed drift' do
|
59
|
+
let(:consumed_otp) { ROTP::TOTP.new(otp_secret).at(Time.now) }
|
60
|
+
|
61
|
+
before do
|
62
|
+
subject.validate_and_consume_otp!(consumed_otp)
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'after the otp interval' do
|
66
|
+
before do
|
67
|
+
travel_to(subject.otp.interval.seconds.from_now)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'fails to validate' do
|
71
|
+
expect(subject.validate_and_consume_otp!(consumed_otp)).to be false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'given a valid OTP used multiple times within the allowed drift after a subsequent login' do
|
77
|
+
let(:consumed_otp) { ROTP::TOTP.new(otp_secret).at(Time.now - subject.class.otp_allowed_drift) }
|
78
|
+
|
79
|
+
before do
|
80
|
+
travel_to(subject.class.otp_allowed_drift.seconds.ago)
|
81
|
+
subject.validate_and_consume_otp!(consumed_otp)
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'after the otp interval' do
|
85
|
+
it 'fails to validate' do
|
86
|
+
travel_to(subject.class.otp_allowed_drift.seconds.from_now)
|
87
|
+
next_otp = ROTP::TOTP.new(otp_secret).at(Time.now)
|
88
|
+
expect(subject.validate_and_consume_otp!(next_otp)).to be true
|
89
|
+
expect(subject.validate_and_consume_otp!(consumed_otp)).to be false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
69
93
|
end
|
70
94
|
|
71
95
|
it 'validates a precisely correct OTP' do
|
@@ -73,6 +97,11 @@ RSpec.shared_examples 'two_factor_authenticatable' do
|
|
73
97
|
expect(subject.validate_and_consume_otp!(otp)).to be true
|
74
98
|
end
|
75
99
|
|
100
|
+
it 'validates a precisely correct OTP with whitespace' do
|
101
|
+
otp = ROTP::TOTP.new(otp_secret).at(Time.now)
|
102
|
+
expect(subject.validate_and_consume_otp!(otp.split("").join(" "))).to be true
|
103
|
+
end
|
104
|
+
|
76
105
|
it 'fails a nil OTP value' do
|
77
106
|
otp = nil
|
78
107
|
expect(subject.validate_and_consume_otp!(otp)).to be false
|
@@ -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
|
@@ -3,8 +3,6 @@ require 'rails/generators'
|
|
3
3
|
module DeviseTwoFactor
|
4
4
|
module Generators
|
5
5
|
class DeviseTwoFactorGenerator < Rails::Generators::NamedBase
|
6
|
-
argument :encryption_key_env, :type => :string, :required => true
|
7
|
-
|
8
6
|
desc 'Creates a migration to add the required attributes to NAME, and ' \
|
9
7
|
'adds the necessary Devise directives to the model'
|
10
8
|
|
@@ -19,9 +17,7 @@ module DeviseTwoFactor
|
|
19
17
|
def create_devise_two_factor_migration
|
20
18
|
migration_arguments = [
|
21
19
|
"add_devise_two_factor_to_#{plural_name}",
|
22
|
-
"
|
23
|
-
"encrypted_otp_secret_iv:string",
|
24
|
-
"encrypted_otp_secret_salt:string",
|
20
|
+
"otp_secret:string",
|
25
21
|
"consumed_timestep:integer",
|
26
22
|
"otp_required_for_login:boolean"
|
27
23
|
]
|
@@ -51,8 +47,7 @@ module DeviseTwoFactor
|
|
51
47
|
indent_depth = class_path.size
|
52
48
|
|
53
49
|
content = [
|
54
|
-
"devise :two_factor_authenticatable
|
55
|
-
" :otp_secret_encryption_key => ENV['#{encryption_key_env}']\n"
|
50
|
+
"devise :two_factor_authenticatable"
|
56
51
|
]
|
57
52
|
|
58
53
|
content << "attr_accessible :otp_attempt\n" if needs_attr_accessible?
|
@@ -6,34 +6,15 @@ class TwoFactorAuthenticatableDouble
|
|
6
6
|
include ::ActiveModel::Validations::Callbacks
|
7
7
|
extend ::Devise::Models
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
attr_accessor :consumed_timestep
|
14
|
-
|
15
|
-
def save(validate)
|
16
|
-
# noop for testing
|
17
|
-
true
|
9
|
+
# stub out the ::ActiveRecord::Encryption::EncryptableRecord API
|
10
|
+
attr_accessor :otp_secret
|
11
|
+
def self.encrypts(*attrs)
|
12
|
+
nil
|
18
13
|
end
|
19
|
-
end
|
20
|
-
|
21
|
-
class TwoFactorAuthenticatableWithCustomizeAttrEncryptedDouble
|
22
|
-
extend ::ActiveModel::Callbacks
|
23
|
-
include ::ActiveModel::Validations::Callbacks
|
24
|
-
|
25
|
-
# like https://github.com/tinfoil/devise-two-factor/blob/cf73e52043fbe45b74d68d02bc859522ad22fe73/UPGRADING.md#guide-to-upgrading-from-2x-to-3x
|
26
|
-
extend ::AttrEncrypted
|
27
|
-
attr_encrypted :otp_secret,
|
28
|
-
:key => 'test-key'*8,
|
29
|
-
:mode => :per_attribute_iv_and_salt,
|
30
|
-
:algorithm => 'aes-256-cbc'
|
31
|
-
|
32
|
-
extend ::Devise::Models
|
33
14
|
|
34
15
|
define_model_callbacks :update
|
35
16
|
|
36
|
-
devise :two_factor_authenticatable
|
17
|
+
devise :two_factor_authenticatable
|
37
18
|
|
38
19
|
attr_accessor :consumed_timestep
|
39
20
|
|
@@ -51,33 +32,6 @@ describe ::Devise::Models::TwoFactorAuthenticatable do
|
|
51
32
|
end
|
52
33
|
end
|
53
34
|
|
54
|
-
describe ::Devise::Models::TwoFactorAuthenticatable do
|
55
|
-
context 'When included in a class' do
|
56
|
-
subject { TwoFactorAuthenticatableWithCustomizeAttrEncryptedDouble.new }
|
57
|
-
|
58
|
-
it_behaves_like 'two_factor_authenticatable'
|
59
|
-
|
60
|
-
before :each do
|
61
|
-
subject.otp_secret = subject.class.generate_otp_secret
|
62
|
-
subject.consumed_timestep = nil
|
63
|
-
end
|
64
|
-
|
65
|
-
describe 'otp_secret options' do
|
66
|
-
it 'should be of the key' do
|
67
|
-
expect(subject.encrypted_attributes[:otp_secret][:key]).to eq('test-key'*8)
|
68
|
-
end
|
69
|
-
|
70
|
-
it 'should be of the mode' do
|
71
|
-
expect(subject.encrypted_attributes[:otp_secret][:mode]).to eq(:per_attribute_iv_and_salt)
|
72
|
-
end
|
73
|
-
|
74
|
-
it 'should be of the mode' do
|
75
|
-
expect(subject.encrypted_attributes[:otp_secret][:algorithm]).to eq('aes-256-cbc')
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
35
|
describe ::Devise::Models::TwoFactorAuthenticatable do
|
82
36
|
context 'When clean_up_passwords is called ' do
|
83
37
|
subject { TwoFactorAuthenticatableDouble.new }
|
@@ -85,11 +39,11 @@ describe ::Devise::Models::TwoFactorAuthenticatable do
|
|
85
39
|
subject.otp_attempt = 'foo'
|
86
40
|
subject.password_confirmation = 'foo'
|
87
41
|
end
|
88
|
-
it 'otp_attempt should be nill' do
|
42
|
+
it 'otp_attempt should be nill' do
|
89
43
|
subject.clean_up_passwords
|
90
44
|
expect(subject.otp_attempt).to be_nil
|
91
45
|
end
|
92
|
-
it 'password_confirmation should be nill' do
|
46
|
+
it 'password_confirmation should be nill' do
|
93
47
|
subject.clean_up_passwords
|
94
48
|
expect(subject.password_confirmation).to be_nil
|
95
49
|
end
|
@@ -6,10 +6,15 @@ class TwoFactorBackupableDouble
|
|
6
6
|
include ::ActiveModel::Validations::Callbacks
|
7
7
|
extend ::Devise::Models
|
8
8
|
|
9
|
+
# stub out the ::ActiveRecord::Encryption::EncryptableRecord API
|
10
|
+
attr_accessor :otp_secret
|
11
|
+
def self.encrypts(*attrs)
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
9
15
|
define_model_callbacks :update
|
10
16
|
|
11
|
-
devise :two_factor_authenticatable, :two_factor_backupable
|
12
|
-
:otp_secret_encryption_key => 'test-key'*4
|
17
|
+
devise :two_factor_authenticatable, :two_factor_backupable
|
13
18
|
|
14
19
|
attr_accessor :otp_backup_codes
|
15
20
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -21,13 +21,11 @@ require 'rspec'
|
|
21
21
|
require 'faker'
|
22
22
|
require 'devise-two-factor'
|
23
23
|
require 'devise_two_factor/spec_helpers'
|
24
|
-
require 'active_support/testing/time_helpers'
|
25
24
|
|
26
25
|
# Requires supporting files with custom matchers and macros, etc,
|
27
26
|
# in ./support/ and its subdirectories.
|
28
27
|
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
29
28
|
|
30
29
|
RSpec.configure do |config|
|
31
|
-
config.include ActiveSupport::Testing::TimeHelpers
|
32
30
|
config.order = 'random'
|
33
31
|
end
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: devise-two-factor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 5.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shane Wilton
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain:
|
11
11
|
- |
|
@@ -14,7 +14,7 @@ cert_chain:
|
|
14
14
|
VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEfMB0GA1UE
|
15
15
|
ChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEfMB0GA1UEAxMWVGluZm9pbCBTZWN1
|
16
16
|
cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYbc3VwcG9ydEB0aW5mb2lsc2VjdXJp
|
17
|
-
|
17
|
+
dHkuY29tMB4XDTIxMDkwOTE4MjIwMFoXDTI1MDkwOTE4MjIwMFowgZwxCzAJBgNV
|
18
18
|
BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
|
19
19
|
ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
|
20
20
|
aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
|
@@ -38,25 +38,25 @@ cert_chain:
|
|
38
38
|
BAUwAwEB/zARBglghkgBhvhCAQEEBAMCAQYwCQYDVR0SBAIwADArBglghkgBhvhC
|
39
39
|
AQ0EHhYcVGlueUNBIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAmBgNVHREEHzAdgRtz
|
40
40
|
dXBwb3J0QHRpbmZvaWxzZWN1cml0eS5jb20wDgYDVR0PAQH/BAQDAgEGMA0GCSqG
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
41
|
+
SIb3DQEBBQUAA4ICAQBZy4JJSmwLuO0nZbdr4tJeVS2P8bcGi6PzAcdzVfwzjp6n
|
42
|
+
5qf8m4O8my4lnJieom0GrWSHQoPY1Yur4hEoZbugKO9DWZL3dTiGcrgw0TbQ6Gtq
|
43
|
+
TTPatW3LA21qFJwvohSvLqPdmZuM+H9g49sdl2kNTDVI6iUyMYuNpL14aPKPGBFo
|
44
|
+
o7UjciT1h7JtJl9b/fXrbPeRHBwpZXWeipiPGv/OZW5KnOsNlUkTquS7Zj4ETkIC
|
45
|
+
6mVtmsLvq+YwFthFyMU37pXwYxcmqRmH6lX+XC6AVW5oO4GBmG+Zr/Z+h5Cih5ca
|
46
|
+
/mX88RkO+dGTjw1IdxKmxOqKL62OBATKrTDJ/scsmRptynA4TunYW+7ikOpDbPfL
|
47
|
+
l18aleLISlcgWJg/Czf2nmBqAClPLnhV8qxWsvt58MQQ/Jpoggvpl8EG1PylWiBS
|
48
|
+
Kc/4Ad/FKQFpTzXUgDg2kV07npVjYbBzA5p4ZSWSlflFu93jb9gg2+qtnRSImVCZ
|
49
|
+
nQjZdsv8hebElPAIbtJjSnoH1Kz2ucYLakdF1UMKnpp1PVREtuKPz/foU9KUHs0z
|
50
|
+
dWRALx8cWG4uKK9AIEUlVdGKfX0Wj0qFK0KGxl3f3jObud5Agwue2EPKWwUzEGUh
|
51
|
+
Iqp60gNw3vBdKHw4dh1bfcbXWnRDL+OQPuOFZeMWgu1QmeHeuggYtYtRg7V5Kg==
|
52
52
|
-----END CERTIFICATE-----
|
53
53
|
- |
|
54
54
|
-----BEGIN CERTIFICATE-----
|
55
|
-
MIIGADCCA+
|
55
|
+
MIIGADCCA+igAwIBAgIIHIF9ta6cW3YwDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV
|
56
56
|
BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
|
57
57
|
ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
|
58
58
|
aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
|
59
|
-
|
59
|
+
eS5jb20wHhcNMjIwMzIyMjI1MzAwWhcNMjUwOTA5MTgyMjAwWjCBiDELMAkGA1UE
|
60
60
|
BhMCVVMxCzAJBgNVBAgTAkNBMR8wHQYDVQQKExZUaW5mb2lsIFNlY3VyaXR5LCBJ
|
61
61
|
bmMuMR0wGwYDVQQDExR0aW5mb2lsc2VjdXJpdHktZ2VtczEsMCoGCSqGSIb3DQEJ
|
62
62
|
ARYdZW5naW5lZXJzQHRpbmZvaWxzZWN1cml0eS5jb20wggIiMA0GCSqGSIb3DQEB
|
@@ -73,75 +73,49 @@ cert_chain:
|
|
73
73
|
WEOv4T1qTXHAOypyzmgodVRG/PrlsSMOBfE515kG1mDMGjRcCpEtlskgxUbf7qM7
|
74
74
|
hQIDAQABo1gwVjAJBgNVHRMEAjAAMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHBzOi8v
|
75
75
|
d3d3LnRpbmZvaWxzZWN1cml0eS5jb20vc2VjdXJpdHkvcmV2b2NhdGlvbl9saXN0
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
76
|
+
MA0GCSqGSIb3DQEBDQUAA4ICAQAiYF/m2ny/mxFvBVxHfdYuzybhCvsEUd+TSnoe
|
77
|
+
mqOWntY3sxCOaY0aGOMB4vyg9G+oP/kT4m63sD4uQxeuU7WOjaG2smCSS5q+PSWS
|
78
|
+
v63gILqPamjSyP/Om864EA6YlvVQ7nPXhVDEaiBt3iliefJGmb0wWSbbDCmq3aMb
|
79
|
+
WTLuax/IeY6MjJi20LutIcuz+VX8OxlA1hSpgAToMz3xrhA8fPt5UkKhkDkPFYBF
|
80
|
+
5htKVipyijChWsXyt33YM2qGaavTEXzxza1I99PGNRKxUMvbSMas4YaLqkBpQSc+
|
81
|
+
mcrLWYPiXWsePGu+j08AypE2Ubp4AOSZJN9rBBGotC3gofipo+K/sBiOM9xXI76Q
|
82
|
+
0HYOxXPa7D7UQQG1R9i0rcxmf9qepIVYCldmqVkKKDizcDo5UI9lRiLFjDyQhn6l
|
83
|
+
YFY9bPQ4lKTK5Jr3M6+dV7fHxLhqXyMGs1905IUb7qvB7Bq/f0qJfC0JZuY/qdn2
|
84
|
+
lL0SeFKOVsjErtobh3u8p8j2USkc8uJgIANHpXEMEExdp899CV/eVjh3TpAR7E6T
|
85
|
+
mg7Q9Hi6Hh8z+Le9iR4I49vPEWDQEvj35IT6VfwU79UfIOlX+DkW8fFkPbaut3Se
|
86
|
+
vqIDv6JBG9I16h/HhchntKfM58MI1bNZFBSdZqYOJiL8JIjP8HNIk76Y366ppG29
|
87
|
+
EhBYYg==
|
88
88
|
-----END CERTIFICATE-----
|
89
|
-
date:
|
89
|
+
date: 2022-07-11 00:00:00.000000000 Z
|
90
90
|
dependencies:
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: railties
|
93
93
|
requirement: !ruby/object:Gem::Requirement
|
94
94
|
requirements:
|
95
|
-
- - "
|
95
|
+
- - "~>"
|
96
96
|
- !ruby/object:Gem::Version
|
97
|
-
version: '
|
97
|
+
version: '7.0'
|
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: '7.0'
|
105
105
|
- !ruby/object:Gem::Dependency
|
106
106
|
name: activesupport
|
107
107
|
requirement: !ruby/object:Gem::Requirement
|
108
108
|
requirements:
|
109
|
-
- - "
|
110
|
-
- !ruby/object:Gem::Version
|
111
|
-
version: '6.2'
|
112
|
-
type: :runtime
|
113
|
-
prerelease: false
|
114
|
-
version_requirements: !ruby/object:Gem::Requirement
|
115
|
-
requirements:
|
116
|
-
- - "<"
|
117
|
-
- !ruby/object:Gem::Version
|
118
|
-
version: '6.2'
|
119
|
-
- !ruby/object:Gem::Dependency
|
120
|
-
name: attr_encrypted
|
121
|
-
requirement: !ruby/object:Gem::Requirement
|
122
|
-
requirements:
|
123
|
-
- - ">="
|
124
|
-
- !ruby/object:Gem::Version
|
125
|
-
version: '1.3'
|
126
|
-
- - "!="
|
127
|
-
- !ruby/object:Gem::Version
|
128
|
-
version: '2'
|
129
|
-
- - "<"
|
109
|
+
- - "~>"
|
130
110
|
- !ruby/object:Gem::Version
|
131
|
-
version: '
|
111
|
+
version: '7.0'
|
132
112
|
type: :runtime
|
133
113
|
prerelease: false
|
134
114
|
version_requirements: !ruby/object:Gem::Requirement
|
135
115
|
requirements:
|
136
|
-
- - "
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: '1.3'
|
139
|
-
- - "!="
|
140
|
-
- !ruby/object:Gem::Version
|
141
|
-
version: '2'
|
142
|
-
- - "<"
|
116
|
+
- - "~>"
|
143
117
|
- !ruby/object:Gem::Version
|
144
|
-
version: '
|
118
|
+
version: '7.0'
|
145
119
|
- !ruby/object:Gem::Dependency
|
146
120
|
name: devise
|
147
121
|
requirement: !ruby/object:Gem::Requirement
|
@@ -260,9 +234,9 @@ executables: []
|
|
260
234
|
extensions: []
|
261
235
|
extra_rdoc_files: []
|
262
236
|
files:
|
237
|
+
- ".github/workflows/ci.yml"
|
263
238
|
- ".gitignore"
|
264
239
|
- ".rspec"
|
265
|
-
- ".travis.yml"
|
266
240
|
- Appraisals
|
267
241
|
- CHANGELOG.md
|
268
242
|
- CONTRIBUTING.md
|
@@ -274,12 +248,14 @@ files:
|
|
274
248
|
- certs/tinfoil-cacert.pem
|
275
249
|
- certs/tinfoilsecurity-gems-cert.pem
|
276
250
|
- devise-two-factor.gemspec
|
277
|
-
- gemfiles/
|
278
|
-
- gemfiles/
|
279
|
-
- gemfiles/
|
280
|
-
- gemfiles/
|
281
|
-
- gemfiles/
|
282
|
-
- gemfiles/
|
251
|
+
- gemfiles/rails_4.1.gemfile
|
252
|
+
- gemfiles/rails_4.2.gemfile
|
253
|
+
- gemfiles/rails_5.0.gemfile
|
254
|
+
- gemfiles/rails_5.1.gemfile
|
255
|
+
- gemfiles/rails_5.2.gemfile
|
256
|
+
- gemfiles/rails_6.0.gemfile
|
257
|
+
- gemfiles/rails_6.1.gemfile
|
258
|
+
- gemfiles/rails_7.0.gemfile
|
283
259
|
- lib/devise-two-factor.rb
|
284
260
|
- lib/devise_two_factor/models.rb
|
285
261
|
- lib/devise_two_factor/models/two_factor_authenticatable.rb
|
@@ -299,7 +275,7 @@ homepage: https://github.com/tinfoil/devise-two-factor
|
|
299
275
|
licenses:
|
300
276
|
- MIT
|
301
277
|
metadata: {}
|
302
|
-
post_install_message:
|
278
|
+
post_install_message:
|
303
279
|
rdoc_options: []
|
304
280
|
require_paths:
|
305
281
|
- lib
|
@@ -314,8 +290,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
314
290
|
- !ruby/object:Gem::Version
|
315
291
|
version: '0'
|
316
292
|
requirements: []
|
317
|
-
rubygems_version: 3.
|
318
|
-
signing_key:
|
293
|
+
rubygems_version: 3.2.32
|
294
|
+
signing_key:
|
319
295
|
specification_version: 4
|
320
296
|
summary: Barebones two-factor authentication with Devise
|
321
297
|
test_files:
|
metadata.gz.sig
CHANGED
Binary file
|
data/.travis.yml
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
sudo: false
|
2
|
-
language: ruby
|
3
|
-
cache: bundler
|
4
|
-
before_install:
|
5
|
-
- gem i rubygems-update -v '<3' && update_rubygems
|
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
|
15
|
-
rvm:
|
16
|
-
- "2.1"
|
17
|
-
- "2.2"
|
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
|