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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33ab6513476203a5a4135af19c1f3bbddeeed83fb2ed8bf3a74c2afe2e74be9b
4
- data.tar.gz: 16068a92b6b20aa06108cb0e3cd294dc5c98c19563311d024dcfc7e0573030ad
3
+ metadata.gz: f0f81d936ba021c504827ebf9a6faa199f7a0a8f714fee2d9ce6d48acbde423b
4
+ data.tar.gz: 38bf04b9361f64618c84081c5ce5436f523f8476c625b91b92cfba8e56e2cd5c
5
5
  SHA512:
6
- metadata.gz: 258cd2abf3bc9beb80c0f9fc596b33055efa24ca53177847fbab5a38b80a220e322a2739303b30128c356000dabd708bcc77e835dacecee2e3c9fe51b66c2b33
7
- data.tar.gz: 940c49c9b2cbea4832ee8f66c39b26c1c6ad45d09dc054cf0c54d0be823219e18ad4ef7ca12dc388e2eec319534be0d880b5f9d3e2eb5fcace86ce6b9008e960
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" do
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-2" do
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-0" do
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-1" do
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-2" do
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-0" do
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
- [![Build Status](https://travis-ci.org/tinfoil/devise-two-factor.svg?branch=master)](https://travis-ci.org/tinfoil/devise-two-factor)
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
- First, you'll need a Rails application setup with Devise. Visit the Devise [homepage](https://github.com/plataformatec/devise) for instructions.
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
- You can add Devise-Two-Factor to your Gemfile with:
25
+ 1. A Rails application with [devise](https://github.com/heartcombo/devise) installed
26
+ 1. Secrets configured for ActiveRecord encrypted attributes
27
27
 
28
- ```ruby
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
- Next, since Devise-Two-Factor encrypts its secrets before storing them in the database, you'll need to generate an encryption key, and store it in an environment variable of your choice. Set the encryption key in the model that uses Devise:
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
- ```ruby
35
- devise :two_factor_authenticatable,
36
- :otp_secret_encryption_key => ENV['YOUR_ENCRYPTION_KEY_HERE']
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
- Finally, you can automate all of the required setup by simply running:
41
+ Add Devise-Two-Factor to your Gemfile with:
41
42
 
42
43
  ```ruby
43
- rails generate devise_two_factor MODEL ENVIRONMENT_VARIABLE
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
- This generator will add a few columns to the specified model:
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
- Remember to apply the new migration.
49
+ There is a generator which automates most of the setup:
57
50
 
58
- ```ruby
59
- bundle exec rake db:migrate
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
- 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.
56
+ Where `MODEL` is the name of the model you wish to add two-factor functionality to (for example `user`)
63
57
 
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.
58
+ This generator will:
65
59
 
66
- If you're running Rails 4, you'll also 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`:
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
- ```ruby
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
- protected
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
- If you're running Devise 4.0.0 or above, you'll want to use `.permit` instead:
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
- before_action :configure_permitted_parameters, if: :devise_controller?
80
+ # app/controllers/application_controller.rb
84
81
 
85
- ...
82
+ before_action :configure_permitted_parameters, if: :devise_controller?
86
83
 
87
- protected
84
+ # ...
88
85
 
89
- def configure_permitted_parameters
90
- devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
91
- end
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
- **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.**
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.
@@ -3,7 +3,7 @@ MIIHSjCCBTKgAwIBAgIJAK2u0LojMCNgMA0GCSqGSIb3DQEBBQUAMIGcMQswCQYD
3
3
  VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEfMB0GA1UE
4
4
  ChMWVGluZm9pbCBTZWN1cml0eSwgSW5jLjEfMB0GA1UEAxMWVGluZm9pbCBTZWN1
5
5
  cml0eSwgSW5jLjEqMCgGCSqGSIb3DQEJARYbc3VwcG9ydEB0aW5mb2lsc2VjdXJp
6
- dHkuY29tMB4XDTExMTIyNzA1MDc0N1oXDTIxMTIyNDA1MDc0N1owgZwxCzAJBgNV
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
- SIb3DQEBBQUAA4ICAQAD7nsmdg1vStFTi8/P2rgSFxlXxZT0aaVVB1bFBe/m5phb
31
- MjvKQ7VAuiFZxEp3oBNdXTi4FzT1QjhRKdlYMgKZQnU+XDLLIYuoi+atxr5qGD4B
32
- m58eCGO6ZEutVs3Z7s63UOm5rG0zJ+IEWh8VHMvxgSwiX88QyJuhOtqeiKhIeSGZ
33
- 2/qGGMWgsScnPg3J/ZVOIKUn/4ljEDlC64Gh5Zz5PZUbGSXPMhdYbSD3EknDvEGA
34
- omYW4jlPMeK3GJgwAZu9yWC8hHGFpiMca/6W0W622cg7MX+CByOd+24dvWFnOHur
35
- NHBqI+kZo/7Sjdm8x7TWEOz9Rfh5RPMeVNRTj4iq0B6GzfaecT3Yn8y7HTRRiWns
36
- IYpP+iHCFYnZhDZsFi4ccKqxKtj6BGmhLf00FuNpgkvrsU3cXrhidkCaYGYj1SME
37
- 1CMfy0PPKVDpDKeFb6y0NvLf4d57vi99dZAvSJEO18rrNEHN2VUfCKRPA/mBSMLY
38
- RxKWAby1YVT/8iC9JWix9yvgsEUtTLyOFxLGtgj3PRiQSvbNe/jK4G9WAIFe6R9E
39
- 9+HUO2owcmyFXyU3rC/z/lBfDP+2pIRFdUVRGlYCMeUqR08PXpfva5+NQz21fC69
40
- FPRMZvXh70ntnFaWAq+j6NCss+AauC8ckECiQsTgbzJvJd6C3mJXYHkNCQODhg==
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+igAwIBAgIIP4wV6YA6CO0wDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV
2
+ MIIGADCCA+igAwIBAgIIHIF9ta6cW3YwDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV
3
3
  BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
4
4
  ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
5
5
  aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
6
- eS5jb20wHhcNMjEwNDA4MTUxODAwWhcNMjExMjI0MDUwNzAwWjCBiDELMAkGA1UE
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
- 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==
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-----
@@ -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', '< 6.2'
25
- s.add_runtime_dependency 'activesupport', '< 6.2'
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
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "railties", "~> 6.0"
6
+ gem "activesupport", "~> 6.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "railties", "~> 6.1"
6
+ gem "activesupport", "~> 6.1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "railties", "~> 7.0"
6
+ gem "activesupport", "~> 7.0"
7
+
8
+ gemspec path: "../"
@@ -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
- 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
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
- [:encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt, :consumed_timestep]
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
- if totp.verify(code, drift_behind: self.class.otp_allowed_drift, drift_ahead: self.class.otp_allowed_drift)
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(:encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt, :consumed_timestep)
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
@@ -1,3 +1,3 @@
1
1
  module DeviseTwoFactor
2
- VERSION = '4.0.0'.freeze
2
+ VERSION = '5.0.0'.freeze
3
3
  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
- "encrypted_otp_secret:string",
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
- define_model_callbacks :update
10
-
11
- devise :two_factor_authenticatable, :otp_secret_encryption_key => 'test-key'*4
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, :otp_secret_encryption_key => 'test-key'*4
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.0.0
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
- dHkuY29tMB4XDTExMTIyNzA1MDc0N1oXDTIxMTIyNDA1MDc0N1owgZwxCzAJBgNV
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
- SIb3DQEBBQUAA4ICAQAD7nsmdg1vStFTi8/P2rgSFxlXxZT0aaVVB1bFBe/m5phb
42
- MjvKQ7VAuiFZxEp3oBNdXTi4FzT1QjhRKdlYMgKZQnU+XDLLIYuoi+atxr5qGD4B
43
- m58eCGO6ZEutVs3Z7s63UOm5rG0zJ+IEWh8VHMvxgSwiX88QyJuhOtqeiKhIeSGZ
44
- 2/qGGMWgsScnPg3J/ZVOIKUn/4ljEDlC64Gh5Zz5PZUbGSXPMhdYbSD3EknDvEGA
45
- omYW4jlPMeK3GJgwAZu9yWC8hHGFpiMca/6W0W622cg7MX+CByOd+24dvWFnOHur
46
- NHBqI+kZo/7Sjdm8x7TWEOz9Rfh5RPMeVNRTj4iq0B6GzfaecT3Yn8y7HTRRiWns
47
- IYpP+iHCFYnZhDZsFi4ccKqxKtj6BGmhLf00FuNpgkvrsU3cXrhidkCaYGYj1SME
48
- 1CMfy0PPKVDpDKeFb6y0NvLf4d57vi99dZAvSJEO18rrNEHN2VUfCKRPA/mBSMLY
49
- RxKWAby1YVT/8iC9JWix9yvgsEUtTLyOFxLGtgj3PRiQSvbNe/jK4G9WAIFe6R9E
50
- 9+HUO2owcmyFXyU3rC/z/lBfDP+2pIRFdUVRGlYCMeUqR08PXpfva5+NQz21fC69
51
- FPRMZvXh70ntnFaWAq+j6NCss+AauC8ckECiQsTgbzJvJd6C3mJXYHkNCQODhg==
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+igAwIBAgIIP4wV6YA6CO0wDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV
55
+ MIIGADCCA+igAwIBAgIIHIF9ta6cW3YwDQYJKoZIhvcNAQENBQAwgZwxCzAJBgNV
56
56
  BAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQK
57
57
  ExZUaW5mb2lsIFNlY3VyaXR5LCBJbmMuMR8wHQYDVQQDExZUaW5mb2lsIFNlY3Vy
58
58
  aXR5LCBJbmMuMSowKAYJKoZIhvcNAQkBFhtzdXBwb3J0QHRpbmZvaWxzZWN1cml0
59
- eS5jb20wHhcNMjEwNDA4MTUxODAwWhcNMjExMjI0MDUwNzAwWjCBiDELMAkGA1UE
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
- 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==
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: 2021-04-08 00:00:00.000000000 Z
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: '6.2'
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: '6.2'
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: '4'
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: '4'
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/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
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.0.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
@@ -1,8 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "railties", "~> 6.0.0.rc2"
6
- gem "activesupport", "~> 6.0.0.rc2"
7
-
8
- gemspec path: "../"