devise-two-factor 5.0.0 → 6.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/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +3 -3
- data/.markdownlint.json +6 -0
- data/CHANGELOG.md +15 -0
- data/README.md +75 -30
- data/SECURITY.md +5 -0
- data/UPGRADING.md +38 -15
- data/devise-two-factor.gemspec +4 -6
- data/gemfiles/rails_7.0.gemfile +2 -2
- data/gemfiles/{rails_4.1.gemfile → rails_7.1.gemfile} +2 -2
- data/lib/devise-two-factor.rb +6 -4
- data/lib/devise_two_factor/models/two_factor_authenticatable.rb +4 -2
- data/lib/devise_two_factor/models/two_factor_backupable.rb +3 -2
- data/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb +5 -5
- data/lib/devise_two_factor/spec_helpers/two_factor_backupable_shared_examples.rb +2 -2
- data/lib/devise_two_factor/strategies/two_factor_authenticatable.rb +1 -1
- data/lib/devise_two_factor/strategies/two_factor_backupable.rb +0 -3
- data/lib/devise_two_factor/version.rb +1 -1
- data/spec/devise/models/two_factor_authenticatable_spec.rb +5 -1
- data/spec/devise/models/two_factor_backupable_spec.rb +4 -0
- data/spec/spec_helper.rb +0 -1
- data.tar.gz.sig +0 -0
- metadata +12 -28
- metadata.gz.sig +0 -0
- data/gemfiles/rails_4.2.gemfile +0 -8
- data/gemfiles/rails_5.0.gemfile +0 -8
- data/gemfiles/rails_5.1.gemfile +0 -8
- data/gemfiles/rails_5.2.gemfile +0 -8
- data/gemfiles/rails_6.0.gemfile +0 -8
- data/gemfiles/rails_6.1.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: 131bab7308f2b9d46a41b5e11b85411e0cd097e97f16c82356eadb1cf87d5cc3
|
4
|
+
data.tar.gz: b117115cfbb9ffe4f6dcec8127de0e0d51ca5fa835407a82cad8afc17f923f5f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 187bd4ed05b0ad83da40cbe208c4bbe2ce91581d95f437cdaa27364ddb0be3696a2a03aad62e8a02f07adaeca24778202710f20f79422156b4aef63d13a03721
|
7
|
+
data.tar.gz: 5635dccf010dd259404e9e03092eb9e107896b31f178d1ae3760046aa794fe449eb3bd2929bfb08c29df725376d8abc3dbb3f80e2dfb63405172c6130c24c687
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/.github/workflows/ci.yml
CHANGED
@@ -12,14 +12,14 @@ jobs:
|
|
12
12
|
fail-fast: false
|
13
13
|
matrix:
|
14
14
|
# Due to https://github.com/actions/runner/issues/849, we should quote versions
|
15
|
-
ruby: ['
|
16
|
-
rails: ['7.0']
|
15
|
+
ruby: ['3.1', '3.2', '3.3', 'truffleruby-head']
|
16
|
+
rails: ['7.0', '7.1']
|
17
17
|
|
18
18
|
name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}
|
19
19
|
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
|
20
20
|
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/rails_${{ matrix.rails }}.gemfile
|
21
21
|
steps:
|
22
|
-
- uses: actions/checkout@
|
22
|
+
- uses: actions/checkout@v4
|
23
23
|
- name: Set up Ruby
|
24
24
|
uses: ruby/setup-ruby@v1
|
25
25
|
with:
|
data/.markdownlint.json
ADDED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,21 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## 5.1.0
|
6
|
+
|
7
|
+
- Remove faker dev dependency
|
8
|
+
- Insert two_factor_authenticatable at the top of the devise module list
|
9
|
+
- README and CI improvements
|
10
|
+
|
11
|
+
## 5.0.0
|
12
|
+
|
13
|
+
**Breaking Changes**
|
14
|
+
- attr_encrypted has been deprecated in favor of native Rails attribute encryption. See [UPGRADING.md](UPGRADING.md) for details on how to migrate your records. You **must** use or build a migration strategy (see examples in [UPGRADING.md](UPGRADING.md)) to use existing data!
|
15
|
+
- Rails 7 is now required.
|
16
|
+
|
17
|
+
## 4.1.0 / 4.1.1
|
18
|
+
- Add support for attr_encrypted v4
|
19
|
+
|
5
20
|
## 4.0.2
|
6
21
|
- Add Rails 7.0 support
|
7
22
|
- Renew signing certificate
|
data/README.md
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# Devise-Two-Factor Authentication
|
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
2
|
|
4
|
-
![Build Status](https://github.com/
|
3
|
+
![Build Status](https://github.com/devise-two-factor/devise-two-factor/actions/workflows/ci.yml/badge.svg)
|
5
4
|
|
6
5
|
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
6
|
|
@@ -11,12 +10,12 @@ Devise-Two-Factor is a minimalist extension to Devise which offers support for t
|
|
11
10
|
* Is extensible, and includes two-factor backup codes as an example of how plugins can be structured
|
12
11
|
|
13
12
|
## Contributing
|
13
|
+
|
14
14
|
We welcome pull requests, bug reports, and other contributions. We're especially looking for help getting this gem fully compatible with Rails 5+ and squashing any deprecation messages.
|
15
15
|
|
16
16
|
## Example App
|
17
|
-
An example Rails 4 application is provided in the `demo` directory. It showcases a minimal example of Devise-Two-Factor in action, and can act as a reference for integrating the gem into your own application.
|
18
17
|
|
19
|
-
|
18
|
+
See [examples](demo/README.md).
|
20
19
|
|
21
20
|
## Getting Started
|
22
21
|
|
@@ -27,15 +26,31 @@ Devise-Two-Factor doesn't require much to get started, but there are two prerequ
|
|
27
26
|
|
28
27
|
First, you'll need a Rails application setup with Devise. Visit the Devise [homepage](https://github.com/plataformatec/devise) for instructions.
|
29
28
|
|
30
|
-
Devise-Two-Factor uses [ActiveRecord encrypted attributes](https://edgeguides.rubyonrails.org/active_record_encryption.html)
|
29
|
+
Devise-Two-Factor uses [ActiveRecord encrypted attributes](https://edgeguides.rubyonrails.org/active_record_encryption.html). If you haven't already set up ActiveRecord encryption you must generate a key set and configure your application to use them either with Rails' encrypted credentials or from another source such as environment variables.
|
31
30
|
|
32
31
|
```bash
|
33
|
-
#
|
34
|
-
|
32
|
+
# Generates a random key set and outputs it to stdout
|
33
|
+
./bin/rails db:encryption:init
|
34
|
+
```
|
35
|
+
|
36
|
+
You can load the key set using Rails' credentials.
|
37
|
+
|
38
|
+
```bash
|
39
|
+
# Copy the generated key set into your encrypted credentials file
|
40
|
+
# Setting the EDITOR environment variable is optional, but without it your default editor will open
|
41
|
+
EDITOR="code --wait" ./bin/rails credentials:edit
|
42
|
+
```
|
43
|
+
|
44
|
+
To learn more about credentials run `./bin/rails credentials:help`.
|
35
45
|
|
36
|
-
|
37
|
-
|
38
|
-
|
46
|
+
Alternatively, you can configure your application with environment variables rather than Rails' credentials.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# Copy the generate key set and set them as environment variables
|
50
|
+
|
51
|
+
config.active_record.encryption.primary_key = ENV['ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY']
|
52
|
+
config.active_record.encryption.deterministic_key = ENV['ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY']
|
53
|
+
config.active_record.encryption.key_derivation_salt = ENV['ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT']
|
39
54
|
```
|
40
55
|
|
41
56
|
Add Devise-Two-Factor to your Gemfile with:
|
@@ -58,11 +73,13 @@ Where `MODEL` is the name of the model you wish to add two-factor functionality
|
|
58
73
|
This generator will:
|
59
74
|
|
60
75
|
1. Create a new migration which adds a few columns to the specified model:
|
76
|
+
|
61
77
|
```ruby
|
62
78
|
add_column :users, :otp_secret, :string
|
63
79
|
add_column :users, :consumed_timestep, :integer
|
64
80
|
add_column :users, :otp_required_for_login, :boolean
|
65
81
|
```
|
82
|
+
|
66
83
|
1. Edit `app/models/MODEL.rb` (where MODEL is your model name):
|
67
84
|
* add the `:two_factor_authenticatable` devise module
|
68
85
|
* remove the `:database_authenticatable` if present because it is incompatible with `:two_factor_authenticatable`
|
@@ -95,6 +112,7 @@ Finally you should verify that `:database_authenticatable` is **not** being load
|
|
95
112
|
**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!
|
96
113
|
|
97
114
|
## Designing Your Workflow
|
115
|
+
|
98
116
|
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.
|
99
117
|
|
100
118
|
There are two key workflows you'll have to think about:
|
@@ -104,8 +122,8 @@ There are two key workflows you'll have to think about:
|
|
104
122
|
|
105
123
|
We chose to keep things as simple as possible, and our implementation can be found by registering at [Tinfoil Security](https://www.tinfoilsecurity.com/), and enabling two-factor authentication from the [security settings page](https://www.tinfoilsecurity.com/account/security).
|
106
124
|
|
107
|
-
|
108
125
|
### Logging In
|
126
|
+
|
109
127
|
Logging in with two-factor authentication works extremely similarly to regular database authentication in Devise. The `TwoFactorAuthenticatable` strategy accepts three parameters:
|
110
128
|
|
111
129
|
1. email
|
@@ -115,11 +133,13 @@ Logging in with two-factor authentication works extremely similarly to regular d
|
|
115
133
|
These parameters can be submitted to the standard Devise login route, and the strategy will handle the authentication of the user for you.
|
116
134
|
|
117
135
|
### Disabling Automatic Login After Password Resets
|
136
|
+
|
118
137
|
If you use the Devise `recoverable` strategy, the default behavior after a password reset is to automatically authenticate the user and log them in. This is obviously a problem if a user has two-factor authentication enabled, as resetting the password would get around the two-factor requirement.
|
119
138
|
|
120
139
|
Because of this, you need to set `sign_in_after_reset_password` to `false` (either globally in your Devise initializer or via `devise_for`).
|
121
140
|
|
122
141
|
### Enabling Two-Factor Authentication
|
142
|
+
|
123
143
|
Enabling two-factor authentication for a user is easy. For example, if my user model were named User, I could do the following:
|
124
144
|
|
125
145
|
```ruby
|
@@ -150,6 +170,7 @@ current_user.current_otp
|
|
150
170
|
```
|
151
171
|
|
152
172
|
The generated code will be valid for the duration specified by `otp_allowed_drift`. This value can be modified by adding a config in `config/initializers/devise.rb`.
|
173
|
+
|
153
174
|
```ruby
|
154
175
|
Devise.otp_allowed_drift = 240 # value in seconds
|
155
176
|
Devise.setup do |config|
|
@@ -166,13 +187,27 @@ However you decide to handle enrollment, there are a few important consideration
|
|
166
187
|
It sounds like a lot of work, but most of these problems have been very elegantly solved by other people. We recommend taking a look at the excellent workflows used by Heroku and Google for inspiration.
|
167
188
|
|
168
189
|
### Filtering sensitive parameters from the logs
|
190
|
+
|
169
191
|
To prevent two-factor authentication codes from leaking if your application logs get breached, you'll want to filter sensitive parameters from the Rails logs. Add the following to `config/initializers/filter_parameter_logging.rb`:
|
170
192
|
|
171
193
|
```ruby
|
172
194
|
Rails.application.config.filter_parameters += [:otp_attempt]
|
173
195
|
```
|
174
196
|
|
197
|
+
### Preventing Brute-Force Attacks
|
198
|
+
|
199
|
+
With any authentication solution it is also important to protect your users from brute-force attacks. For Devise-Two-Factor specifically if a user's username and password have already been compromised an attacker would be able to try possible TOTP codes and see if they can hit a lucky collision to log in. While Devise-Two-Factor is open-ended by design and cannot solve this for all applications natively there are some possible mitigations to consider. A non-exhaustive list follows:
|
200
|
+
|
201
|
+
1. Use the `lockable` strategy from Devise to lock a user after a certain number of failed login attempts. See https://www.rubydoc.info/github/heartcombo/devise/main/Devise/Models/Lockable for more information.
|
202
|
+
2. Configure a rate limit for your application, especially on the endpoints used to log in. One such library to accomplish this is [rack-attack](https://rubygems.org/gems/rack-attack).
|
203
|
+
3. When displaying authentication errors hide whether validating a username/password combination failed or a two-factor code failed behind a more generic error message.
|
204
|
+
|
205
|
+
#### Acknowledgements
|
206
|
+
|
207
|
+
Thank you to Christian Reitter (Radically Open Security) and Chris MacNaughton (Centauri Solutions) for reporting the issue.
|
208
|
+
|
175
209
|
## Backup Codes
|
210
|
+
|
176
211
|
Devise-Two-Factor is designed with extensibility in mind. One such extension, `TwoFactorBackupable`, is included and serves as a good example of how to extend this gem. This plugin allows you to add the ability to generate single-use backup codes for a user, which they may use to bypass two-factor authentication, in the event that they lose access to their device.
|
177
212
|
|
178
213
|
To install it, you need to add the `:two_factor_backupable` directive to your model.
|
@@ -187,17 +222,39 @@ You'll also be required to enable the `:two_factor_backupable` strategy, by addi
|
|
187
222
|
manager.default_strategies(:scope => :user).unshift :two_factor_backupable
|
188
223
|
```
|
189
224
|
|
190
|
-
|
225
|
+
### Migration
|
226
|
+
|
227
|
+
The final installation step may be dependent on your version of Rails.
|
228
|
+
|
229
|
+
#### PostgreSQL
|
191
230
|
|
192
231
|
```ruby
|
193
232
|
class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration
|
194
233
|
def change
|
195
|
-
# Change type from :string to :text if using MySQL database
|
196
234
|
add_column :users, :otp_backup_codes, :string, array: true
|
197
235
|
end
|
198
236
|
end
|
199
237
|
```
|
200
238
|
|
239
|
+
#### MySQL
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
# migration
|
243
|
+
class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration
|
244
|
+
def change
|
245
|
+
add_column :users, :otp_backup_codes, :text
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# model
|
250
|
+
class User < ApplicationRecord
|
251
|
+
devise :two_factor_backupable
|
252
|
+
serialize :otp_backup_codes, Array
|
253
|
+
end
|
254
|
+
```
|
255
|
+
|
256
|
+
### Generation
|
257
|
+
|
201
258
|
You can then generate backup codes for a user:
|
202
259
|
|
203
260
|
```ruby
|
@@ -215,23 +272,8 @@ devise :two_factor_backupable, otp_backup_code_length: 32,
|
|
215
272
|
otp_number_of_backup_codes: 10
|
216
273
|
```
|
217
274
|
|
218
|
-
### Help! I'm not using Rails 4.0!
|
219
|
-
Don't worry! `TwoFactorBackupable` stores the backup codes as an array of strings in the database. In Rails 4.0 this is supported natively, but in earlier versions you can use a gem to emulate this behavior: we recommend [activerecord-postgres-array](https://github.com/tlconnor/activerecord-postgres-array).
|
220
|
-
|
221
|
-
You'll then simply have to create a migration to add an array named `otp_backup_codes` to your model. If you use the above gem, this migration might look like:
|
222
|
-
|
223
|
-
```ruby
|
224
|
-
class AddTwoFactorBackupCodesToUsers < ActiveRecord::Migration
|
225
|
-
def change
|
226
|
-
# Change type from :string_array to :text_array if using MySQL database
|
227
|
-
add_column :users, :otp_backup_codes, :string_array
|
228
|
-
end
|
229
|
-
end
|
230
|
-
```
|
231
|
-
|
232
|
-
Now just continue with the setup in the previous section, skipping the generator step.
|
233
|
-
|
234
275
|
## Testing
|
276
|
+
|
235
277
|
Devise-Two-Factor includes shared-examples for both `TwoFactorAuthenticatable` and `TwoFactorBackupable`. Adding the following two lines to the specs for your two-factor enabled models will allow you to test your models for two-factor functionality:
|
236
278
|
|
237
279
|
```ruby
|
@@ -242,6 +284,7 @@ it_behaves_like "two_factor_backupable"
|
|
242
284
|
```
|
243
285
|
|
244
286
|
## Troubleshooting
|
287
|
+
|
245
288
|
If you are using Rails 4.x and Ruby >= 2.7, you may get an error like
|
246
289
|
|
247
290
|
```
|
@@ -251,9 +294,11 @@ Failure/Error: require 'devise'
|
|
251
294
|
NoMethodError:
|
252
295
|
undefined method `new' for BigDecimal:Class
|
253
296
|
```
|
297
|
+
|
254
298
|
see https://github.com/ruby/bigdecimal#which-version-should-you-select and https://github.com/ruby/bigdecimal/issues/127
|
255
299
|
for more details, but you should be able to solve this
|
256
300
|
by explicitly requiring an older version of bigdecimal in your gemfile like
|
257
|
-
|
301
|
+
|
302
|
+
```ruby
|
258
303
|
gem "bigdecimal", "~> 1.4"
|
259
304
|
```
|
data/SECURITY.md
ADDED
data/UPGRADING.md
CHANGED
@@ -1,8 +1,25 @@
|
|
1
|
-
# Upgrading
|
1
|
+
# Upgrading
|
2
2
|
|
3
|
-
##
|
3
|
+
## Upgrading from 5.x to 6.x
|
4
4
|
|
5
|
-
###
|
5
|
+
### save!
|
6
|
+
|
7
|
+
`consume_otp!` and `invalidate_otp_backup_code!` now call `save!` instead of `save` (or nothing at all in the case of `invalide_otp_backup_code!`). If you manually called `save`/`save!` after calling `invalidate_otp_backup_code` you may be able to remove it.
|
8
|
+
|
9
|
+
### Secret Lengths
|
10
|
+
|
11
|
+
The `otp_secret_length` and `otp_backup_code_length` options have changed to be the number of random bytes that are generated.
|
12
|
+
If you had configured these values you may want to change them if you wish to keep the same output length.
|
13
|
+
|
14
|
+
`otp_secret_length` now has a default value of 20, generating a 160 bit secret key with an output length length of 32 bytes.
|
15
|
+
|
16
|
+
`otp_backup_code_length` now has a default value of 16, generating a 32 byte backup code.
|
17
|
+
|
18
|
+
## Upgrading from 4.x to 5.x
|
19
|
+
|
20
|
+
### Background
|
21
|
+
|
22
|
+
#### Database columns in version 4.x and older
|
6
23
|
|
7
24
|
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
25
|
|
@@ -21,7 +38,7 @@ otp_required_for_login
|
|
21
38
|
|
22
39
|
A fresh install of 4.x would create all five of the database columns above.
|
23
40
|
|
24
|
-
|
41
|
+
#### Database columns in version 5.x and later
|
25
42
|
|
26
43
|
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
44
|
|
@@ -33,18 +50,15 @@ consumed_timestep
|
|
33
50
|
otp_required_for_login
|
34
51
|
```
|
35
52
|
|
36
|
-
### Upgrading from 4.x to 5.x
|
37
|
-
|
38
|
-
|
39
53
|
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
54
|
|
41
|
-
|
55
|
+
### Assumptions
|
42
56
|
|
43
57
|
This guide assumes you are upgrading an existing Rails 6 app (with `devise` and `devise-two-factor`) to Rails 7.
|
44
58
|
|
45
59
|
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
60
|
|
47
|
-
|
61
|
+
### Phase 1: Upgrading devise-two-factor as part of Rails 7 upgrade
|
48
62
|
|
49
63
|
1. Update the version constraint for Rails in your `Gemfile` to your desired version e.g. `gem "rails", "~> 7.0.3"`
|
50
64
|
1. Run `bundle install` and resolve any issues with dependencies.
|
@@ -59,7 +73,7 @@ This gem must be upgraded **as part of a Rails 7 upgrade**. See [the official Ra
|
|
59
73
|
```
|
60
74
|
1. Add a `legacy_otp_secret` method to your user model e.g. `User`.
|
61
75
|
* 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](
|
76
|
+
* The implementation shown below works if you set up devise-two-factor with the settings suggested in the [OLD README](https://github.com/devise-two-factor/devise-two-factor/blob/8d74f5ee45594bf00e60d5d49eb6fcde82c2d2ba/README.md).
|
63
77
|
* If you have customised the encryption scheme used to store the OTP secret then you will need to update this method to match.
|
64
78
|
* 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
79
|
```ruby
|
@@ -87,7 +101,7 @@ This gem must be upgraded **as part of a Rails 7 upgrade**. See [the official Ra
|
|
87
101
|
cipher_text = raw_cipher_text[0..-17]
|
88
102
|
auth_tag = raw_cipher_text[-16..-1]
|
89
103
|
|
90
|
-
# this
|
104
|
+
# this algorithm lifted from
|
91
105
|
# https://github.com/attr-encrypted/encryptor/blob/master/lib/encryptor.rb#L54
|
92
106
|
|
93
107
|
# create an OpenSSL object which will decrypt the AES cipher with 256 bit
|
@@ -101,7 +115,7 @@ This gem must be upgraded **as part of a Rails 7 upgrade**. See [the official Ra
|
|
101
115
|
cipher.decrypt
|
102
116
|
|
103
117
|
# Use a Password-Based Key Derivation Function to generate the key actually
|
104
|
-
# used for
|
118
|
+
# used for encryption from the key we got as input.
|
105
119
|
cipher.key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(key, salt, hmac_iterations, cipher.key_len)
|
106
120
|
|
107
121
|
# set the Initialization Vector (IV)
|
@@ -149,7 +163,7 @@ You can now deploy your upgraded application and devise-two-factor should work a
|
|
149
163
|
|
150
164
|
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
165
|
|
152
|
-
|
166
|
+
### Phase 2: Clean up
|
153
167
|
|
154
168
|
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
169
|
|
@@ -190,8 +204,17 @@ This "clean up" phase can happen at the same time as your initial deployment but
|
|
190
204
|
end
|
191
205
|
end
|
192
206
|
```
|
207
|
+
1. Remove `otp_secret_encryption_key` from the model setup. This also assumes you successfully ran the rake task in step 1.
|
208
|
+
```ruby
|
209
|
+
# from this:
|
210
|
+
devise :two_factor_authenticatable,
|
211
|
+
otp_secret_encryption_key: ENV['YOUR_ENCRYPTION_KEY_HERE']
|
212
|
+
|
213
|
+
# to this:
|
214
|
+
devise :two_factor_authenticatable
|
215
|
+
```
|
193
216
|
|
194
|
-
|
217
|
+
## Upgrading from 2.x to 3.x
|
195
218
|
|
196
219
|
Pull request #76 allows for compatibility with `attr_encrypted` 3.0, which should be used due to a security vulnerability discovered in 2.0.
|
197
220
|
|
@@ -211,7 +234,7 @@ class User < ActiveRecord::Base
|
|
211
234
|
:otp_secret_encryption_key => ENV['DEVISE_TWO_FACTOR_ENCRYPTION_KEY']
|
212
235
|
```
|
213
236
|
|
214
|
-
|
237
|
+
## Upgrading from 1.x to 2.x
|
215
238
|
|
216
239
|
Pull request #43 added a new field to protect against "shoulder-surfing" attacks. If upgrading, you'll need to add the `:consumed_timestep` column to your `Users` model.
|
217
240
|
|
data/devise-two-factor.gemspec
CHANGED
@@ -5,12 +5,11 @@ Gem::Specification.new do |s|
|
|
5
5
|
s.name = 'devise-two-factor'
|
6
6
|
s.version = DeviseTwoFactor::VERSION.dup
|
7
7
|
s.platform = Gem::Platform::RUBY
|
8
|
-
s.
|
8
|
+
s.license = 'MIT'
|
9
9
|
s.summary = 'Barebones two-factor authentication with Devise'
|
10
|
-
s.
|
11
|
-
s.
|
12
|
-
s.
|
13
|
-
s.authors = ['Shane Wilton']
|
10
|
+
s.homepage = 'https://github.com/devise-two-factor/devise-two-factor'
|
11
|
+
s.description = 'Devise-Two-Factor is a minimalist extension to Devise which offers support for two-factor authentication through the TOTP scheme.'
|
12
|
+
s.authors = ['Quinn Wilton']
|
14
13
|
|
15
14
|
s.cert_chain = [
|
16
15
|
'certs/tinfoil-cacert.pem',
|
@@ -31,5 +30,4 @@ Gem::Specification.new do |s|
|
|
31
30
|
s.add_development_dependency 'bundler', '> 1.0'
|
32
31
|
s.add_development_dependency 'rspec', '> 3'
|
33
32
|
s.add_development_dependency 'simplecov'
|
34
|
-
s.add_development_dependency 'faker'
|
35
33
|
end
|
data/gemfiles/rails_7.0.gemfile
CHANGED
data/lib/devise-two-factor.rb
CHANGED
@@ -3,9 +3,10 @@ require 'devise_two_factor/models'
|
|
3
3
|
require 'devise_two_factor/strategies'
|
4
4
|
|
5
5
|
module Devise
|
6
|
-
# The length of generated OTP
|
6
|
+
# The length of randomly generated OTP shared secret (in bytes).
|
7
|
+
# The secrets will be base32-encoded and have a length 1.6 times the configured value.
|
7
8
|
mattr_accessor :otp_secret_length
|
8
|
-
@@otp_secret_length =
|
9
|
+
@@otp_secret_length = 20
|
9
10
|
|
10
11
|
# The number of seconds before and after the current
|
11
12
|
# time for which codes will be accepted
|
@@ -20,7 +21,8 @@ module Devise
|
|
20
21
|
mattr_accessor :otp_encrypted_attribute_options
|
21
22
|
@@otp_encrypted_attribute_options = {}
|
22
23
|
|
23
|
-
# The length of
|
24
|
+
# The length of randomly generated OTP backup codes (in bytes).
|
25
|
+
# The codes will be hex-encoded and have a length twice the configured value.
|
24
26
|
mattr_accessor :otp_backup_code_length
|
25
27
|
@@otp_backup_code_length = 16
|
26
28
|
|
@@ -31,7 +33,7 @@ module Devise
|
|
31
33
|
end
|
32
34
|
|
33
35
|
Devise.add_module(:two_factor_authenticatable, :route => :session, :strategy => true,
|
34
|
-
:controller => :sessions, :model => true)
|
36
|
+
:controller => :sessions, :model => true, :insert_at => 0)
|
35
37
|
|
36
38
|
Devise.add_module(:two_factor_backupable, :route => :session, :strategy => true,
|
37
39
|
:controller => :sessions, :model => true)
|
@@ -81,7 +81,8 @@ module Devise
|
|
81
81
|
def consume_otp!
|
82
82
|
if self.consumed_timestep != current_otp_timestep
|
83
83
|
self.consumed_timestep = current_otp_timestep
|
84
|
-
|
84
|
+
save!(validate: false)
|
85
|
+
return true
|
85
86
|
end
|
86
87
|
|
87
88
|
false
|
@@ -93,8 +94,9 @@ module Devise
|
|
93
94
|
:otp_encrypted_attribute_options,
|
94
95
|
:otp_secret_encryption_key)
|
95
96
|
|
97
|
+
# Geneartes an OTP secret of the specified length, returning it after Base32 encoding.
|
96
98
|
def generate_otp_secret(otp_secret_length = self.otp_secret_length)
|
97
|
-
ROTP::Base32.
|
99
|
+
ROTP::Base32.random(otp_secret_length)
|
98
100
|
end
|
99
101
|
|
100
102
|
# Return value will be splatted with ** so return a version of the
|
@@ -20,7 +20,7 @@ module Devise
|
|
20
20
|
code_length = self.class.otp_backup_code_length
|
21
21
|
|
22
22
|
number_of_codes.times do
|
23
|
-
codes << SecureRandom.hex(code_length
|
23
|
+
codes << SecureRandom.hex(code_length)
|
24
24
|
end
|
25
25
|
|
26
26
|
hashed_codes = codes.map { |code| Devise::Encryptor.digest(self.class, code) }
|
@@ -30,7 +30,7 @@ module Devise
|
|
30
30
|
end
|
31
31
|
|
32
32
|
# Returns true and invalidates the given code
|
33
|
-
#
|
33
|
+
# if that code is a valid backup code.
|
34
34
|
def invalidate_otp_backup_code!(code)
|
35
35
|
codes = self.otp_backup_codes || []
|
36
36
|
|
@@ -39,6 +39,7 @@ module Devise
|
|
39
39
|
|
40
40
|
codes.delete(backup_code)
|
41
41
|
self.otp_backup_codes = codes
|
42
|
+
save!(validate: false)
|
42
43
|
return true
|
43
44
|
end
|
44
45
|
|
@@ -13,8 +13,8 @@ RSpec.shared_examples 'two_factor_authenticatable' do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
describe '#otp_secret' do
|
16
|
-
it 'should be of the
|
17
|
-
expect(subject.otp_secret.length).to eq(subject.class.otp_secret_length)
|
16
|
+
it 'should be of the expected length' do
|
17
|
+
expect(subject.otp_secret.length).to eq(subject.class.otp_secret_length*8/5)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -125,15 +125,15 @@ RSpec.shared_examples 'two_factor_authenticatable' do
|
|
125
125
|
|
126
126
|
describe '#otp_provisioning_uri' do
|
127
127
|
let(:otp_secret_length) { subject.class.otp_secret_length }
|
128
|
-
let(:account) {
|
128
|
+
let(:account) { 'user@host.example' }
|
129
129
|
let(:issuer) { 'Tinfoil' }
|
130
130
|
|
131
131
|
it 'should return uri with specified account' do
|
132
|
-
expect(subject.otp_provisioning_uri(account)).to match(%r{otpauth://totp/#{CGI.escape(account)}\?secret=\w{#{otp_secret_length}}})
|
132
|
+
expect(subject.otp_provisioning_uri(account)).to match(%r{otpauth://totp/#{CGI.escape(account)}\?secret=\w{#{otp_secret_length*8/5}}})
|
133
133
|
end
|
134
134
|
|
135
135
|
it 'should return uri with issuer option' do
|
136
|
-
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{issuer}:#{CGI.escape(account)}\?.*secret=\w{#{otp_secret_length}}(&|$)})
|
136
|
+
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{issuer}:#{CGI.escape(account)}\?.*secret=\w{#{otp_secret_length*8/5}}(&|$)})
|
137
137
|
expect(subject.otp_provisioning_uri(account, issuer: issuer)).to match(%r{otpauth://totp/#{issuer}:#{CGI.escape(account)}\?.*issuer=#{issuer}(&|$)})
|
138
138
|
end
|
139
139
|
end
|
@@ -17,7 +17,7 @@ RSpec.shared_examples 'two_factor_backupable' do
|
|
17
17
|
|
18
18
|
it 'generates recovery codes of the correct length' do
|
19
19
|
@plaintext_codes.each do |code|
|
20
|
-
expect(code.length).to eq(subject.class.otp_backup_code_length)
|
20
|
+
expect(code.length).to eq(subject.class.otp_backup_code_length*2)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -34,7 +34,7 @@ RSpec.shared_examples 'two_factor_backupable' do
|
|
34
34
|
end
|
35
35
|
|
36
36
|
context 'with existing recovery codes' do
|
37
|
-
let(:old_codes) {
|
37
|
+
let(:old_codes) { ['adam', 'betty', 'charles'] }
|
38
38
|
let(:old_codes_hashed) { old_codes.map { |x| Devise::Encryptor.digest(subject.class, x) } }
|
39
39
|
|
40
40
|
before do
|
@@ -21,7 +21,7 @@ module Devise
|
|
21
21
|
|
22
22
|
def validate_otp(resource)
|
23
23
|
return true unless resource.otp_required_for_login
|
24
|
-
return if params[scope]['otp_attempt'].nil?
|
24
|
+
return if params[scope].nil? || params[scope]['otp_attempt'].nil?
|
25
25
|
resource.validate_and_consume_otp!(params[scope]['otp_attempt'])
|
26
26
|
end
|
27
27
|
end
|
@@ -6,9 +6,6 @@ module Devise
|
|
6
6
|
resource = mapping.to.find_for_database_authentication(authentication_hash)
|
7
7
|
|
8
8
|
if validate(resource) { resource.invalidate_otp_backup_code!(params[scope]['otp_attempt']) }
|
9
|
-
# Devise fails to authenticate invalidated resources, but if we've
|
10
|
-
# gotten here, the object changed (Since we deleted a recovery code)
|
11
|
-
resource.save!
|
12
9
|
super
|
13
10
|
end
|
14
11
|
|
@@ -18,13 +18,17 @@ class TwoFactorAuthenticatableDouble
|
|
18
18
|
|
19
19
|
attr_accessor :consumed_timestep
|
20
20
|
|
21
|
-
def save(
|
21
|
+
def save!(_)
|
22
22
|
# noop for testing
|
23
23
|
true
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
27
|
describe ::Devise::Models::TwoFactorAuthenticatable do
|
28
|
+
it 'should be inserted prior to other devise modules' do
|
29
|
+
expect(Devise::ALL.first).to eq(:two_factor_authenticatable)
|
30
|
+
end
|
31
|
+
|
28
32
|
context 'When included in a class' do
|
29
33
|
subject { TwoFactorAuthenticatableDouble.new }
|
30
34
|
|
data/spec/spec_helper.rb
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: devise-two-factor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 6.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Quinn Wilton
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain:
|
@@ -86,7 +86,7 @@ cert_chain:
|
|
86
86
|
vqIDv6JBG9I16h/HhchntKfM58MI1bNZFBSdZqYOJiL8JIjP8HNIk76Y366ppG29
|
87
87
|
EhBYYg==
|
88
88
|
-----END CERTIFICATE-----
|
89
|
-
date:
|
89
|
+
date: 2024-09-17 00:00:00.000000000 Z
|
90
90
|
dependencies:
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: railties
|
@@ -214,28 +214,17 @@ dependencies:
|
|
214
214
|
- - ">="
|
215
215
|
- !ruby/object:Gem::Version
|
216
216
|
version: '0'
|
217
|
-
-
|
218
|
-
|
219
|
-
|
220
|
-
requirements:
|
221
|
-
- - ">="
|
222
|
-
- !ruby/object:Gem::Version
|
223
|
-
version: '0'
|
224
|
-
type: :development
|
225
|
-
prerelease: false
|
226
|
-
version_requirements: !ruby/object:Gem::Requirement
|
227
|
-
requirements:
|
228
|
-
- - ">="
|
229
|
-
- !ruby/object:Gem::Version
|
230
|
-
version: '0'
|
231
|
-
description: Barebones two-factor authentication with Devise
|
232
|
-
email: engineers@tinfoilsecurity.com
|
217
|
+
description: Devise-Two-Factor is a minimalist extension to Devise which offers support
|
218
|
+
for two-factor authentication through the TOTP scheme.
|
219
|
+
email:
|
233
220
|
executables: []
|
234
221
|
extensions: []
|
235
222
|
extra_rdoc_files: []
|
236
223
|
files:
|
224
|
+
- ".github/dependabot.yml"
|
237
225
|
- ".github/workflows/ci.yml"
|
238
226
|
- ".gitignore"
|
227
|
+
- ".markdownlint.json"
|
239
228
|
- ".rspec"
|
240
229
|
- Appraisals
|
241
230
|
- CHANGELOG.md
|
@@ -244,18 +233,13 @@ files:
|
|
244
233
|
- LICENSE
|
245
234
|
- README.md
|
246
235
|
- Rakefile
|
236
|
+
- SECURITY.md
|
247
237
|
- UPGRADING.md
|
248
238
|
- certs/tinfoil-cacert.pem
|
249
239
|
- certs/tinfoilsecurity-gems-cert.pem
|
250
240
|
- devise-two-factor.gemspec
|
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
241
|
- gemfiles/rails_7.0.gemfile
|
242
|
+
- gemfiles/rails_7.1.gemfile
|
259
243
|
- lib/devise-two-factor.rb
|
260
244
|
- lib/devise_two_factor/models.rb
|
261
245
|
- lib/devise_two_factor/models/two_factor_authenticatable.rb
|
@@ -271,7 +255,7 @@ files:
|
|
271
255
|
- spec/devise/models/two_factor_authenticatable_spec.rb
|
272
256
|
- spec/devise/models/two_factor_backupable_spec.rb
|
273
257
|
- spec/spec_helper.rb
|
274
|
-
homepage: https://github.com/
|
258
|
+
homepage: https://github.com/devise-two-factor/devise-two-factor
|
275
259
|
licenses:
|
276
260
|
- MIT
|
277
261
|
metadata: {}
|
@@ -290,7 +274,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
290
274
|
- !ruby/object:Gem::Version
|
291
275
|
version: '0'
|
292
276
|
requirements: []
|
293
|
-
rubygems_version: 3.
|
277
|
+
rubygems_version: 3.5.11
|
294
278
|
signing_key:
|
295
279
|
specification_version: 4
|
296
280
|
summary: Barebones two-factor authentication with Devise
|
metadata.gz.sig
CHANGED
Binary file
|
data/gemfiles/rails_4.2.gemfile
DELETED
data/gemfiles/rails_5.0.gemfile
DELETED
data/gemfiles/rails_5.1.gemfile
DELETED
data/gemfiles/rails_5.2.gemfile
DELETED
data/gemfiles/rails_6.0.gemfile
DELETED