devise-two-factor 4.1.1 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +3 -18
- data/.github/workflows/push.yml +28 -0
- data/.markdownlint.json +6 -0
- data/Appraisals +9 -34
- data/CHANGELOG.md +59 -0
- data/README.md +110 -67
- data/Rakefile +2 -0
- data/SECURITY.md +5 -0
- data/UPGRADING.md +218 -2
- data/devise-two-factor.gemspec +8 -15
- data/gemfiles/{rails_4.2.gemfile → rails_7.2.gemfile} +2 -2
- data/gemfiles/{rails_5.0.gemfile → rails_8.0.gemfile} +2 -2
- data/gemfiles/{rails_4.1.gemfile → rails_8.1.gemfile} +2 -2
- data/lib/devise-two-factor.rb +12 -5
- data/lib/devise_two_factor/models/two_factor_authenticatable.rb +41 -29
- data/lib/devise_two_factor/models/two_factor_backupable.rb +6 -2
- data/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb +6 -18
- data/lib/devise_two_factor/spec_helpers/two_factor_backupable_shared_examples.rb +53 -24
- data/lib/devise_two_factor/strategies/two_factor_authenticatable.rb +8 -2
- data/lib/devise_two_factor/strategies/two_factor_backupable.rb +6 -4
- 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 +11 -69
- data/spec/devise/models/two_factor_backupable_spec.rb +11 -2
- data/spec/spec_helper.rb +0 -1
- metadata +42 -134
- checksums.yaml.gz.sig +0 -0
- data/certs/tinfoil-cacert.pem +0 -41
- data/certs/tinfoilsecurity-gems-cert.pem +0 -35
- 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
- data/gemfiles/rails_7.0.gemfile +0 -8
- data.tar.gz.sig +0 -0
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48a006cc0a0b85e48b88b8c8521231b65691c513d1c7bf0c55fac2b24fe73d07
|
|
4
|
+
data.tar.gz: e3d988b573e1720e5f1532a8fb161e3399cfd601273cf971957b33a6ad82a489
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee0fd9a8b7042adf26790545788508991ad06bab0191e2d75b3f7119f283f10fa922fa5a51353199539f8a220cb966706820538c22252e5f98a9f976077a8137
|
|
7
|
+
data.tar.gz: ad571291a241e5b7080f62c9ad410f91a0b1f140ff3d978370136212e659670cc8177328c1c306d6957ef925b7789dada8449e2aaa9e4f4096a78f7a97d7f777
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -12,29 +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: ['
|
|
17
|
-
exclude:
|
|
18
|
-
- { ruby: '2.3', rails: '7.0' }
|
|
19
|
-
- { ruby: '2.4', rails: '7.0' }
|
|
20
|
-
- { ruby: '2.5', rails: '7.0' }
|
|
21
|
-
- { ruby: '2.6', rails: '7.0' }
|
|
22
|
-
- { ruby: '2.3', rails: '6.0' }
|
|
23
|
-
- { ruby: '2.3', rails: '6.1' }
|
|
24
|
-
- { ruby: '2.4', rails: '6.0' }
|
|
25
|
-
- { ruby: '2.4', rails: '6.1' }
|
|
26
|
-
- { ruby: '2.7', rails: '4.1' }
|
|
27
|
-
- { ruby: '2.7', rails: '4.2' }
|
|
28
|
-
- { ruby: '3.0', rails: '4.1' }
|
|
29
|
-
- { ruby: '3.0', rails: '4.2' }
|
|
30
|
-
- { ruby: 'truffleruby-head', rails: '4.1' }
|
|
31
|
-
- { ruby: 'truffleruby-head', rails: '4.2' }
|
|
15
|
+
ruby: ['3.2', '3.3', '3.4', '4.0', 'truffleruby-head']
|
|
16
|
+
rails: ['7.2', '8.0', '8.1']
|
|
32
17
|
|
|
33
18
|
name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}
|
|
34
19
|
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
|
|
35
20
|
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/rails_${{ matrix.rails }}.gemfile
|
|
36
21
|
steps:
|
|
37
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@v6
|
|
38
23
|
- name: Set up Ruby
|
|
39
24
|
uses: ruby/setup-ruby@v1
|
|
40
25
|
with:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Push Gem
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- v*
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
deployment:
|
|
10
|
+
name: Push gem to RubyGems.org
|
|
11
|
+
environment: RubyGems
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
|
16
|
+
contents: write # IMPORTANT: this permission is required for `rake release` to push the release tag
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
# Set up
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
|
+
- name: Set up Ruby
|
|
22
|
+
uses: ruby/setup-ruby@v1
|
|
23
|
+
with:
|
|
24
|
+
bundler-cache: true
|
|
25
|
+
ruby-version: ruby
|
|
26
|
+
|
|
27
|
+
# Release
|
|
28
|
+
- uses: rubygems/release-gem@v1
|
data/.markdownlint.json
ADDED
data/Appraisals
CHANGED
|
@@ -1,39 +1,14 @@
|
|
|
1
|
-
appraise "rails-
|
|
2
|
-
gem 'railties', '~>
|
|
3
|
-
gem 'activesupport', '~>
|
|
1
|
+
appraise "rails-7.2" do
|
|
2
|
+
gem 'railties', '~> 7.2.0'
|
|
3
|
+
gem 'activesupport', '~> 7.2.0'
|
|
4
4
|
end
|
|
5
5
|
|
|
6
|
-
appraise "rails-
|
|
7
|
-
gem 'railties', '~>
|
|
8
|
-
gem 'activesupport', '~>
|
|
6
|
+
appraise "rails-8.0" do
|
|
7
|
+
gem 'railties', '~> 8.0.0'
|
|
8
|
+
gem 'activesupport', '~> 8.0.0'
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
appraise "rails-
|
|
12
|
-
gem 'railties', '
|
|
13
|
-
gem 'activesupport', '
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
appraise "rails-5.1" do
|
|
17
|
-
gem 'railties', '~> 5.1'
|
|
18
|
-
gem 'activesupport', '~> 5.1'
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
appraise "rails-5.2" do
|
|
22
|
-
gem 'railties', '~> 5.2'
|
|
23
|
-
gem 'activesupport', '~> 5.2'
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
appraise "rails-6.0" do
|
|
27
|
-
gem 'railties', '~> 6.0'
|
|
28
|
-
gem 'activesupport', '~> 6.0'
|
|
29
|
-
end
|
|
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'
|
|
11
|
+
appraise "rails-8.1" do
|
|
12
|
+
gem 'railties', '8.1.0'
|
|
13
|
+
gem 'activesupport', '8.1.0'
|
|
39
14
|
end
|
data/CHANGELOG.md
CHANGED
|
@@ -2,18 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 6.4.0
|
|
6
|
+
|
|
7
|
+
- Remove upper limit on Devise version (allows v5) from gemspec
|
|
8
|
+
|
|
9
|
+
## 6.3.1
|
|
10
|
+
|
|
11
|
+
- Fix DB-adapter-specific integration issue with backupable shared example
|
|
12
|
+
- Drop support for EOL Rails versions 7.0 and 7.1
|
|
13
|
+
|
|
14
|
+
## 6.3.0
|
|
15
|
+
|
|
16
|
+
- Fixed timing to be consistent when Devise paranoid mode is active.
|
|
17
|
+
|
|
18
|
+
## 6.2.0
|
|
19
|
+
|
|
20
|
+
- Rails 8.1 support
|
|
21
|
+
|
|
22
|
+
## 6.1.0
|
|
23
|
+
|
|
24
|
+
- Rails 8 support
|
|
25
|
+
|
|
26
|
+
## 6.0.0
|
|
27
|
+
|
|
28
|
+
**Breaking Changes**
|
|
29
|
+
- `otp_secret_length` and `otp_backup_code_length` options have changed to be the number of random bytes that are generated. See [UPGRADING.md](UPGRADING.md).
|
|
30
|
+
- `consume_otp!` and `invalidate_otp_backup_code!` now call `save!` instead of `save`. See [UPGRADING.md](UPGRADING.md).
|
|
31
|
+
|
|
32
|
+
## 5.1.0
|
|
33
|
+
|
|
34
|
+
- Remove faker dev dependency
|
|
35
|
+
- Insert two_factor_authenticatable at the top of the devise module list
|
|
36
|
+
- README and CI improvements
|
|
37
|
+
|
|
38
|
+
## 5.0.0
|
|
39
|
+
|
|
40
|
+
**Breaking Changes**
|
|
41
|
+
- 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!
|
|
42
|
+
- Rails 7 is now required.
|
|
43
|
+
|
|
44
|
+
## 4.1.0 / 4.1.1
|
|
45
|
+
|
|
46
|
+
- Add support for attr_encrypted v4
|
|
47
|
+
|
|
5
48
|
## 4.0.2
|
|
49
|
+
|
|
6
50
|
- Add Rails 7.0 support
|
|
7
51
|
- Renew signing certificate
|
|
8
52
|
- Use `after` option of TOTP#verify for additional timestamp verification
|
|
9
53
|
|
|
10
54
|
## 4.0.1
|
|
55
|
+
|
|
11
56
|
- Convert CI from Travis CI to Github Actions ([#198](https://github.com/tinfoil/devise-two-factor/pull/198))
|
|
12
57
|
- Fix ActiveSupport::Testing::TimeHelpers require in shared examples ([#191](https://github.com/tinfoil/devise-two-factor/pull/191))
|
|
13
58
|
- Accept whitespace in provided codes ([#195](https://github.com/tinfoil/devise-two-factor/pull/195))
|
|
14
59
|
- Add Truffleruby head to CI ([#200](https://github.com/tinfoil/devise-two-factor/pull/200))
|
|
15
60
|
|
|
16
61
|
## 4.0.0
|
|
62
|
+
|
|
17
63
|
- [breaking] Drop support for Ruby <= 2.2
|
|
18
64
|
- Update ROTP
|
|
19
65
|
- Add Rails 6.1 support
|
|
@@ -22,20 +68,25 @@
|
|
|
22
68
|
- Bugfixes & cleanup
|
|
23
69
|
|
|
24
70
|
## 3.1.0
|
|
71
|
+
|
|
25
72
|
- Add Rails 6.0 support
|
|
26
73
|
- New gem signing certificate
|
|
27
74
|
- Fix paranoid-mode being ignored
|
|
28
75
|
|
|
29
76
|
## 3.0.3
|
|
77
|
+
|
|
30
78
|
- Add Rails 5.2 support
|
|
31
79
|
|
|
32
80
|
## 3.0.2
|
|
81
|
+
|
|
33
82
|
- Add Rails 5.1 support
|
|
34
83
|
|
|
35
84
|
## 3.0.1
|
|
85
|
+
|
|
36
86
|
- Qualify call to rspec shared_examples
|
|
37
87
|
|
|
38
88
|
## 3.0.0
|
|
89
|
+
|
|
39
90
|
See `UPGRADING.md` for specific help with breaking changes from 2.x to 3.0.0.
|
|
40
91
|
|
|
41
92
|
- Adds support for Devise 4.
|
|
@@ -43,33 +94,41 @@ See `UPGRADING.md` for specific help with breaking changes from 2.x to 3.0.0.
|
|
|
43
94
|
- Blocks the use of attr_encrypted 2.x. There was a significant vulnerability in the encryption implementation in attr_encrypted 2.x, and that version of the gem should not be used.
|
|
44
95
|
|
|
45
96
|
## 2.2.0
|
|
97
|
+
|
|
46
98
|
- Use 192 bits, not 1024, as a secret key length. RFC 4226 recommends a minimum length of 128 bits and a recommended length of 160 bits. Google Authenticator doesn't accept 160 bit keys.
|
|
47
99
|
|
|
48
100
|
## 2.1.0
|
|
101
|
+
|
|
49
102
|
- Return false if OTP value is nil, instead of an ROTP exception.
|
|
50
103
|
|
|
51
104
|
## 2.0.1
|
|
105
|
+
|
|
52
106
|
No user-facing changes.
|
|
53
107
|
|
|
54
108
|
## 2.0.0
|
|
109
|
+
|
|
55
110
|
See `UPGRADING.md` for specific help with breaking changes from 1.x to 2.0.0.
|
|
56
111
|
|
|
57
112
|
- Replace `valid_otp?` method with `validate_and_consume_otp!`.
|
|
58
113
|
- Disallow subsequent OTPs once validated via timesteps.
|
|
59
114
|
|
|
60
115
|
## 1.1.0
|
|
116
|
+
|
|
61
117
|
- Removes runtimez activemodel dependency.
|
|
62
118
|
- Uses `Devise::Encryptor` instead of `Devise.bcrypt`, which is deprecated.
|
|
63
119
|
- Bump `rotp` dependency to 2.x.
|
|
64
120
|
|
|
65
121
|
## 1.0.2
|
|
122
|
+
|
|
66
123
|
- Makes Railties the only requirement for Rails generators.
|
|
67
124
|
- Explicitly check that the `otp_attempt` param is not nil in order to avoid 'ROTP only verifies strings' exceptions.
|
|
68
125
|
- Adding warning about recoverable devise strategy and automatic `sign_in` after a password reset.
|
|
69
126
|
- Loosen dependency version requirements for rotp, devise, and attr_encrypted.
|
|
70
127
|
|
|
71
128
|
## 1.0.1
|
|
129
|
+
|
|
72
130
|
- Add version requirements for dependencies.
|
|
73
131
|
|
|
74
132
|
## 1.0.0
|
|
133
|
+
|
|
75
134
|
- Initial release.
|
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
|
-

|
|
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,89 +10,109 @@ 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
|
+
|
|
22
|
+
Devise-Two-Factor doesn't require much to get started, but there are two prerequisites before you can start using it in your application:
|
|
23
|
+
|
|
24
|
+
1. A Rails application with [devise](https://github.com/heartcombo/devise) installed
|
|
25
|
+
1. Secrets configured for ActiveRecord encrypted attributes
|
|
23
26
|
|
|
24
27
|
First, you'll need a Rails application setup with Devise. Visit the Devise [homepage](https://github.com/plataformatec/devise) for instructions.
|
|
25
28
|
|
|
26
|
-
|
|
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.
|
|
27
30
|
|
|
28
|
-
```
|
|
29
|
-
|
|
31
|
+
```bash
|
|
32
|
+
# Generates a random key set and outputs it to stdout
|
|
33
|
+
./bin/rails db:encryption:init
|
|
30
34
|
```
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```ruby
|
|
35
|
-
devise :two_factor_authenticatable,
|
|
36
|
-
:otp_secret_encryption_key => ENV['YOUR_ENCRYPTION_KEY_HERE']
|
|
36
|
+
You can load the key set using Rails' credentials.
|
|
37
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
|
|
38
42
|
```
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
To learn more about credentials run `./bin/rails credentials:help`.
|
|
45
|
+
|
|
46
|
+
Alternatively, you can configure your application with environment variables rather than Rails' credentials.
|
|
41
47
|
|
|
42
48
|
```ruby
|
|
43
|
-
|
|
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']
|
|
44
54
|
```
|
|
45
55
|
|
|
46
|
-
|
|
56
|
+
Add Devise-Two-Factor to your Gemfile with:
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
```ruby
|
|
59
|
+
# Gemfile
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* encrypted_otp_secret_salt
|
|
53
|
-
* consumed_timestep
|
|
54
|
-
* otp_required_for_login
|
|
61
|
+
gem 'devise-two-factor'
|
|
62
|
+
```
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
There is a generator which automates most of the setup:
|
|
57
65
|
|
|
58
|
-
```
|
|
59
|
-
|
|
66
|
+
```bash
|
|
67
|
+
# MODEL is the name of the model you wish to configure devise_two_factor e.g. User or Admin
|
|
68
|
+
./bin/rails generate devise_two_factor MODEL
|
|
60
69
|
```
|
|
61
70
|
|
|
62
|
-
|
|
71
|
+
Where `MODEL` is the name of the model you wish to add two-factor functionality to (for example `user`)
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
This generator will:
|
|
65
74
|
|
|
66
|
-
|
|
75
|
+
1. Create a new migration which adds a few columns to the specified model:
|
|
67
76
|
|
|
68
|
-
```ruby
|
|
69
|
-
|
|
77
|
+
```ruby
|
|
78
|
+
add_column :users, :otp_secret, :string
|
|
79
|
+
add_column :users, :consumed_timestep, :integer
|
|
80
|
+
add_column :users, :otp_required_for_login, :boolean
|
|
81
|
+
```
|
|
70
82
|
|
|
71
|
-
|
|
83
|
+
1. Edit `app/models/MODEL.rb` (where MODEL is your model name):
|
|
84
|
+
* add the `:two_factor_authenticatable` devise module
|
|
85
|
+
* remove the `:database_authenticatable` devise module, if present; having both modules enabled will lead to issues described below.
|
|
86
|
+
1. Add a Warden config block to your Devise initializer, which enables the strategies required for two-factor authentication.
|
|
72
87
|
|
|
73
|
-
|
|
88
|
+
Remember to apply the new migration after you run the generator:
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
end
|
|
90
|
+
```bash
|
|
91
|
+
./bin/rails db:migrate
|
|
78
92
|
```
|
|
79
93
|
|
|
80
|
-
|
|
94
|
+
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
95
|
|
|
82
96
|
```ruby
|
|
83
|
-
|
|
97
|
+
# app/controllers/application_controller.rb
|
|
84
98
|
|
|
85
|
-
|
|
99
|
+
before_action :configure_permitted_parameters, if: :devise_controller?
|
|
86
100
|
|
|
87
|
-
|
|
101
|
+
# ...
|
|
88
102
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
protected
|
|
104
|
+
|
|
105
|
+
def configure_permitted_parameters
|
|
106
|
+
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
|
107
|
+
end
|
|
92
108
|
```
|
|
93
109
|
|
|
94
|
-
|
|
110
|
+
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. `:two_factor_authenticatable` includes all of `:database_authenticatable`'s functionality; it will still allow login without two-factor authentication until you enable it on your model's records with `otp_required_for_login`.
|
|
111
|
+
|
|
112
|
+
**Loading both `:database_authenticatable` and `:two_factor_authenticatable` in a model is a security issue.** It will allow users to bypass two-factor authentication regardless of how `otp_required_for_login` is set due to the way Warden handles cascading strategies!
|
|
95
113
|
|
|
96
114
|
## Designing Your Workflow
|
|
115
|
+
|
|
97
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.
|
|
98
117
|
|
|
99
118
|
There are two key workflows you'll have to think about:
|
|
@@ -103,8 +122,8 @@ There are two key workflows you'll have to think about:
|
|
|
103
122
|
|
|
104
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).
|
|
105
124
|
|
|
106
|
-
|
|
107
125
|
### Logging In
|
|
126
|
+
|
|
108
127
|
Logging in with two-factor authentication works extremely similarly to regular database authentication in Devise. The `TwoFactorAuthenticatable` strategy accepts three parameters:
|
|
109
128
|
|
|
110
129
|
1. email
|
|
@@ -114,11 +133,13 @@ Logging in with two-factor authentication works extremely similarly to regular d
|
|
|
114
133
|
These parameters can be submitted to the standard Devise login route, and the strategy will handle the authentication of the user for you.
|
|
115
134
|
|
|
116
135
|
### Disabling Automatic Login After Password Resets
|
|
136
|
+
|
|
117
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.
|
|
118
138
|
|
|
119
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`).
|
|
120
140
|
|
|
121
141
|
### Enabling Two-Factor Authentication
|
|
142
|
+
|
|
122
143
|
Enabling two-factor authentication for a user is easy. For example, if my user model were named User, I could do the following:
|
|
123
144
|
|
|
124
145
|
```ruby
|
|
@@ -134,10 +155,7 @@ At Tinfoil Security, we opted to use the excellent [rqrcode-rails3](https://gith
|
|
|
134
155
|
If you decide to do this you'll need to generate a URI to act as the source for the QR code. This can be done using the `User#otp_provisioning_uri` method.
|
|
135
156
|
|
|
136
157
|
```ruby
|
|
137
|
-
issuer
|
|
138
|
-
label = "#{issuer}:#{current_user.email}"
|
|
139
|
-
|
|
140
|
-
current_user.otp_provisioning_uri(label, issuer: issuer)
|
|
158
|
+
current_user.otp_provisioning_uri(current_user.email, issuer: 'Your App')
|
|
141
159
|
|
|
142
160
|
# > "otpauth://totp/Your%20App:user@example.com?secret=[otp_secret]&issuer=Your+App"
|
|
143
161
|
```
|
|
@@ -149,6 +167,7 @@ current_user.current_otp
|
|
|
149
167
|
```
|
|
150
168
|
|
|
151
169
|
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`.
|
|
170
|
+
|
|
152
171
|
```ruby
|
|
153
172
|
Devise.otp_allowed_drift = 240 # value in seconds
|
|
154
173
|
Devise.setup do |config|
|
|
@@ -165,13 +184,27 @@ However you decide to handle enrollment, there are a few important consideration
|
|
|
165
184
|
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.
|
|
166
185
|
|
|
167
186
|
### Filtering sensitive parameters from the logs
|
|
187
|
+
|
|
168
188
|
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`:
|
|
169
189
|
|
|
170
190
|
```ruby
|
|
171
191
|
Rails.application.config.filter_parameters += [:otp_attempt]
|
|
172
192
|
```
|
|
173
193
|
|
|
194
|
+
### Preventing Brute-Force Attacks
|
|
195
|
+
|
|
196
|
+
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:
|
|
197
|
+
|
|
198
|
+
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.
|
|
199
|
+
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).
|
|
200
|
+
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.
|
|
201
|
+
|
|
202
|
+
#### Acknowledgements
|
|
203
|
+
|
|
204
|
+
Thank you to Christian Reitter (Radically Open Security) and Chris MacNaughton (Centauri Solutions) for reporting the issue.
|
|
205
|
+
|
|
174
206
|
## Backup Codes
|
|
207
|
+
|
|
175
208
|
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.
|
|
176
209
|
|
|
177
210
|
To install it, you need to add the `:two_factor_backupable` directive to your model.
|
|
@@ -186,17 +219,39 @@ You'll also be required to enable the `:two_factor_backupable` strategy, by addi
|
|
|
186
219
|
manager.default_strategies(:scope => :user).unshift :two_factor_backupable
|
|
187
220
|
```
|
|
188
221
|
|
|
189
|
-
|
|
222
|
+
### Migration
|
|
223
|
+
|
|
224
|
+
The final installation step may be dependent on your version of Rails.
|
|
225
|
+
|
|
226
|
+
#### PostgreSQL
|
|
190
227
|
|
|
191
228
|
```ruby
|
|
192
229
|
class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration
|
|
193
230
|
def change
|
|
194
|
-
# Change type from :string to :text if using MySQL database
|
|
195
231
|
add_column :users, :otp_backup_codes, :string, array: true
|
|
196
232
|
end
|
|
197
233
|
end
|
|
198
234
|
```
|
|
199
235
|
|
|
236
|
+
#### MySQL, SQL Server, other databases without an array string type
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
# migration
|
|
240
|
+
class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration
|
|
241
|
+
def change
|
|
242
|
+
add_column :users, :otp_backup_codes, :text
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# model
|
|
247
|
+
class User < ApplicationRecord
|
|
248
|
+
devise :two_factor_backupable
|
|
249
|
+
serialize :otp_backup_codes, Array
|
|
250
|
+
end
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Generation
|
|
254
|
+
|
|
200
255
|
You can then generate backup codes for a user:
|
|
201
256
|
|
|
202
257
|
```ruby
|
|
@@ -214,23 +269,8 @@ devise :two_factor_backupable, otp_backup_code_length: 32,
|
|
|
214
269
|
otp_number_of_backup_codes: 10
|
|
215
270
|
```
|
|
216
271
|
|
|
217
|
-
### Help! I'm not using Rails 4.0!
|
|
218
|
-
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).
|
|
219
|
-
|
|
220
|
-
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:
|
|
221
|
-
|
|
222
|
-
```ruby
|
|
223
|
-
class AddTwoFactorBackupCodesToUsers < ActiveRecord::Migration
|
|
224
|
-
def change
|
|
225
|
-
# Change type from :string_array to :text_array if using MySQL database
|
|
226
|
-
add_column :users, :otp_backup_codes, :string_array
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
Now just continue with the setup in the previous section, skipping the generator step.
|
|
232
|
-
|
|
233
272
|
## Testing
|
|
273
|
+
|
|
234
274
|
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:
|
|
235
275
|
|
|
236
276
|
```ruby
|
|
@@ -241,6 +281,7 @@ it_behaves_like "two_factor_backupable"
|
|
|
241
281
|
```
|
|
242
282
|
|
|
243
283
|
## Troubleshooting
|
|
284
|
+
|
|
244
285
|
If you are using Rails 4.x and Ruby >= 2.7, you may get an error like
|
|
245
286
|
|
|
246
287
|
```
|
|
@@ -250,9 +291,11 @@ Failure/Error: require 'devise'
|
|
|
250
291
|
NoMethodError:
|
|
251
292
|
undefined method `new' for BigDecimal:Class
|
|
252
293
|
```
|
|
294
|
+
|
|
253
295
|
see https://github.com/ruby/bigdecimal#which-version-should-you-select and https://github.com/ruby/bigdecimal/issues/127
|
|
254
296
|
for more details, but you should be able to solve this
|
|
255
297
|
by explicitly requiring an older version of bigdecimal in your gemfile like
|
|
256
|
-
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
257
300
|
gem "bigdecimal", "~> 1.4"
|
|
258
301
|
```
|
data/Rakefile
CHANGED