devise-passwordless 0.7.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +397 -42
- data/UPGRADING.md +143 -0
- data/{lib/generators/devise/passwordless/templates/magic_links_controller.rb.erb → app/controllers/devise/magic_links_controller.rb} +2 -5
- data/app/controllers/devise/passwordless/sessions_controller.rb +49 -0
- data/app/mailers/devise/passwordless/mailer.rb +15 -0
- data/lib/devise/hooks/magic_link_authenticatable.rb +10 -0
- data/lib/devise/models/magic_link_authenticatable.rb +79 -9
- data/lib/devise/monkeypatch.rb +82 -0
- data/lib/devise/passwordless/login_token.rb +7 -52
- data/lib/devise/passwordless/rails.rb +25 -0
- data/lib/devise/passwordless/routing.rb +11 -0
- data/lib/devise/passwordless/tokenizers/message_encryptor_tokenizer.rb +66 -0
- data/lib/devise/passwordless/tokenizers/signed_global_id_tokenizer.rb +20 -0
- data/lib/devise/passwordless/version.rb +3 -1
- data/lib/devise/passwordless.rb +24 -0
- data/lib/devise/strategies/magic_link_authenticatable.rb +5 -25
- data/lib/generators/devise/passwordless/install_generator.rb +8 -11
- metadata +46 -17
- data/.github/workflows/test.yml +0 -45
- data/.gitignore +0 -16
- data/.rspec +0 -4
- data/.travis.yml +0 -7
- data/Gemfile +0 -13
- data/Rakefile +0 -6
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/devise-passwordless.gemspec +0 -41
- data/lib/devise/passwordless/mailer.rb +0 -12
- data/lib/generators/devise/passwordless/templates/sessions_controller.rb.erb +0 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c2ccc142bc114ca58125c72ea17c9646a6bd602d2a70e99de49b6412df12950
|
4
|
+
data.tar.gz: 7682b97e852e56559ef5babf8bf4f9e0c323083a32b3e16f8d932976e3204aea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4ac233d2eff38815cca3fadbe0c5b84e919b6129e7e763f30fb18cb5e75b095d10362126e8448fed612dad292463a997d82b36c8ffcb8b95252aacc20db5e46d
|
7
|
+
data.tar.gz: 4feb6345e80a8f2a0ea37527f7926cbff412e70a6bfda8a35166cc686f7eef0bb28d8a63a3702c59ac5ccbba308ca2386ec86e083d748e57bd6fdcdd176f49b1
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
## 1.0.0
|
2
|
+
|
3
|
+
### Enhancements
|
4
|
+
|
5
|
+
* Tokenization encoding/decoding is now fully customizable
|
6
|
+
* Tokenizer encoding now supports extra metadata ([#27] - thanks [@fastjames] and [@elucid]!)
|
7
|
+
* Tokenizer encoding now supports `:expires_at` option ([#19], [#21] - thanks [@joeyparis] / [@JoeyLeadJig] and [@bvsatyaram]!)
|
8
|
+
* Turbo is now properly supported ([#23], [#33] - thanks [@iainbeeston] and [@til]!)
|
9
|
+
* Signed GlobalID tokenization supported ([#22])
|
10
|
+
* Concurrent use of password auth (`:database_authenticatable` strategy) now supported ([#13] - thanks [@fschwahn]!)
|
11
|
+
* `Devise.paranoid` is now respected with new ambiguous messaging i18n option `:magic_link_sent_paranoid` following ([#36] - thanks [@cbldev]!)
|
12
|
+
* More thorough integration testing using a dummy Rails app
|
13
|
+
* Added a Rails engine to solve loading issues and tidy up file structuring
|
14
|
+
* `Passwordless::SessionsController` now uses gem source instead of needing to be generated from a template
|
15
|
+
* `MagicLinksController` no longer requires a weird `routes.rb` entry to work
|
16
|
+
* `MagicLinksController` now uses gem source instead of needing to be generated from a template
|
17
|
+
* `magic_link_(path|url)` view helpers are now implemented for all resources (cleans up mailer view template)
|
18
|
+
* Users will be redirected after magic link is sent (customized using `after_magic_link_sent_path_for`)
|
19
|
+
* A warning will be logged if Rails's `filter_parameters` doesn't filter `:token`s from request logs
|
20
|
+
|
21
|
+
### Bugfixes
|
22
|
+
|
23
|
+
* Autoloading issues related to `uninitialized constant` (e.g.
|
24
|
+
`Devise::Passwordless::Mailer`) should now be fixed
|
25
|
+
|
26
|
+
|
27
|
+
[@bvsatyaram]: https://github.com/bvsatyaram
|
28
|
+
[@cbldev]: https://github.com/cbldev
|
29
|
+
[@fastjames]: https://github.com/fastjames
|
30
|
+
[@fschwahn]: https://github.com/fschwahn
|
31
|
+
[@elucid]: https://github.com/elucid
|
32
|
+
[@iainbeeston]: https://github.com/iainbeeston
|
33
|
+
[@joeyparis]: https://github.com/joeyparis
|
34
|
+
[@JoeyLeadJig]: https://github.com/JoeyLeadJig
|
35
|
+
[@til]: https://github.com/til
|
36
|
+
|
37
|
+
[#13]: https://github.com/abevoelker/devise-passwordless/issues/13
|
38
|
+
[#19]: https://github.com/abevoelker/devise-passwordless/pull/19
|
39
|
+
[#21]: https://github.com/abevoelker/devise-passwordless/pull/21
|
40
|
+
[#22]: https://github.com/abevoelker/devise-passwordless/issues/22
|
41
|
+
[#23]: https://github.com/abevoelker/devise-passwordless/pull/23
|
42
|
+
[#27]: https://github.com/abevoelker/devise-passwordless/pull/27
|
43
|
+
[#33]: https://github.com/abevoelker/devise-passwordless/pull/33
|
44
|
+
[#36]: https://github.com/abevoelker/devise-passwordless/pull/36
|
data/README.md
CHANGED
@@ -1,14 +1,25 @@
|
|
1
1
|
# Devise::Passwordless
|
2
2
|
|
3
|
-
A passwordless
|
3
|
+
A passwordless login strategy for [Devise] using emailed magic links
|
4
4
|
|
5
5
|
## Features
|
6
6
|
|
7
|
-
* No
|
8
|
-
*
|
7
|
+
* No passwords - users receive magic link emails to register / sign-in
|
8
|
+
* No database changes needed - magic links are stateless tokens
|
9
|
+
* [Choose your token encoding algorithm or easily write your own](#tokenizers)
|
10
|
+
* [Can be combined with traditional password authentication in the same model](#combining-password-and-passwordless-auth-in-the-same-model)
|
9
11
|
* [Supports multiple user (resource) types](#multiple-user-resource-types)
|
10
12
|
* All the goodness of Devise!
|
11
13
|
|
14
|
+
## 0.x to 1.0 Upgrade
|
15
|
+
|
16
|
+
⭐ The 1.0 release includes significant breaking changes! ⭐
|
17
|
+
|
18
|
+
If you're upgrading from 0.x to 1.0, read [the upgrade guide][] for
|
19
|
+
a list of changes you'll need to make.
|
20
|
+
|
21
|
+
[the upgrade guide]: https://github.com/abevoelker/devise-passwordless/blob/master/UPGRADING.md
|
22
|
+
|
12
23
|
## Installation
|
13
24
|
|
14
25
|
First, install and set up [Devise][].
|
@@ -35,7 +46,7 @@ See the [customization section](#customization) for details on what gets install
|
|
35
46
|
|
36
47
|
## Usage
|
37
48
|
|
38
|
-
This gem adds a `:magic_link_authenticatable` strategy that can be used in your Devise models for passwordless authentication. This strategy plays well with most other Devise strategies (see [*
|
49
|
+
This gem adds a `:magic_link_authenticatable` strategy that can be used in your Devise models for passwordless authentication. This strategy plays well with most other Devise strategies (see [*compatibility with other Devise strategies*](#compatibility-with-other-devise-strategies)).
|
39
50
|
|
40
51
|
For example, if your Devise model is User, enable the strategy like this:
|
41
52
|
|
@@ -46,48 +57,43 @@ class User < ApplicationRecord
|
|
46
57
|
end
|
47
58
|
```
|
48
59
|
|
49
|
-
Then,
|
60
|
+
Then, change your route to process sessions using the passwordless sessions controller:
|
50
61
|
|
51
62
|
```ruby
|
52
63
|
# config/routes.rb
|
53
64
|
Rails.application.routes.draw do
|
54
65
|
devise_for :users,
|
55
66
|
controllers: { sessions: "devise/passwordless/sessions" }
|
56
|
-
devise_scope :user do
|
57
|
-
get "/users/magic_link",
|
58
|
-
to: "devise/passwordless/magic_links#show",
|
59
|
-
as: "users_magic_link"
|
60
|
-
end
|
61
67
|
end
|
62
68
|
```
|
63
69
|
|
64
|
-
Finally,
|
70
|
+
Finally, we need to update Devise's views to remove references to passwords. We will assume you're using the standard Devise views for all your registrations and logins; if you need to support multiple Devise models, some with passwordless login and some with password login, then jump down to the [multiple users section below](#multiple-user-resource-types).
|
71
|
+
|
72
|
+
First, ensure you have Devise views generated for your project under `app/views/devise`. If not, you can generate them with:
|
65
73
|
|
66
|
-
|
74
|
+
```
|
75
|
+
rails generate devise:views
|
76
|
+
```
|
77
|
+
|
78
|
+
Then, delete these files and directories:
|
67
79
|
|
68
80
|
```
|
69
|
-
app/views/devise/passwords
|
70
|
-
app/views/devise/mailer/password_change.html.erb
|
71
|
-
app/views/devise/mailer/reset_password_instructions.html.erb
|
81
|
+
rm -rf app/views/devise/passwords
|
82
|
+
rm -f app/views/devise/mailer/password_change.html.erb
|
83
|
+
rm -f app/views/devise/mailer/reset_password_instructions.html.erb
|
72
84
|
```
|
73
85
|
|
74
|
-
|
86
|
+
Then, edit these files to remove password references:
|
75
87
|
|
76
|
-
*
|
88
|
+
* app/views/devise/registrations/new.html.erb
|
77
89
|
* Delete fields `:password` and `:password_confirmation`
|
78
|
-
*
|
90
|
+
* app/views/devise/registrations/edit.html.erb
|
79
91
|
* Delete fields `:password`, `:password_confirmation`, `:current_password`
|
80
|
-
*
|
92
|
+
* app/views/devise/sessions/new.html.erb
|
81
93
|
* Delete field `:password`
|
82
94
|
|
83
|
-
|
84
|
-
|
85
|
-
You can very easily send a magic link at any point like so:
|
86
|
-
|
87
|
-
```ruby
|
88
|
-
remember_me = true
|
89
|
-
User.send_magic_link(remember_me)
|
90
|
-
```
|
95
|
+
That's it! 🎉 Now check out the customization section so that you
|
96
|
+
may change the default configuration to better match your needs.
|
91
97
|
|
92
98
|
## Customization
|
93
99
|
|
@@ -96,10 +102,14 @@ Configuration options are stored in Devise's initializer at `config/initializers
|
|
96
102
|
```ruby
|
97
103
|
# ==> Configuration for :magic_link_authenticatable
|
98
104
|
|
99
|
-
# Need to use a custom Devise mailer in order to send magic links
|
100
|
-
|
105
|
+
# Need to use a custom Devise mailer in order to send magic links.
|
106
|
+
# If you're already using a custom mailer just have it inherit from
|
107
|
+
# Devise::Passwordless::Mailer instead of Devise::Mailer
|
101
108
|
config.mailer = "Devise::Passwordless::Mailer"
|
102
109
|
|
110
|
+
# Which algorithm to use for tokenizing magic links. See README for descriptions
|
111
|
+
config.passwordless_tokenizer = "SignedGlobalIDTokenizer"
|
112
|
+
|
103
113
|
# Time period after a magic login link is sent out that it will be valid for.
|
104
114
|
# config.passwordless_login_within = 20.minutes
|
105
115
|
|
@@ -115,6 +125,32 @@ config.mailer = "Devise::Passwordless::Mailer"
|
|
115
125
|
# config.passwordless_expire_old_tokens_on_sign_in = false
|
116
126
|
```
|
117
127
|
|
128
|
+
Most config options can be set on a per-model basis. For instance,
|
129
|
+
you can use different tokenizers across different models like so:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
# app/models/user.rb
|
133
|
+
class User < ApplicationRecord
|
134
|
+
devise :magic_link_authenticatable
|
135
|
+
|
136
|
+
def self.passwordless_tokenizer
|
137
|
+
"SignedGlobalIDTokenizer"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# app/models/another_user.rb
|
142
|
+
class AnotherUser < ApplicationRecord
|
143
|
+
devise :magic_link_authenticatable
|
144
|
+
|
145
|
+
def self.passwordless_tokenizer
|
146
|
+
"MessageEncryptorTokenizer"
|
147
|
+
end
|
148
|
+
def self.passwordless_login_within
|
149
|
+
1.hour
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
118
154
|
To customize the magic link email subject line and other status and error messages, modify these values in `config/locales/devise.en.yml`:
|
119
155
|
|
120
156
|
```yaml
|
@@ -123,6 +159,7 @@ en:
|
|
123
159
|
passwordless:
|
124
160
|
not_found_in_database: "Could not find a user for that email address"
|
125
161
|
magic_link_sent: "A login link has been sent to your email address. Please follow the link to log in to your account."
|
162
|
+
magic_link_sent_paranoid: "If your account exists, you will receive an email with a login link. Please follow the link to log in to your account."
|
126
163
|
failure:
|
127
164
|
magic_link_invalid: "Invalid or expired login link."
|
128
165
|
mailer:
|
@@ -130,9 +167,171 @@ en:
|
|
130
167
|
subject: "Here's your magic login link ✨"
|
131
168
|
```
|
132
169
|
|
170
|
+
**Note**: If [Devise's paranoid mode][] is enabled in your Devise initializer, the
|
171
|
+
`:magic_link_sent_paranoid` message will be used both when a user account exists
|
172
|
+
and when it does not exist to prevent account enumeration vulnerabilities. If
|
173
|
+
paranoid mode is disabled, then `:magic_link_sent` will be used for existing
|
174
|
+
accounts, and `:not_found_in_database` when no account was found.
|
175
|
+
|
176
|
+
[Devise's paranoid mode]: https://github.com/heartcombo/devise/wiki/How-To:-Using-paranoid-mode,-avoid-user-enumeration-on-registerable
|
177
|
+
|
133
178
|
To customize the magic link email body, edit `app/views/devise/mailer/magic_link.html.erb`
|
134
179
|
|
135
|
-
|
180
|
+
## Manually creating and sending magic links
|
181
|
+
|
182
|
+
Magic links are created and sent normally using Devise's views for sign-in and registration, but you can create them manually as well.
|
183
|
+
|
184
|
+
To send a magic link email, do this:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
user = User.last
|
188
|
+
user.send_magic_link
|
189
|
+
# additional options are passed through to Devise's mailer logic
|
190
|
+
user.send_magic_link(remember_me: true, subject: "Custom email subject", "X-Entity-Ref-ID": SecureRandom.uuid)
|
191
|
+
```
|
192
|
+
|
193
|
+
If you only need to generate the token portion of a magic link, you can do this:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
# see the tokenizer's #encode method for all supported keyword options
|
197
|
+
token = user.encode_passwordless_token(expires_at: 2.hours.from_now)
|
198
|
+
```
|
199
|
+
|
200
|
+
Or, to generate the full magic link URL, use this URL view helper:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
user_magic_link_url(
|
204
|
+
user: {
|
205
|
+
email: user.email,
|
206
|
+
token: token,
|
207
|
+
remember_me: true
|
208
|
+
}
|
209
|
+
)
|
210
|
+
```
|
211
|
+
|
212
|
+
## Redirecting after magic link is sent
|
213
|
+
|
214
|
+
After a user enters their email on the sign-in page, and a magic link is sent, the user
|
215
|
+
will be redirected back to the `:root` path of the application.
|
216
|
+
|
217
|
+
To provide a custom redirect location, you can write a custom
|
218
|
+
`after_magic_link_sent_path_for` helper, similar to
|
219
|
+
[how Devise's `after_sign_in_path_for` helper works][after_sign_in_path_for]:
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
class ApplicationController < ActionController::Base
|
223
|
+
def after_magic_link_sent_path_for(resource_or_scope)
|
224
|
+
"/foo"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
[after_sign_in_path_for]: https://github.com/heartcombo/devise/wiki/How-To:-Redirect-back-to-current-page-after-sign-in,-sign-out,-sign-up,-update
|
230
|
+
|
231
|
+
If you need to have different paths for multiple different types of resources,
|
232
|
+
you can write something like this:
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
class ApplicationController < ActionController::Base
|
236
|
+
def after_magic_link_sent_path_for(resource)
|
237
|
+
case resource.class
|
238
|
+
when FooUser
|
239
|
+
happy_path
|
240
|
+
when BarUser
|
241
|
+
sad_path
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
And, if you need more complex behavior, you can always write a custom sessions
|
248
|
+
controller for each resource:
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
# app/controllers/custom_sessions_controller.rb
|
252
|
+
class CustomSessionsController < Devise::Passwordless::SessionsController
|
253
|
+
def create
|
254
|
+
# your custom logic
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# config/routes.rb
|
259
|
+
Rails.application.routes.draw do
|
260
|
+
devise_for :users,
|
261
|
+
controllers: { sessions: "custom_sessions" }
|
262
|
+
end
|
263
|
+
```
|
264
|
+
|
265
|
+
## Tokenizers
|
266
|
+
|
267
|
+
Tokenizers handle encoding and decoding of magic link tokens. There are multiple
|
268
|
+
pre-built ones to choose from, or [you can write your own](#your-own-custom-tokenizer).
|
269
|
+
|
270
|
+
Set the default tokenizer in your Devise initializer (`config.passwordless_tokenizer`),
|
271
|
+
which will be the global default. If you want a model to have a different tokenizer
|
272
|
+
than the default, you can define a class method `::passwordless_tokenizer` on your
|
273
|
+
model and that will be used instead. Models can have different tokenizers from
|
274
|
+
each other in this way.
|
275
|
+
|
276
|
+
### SignedGlobalIDTokenizer
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
config.passwordless_tokenizer = "SignedGlobalIDTokenizer"
|
280
|
+
```
|
281
|
+
|
282
|
+
Tokens are [Rails signed Global IDs][globalid]. This is the default for new installs.
|
283
|
+
|
284
|
+
Reasons to use or not use:
|
285
|
+
|
286
|
+
* The implementation is short and simple, so less likely to be buggy
|
287
|
+
* Should work with all ORMs that implement GlobalID support
|
288
|
+
* Cannot add arbitrary metadata to generated tokens
|
289
|
+
* Tokens are signed, not encrypted, so some data will be visible when base64-decoded
|
290
|
+
* Tokens tend to be a little longer (~30 chars IME) than MessageEncryptors'
|
291
|
+
|
292
|
+
[globalid]: https://github.com/rails/globalid
|
293
|
+
|
294
|
+
### MessageEncryptorTokenizer
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
config.passwordless_tokenizer = "MessageEncryptorTokenizer"
|
298
|
+
```
|
299
|
+
|
300
|
+
Tokens are encrypted using Rails's [MessageEncryptor][].
|
301
|
+
|
302
|
+
[MessageEncryptor]: https://api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html
|
303
|
+
|
304
|
+
Reasons to use or not use:
|
305
|
+
|
306
|
+
* This was the only tokenizer in previous library versions
|
307
|
+
* The implementation is longer and more involved than SignedGlobalID
|
308
|
+
* Written with ActiveRecord in mind but may work with other ORMs
|
309
|
+
* Can add arbitrary extra metadata to tokens
|
310
|
+
* Tokens are opaque, due to being encrypted - no data visible when base64-decoded
|
311
|
+
* Tokens tend to be a little shorter than SignedGlobalID IME
|
312
|
+
|
313
|
+
### Your own custom tokenizer
|
314
|
+
|
315
|
+
It's straightforward to write your own tokenizer class; it just needs to respond to
|
316
|
+
`::encode` and `::decode`:
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
class LuckyUserTokenizer
|
320
|
+
def self.encode(resource, *args)
|
321
|
+
"8" * 88 # our token is always lucky!
|
322
|
+
end
|
323
|
+
|
324
|
+
def self.decode(token, resource_class, *args)
|
325
|
+
# ignore token and retrieve a random user
|
326
|
+
[resource_class.order("RANDOM()").limit(1).first, extra_data={}]
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# config/initializers/devise.rb
|
331
|
+
config.passwordless_tokenizer = "::LuckyUserTokenizer"
|
332
|
+
```
|
333
|
+
|
334
|
+
## Multiple user (resource) types
|
136
335
|
|
137
336
|
Devise supports multiple resource types, so we do too.
|
138
337
|
|
@@ -157,18 +356,8 @@ Then just set up your routes like this:
|
|
157
356
|
Rails.application.routes.draw do
|
158
357
|
devise_for :users,
|
159
358
|
controllers: { sessions: "devise/passwordless/sessions" }
|
160
|
-
devise_scope :user do
|
161
|
-
get "/users/magic_link",
|
162
|
-
to: "devise/passwordless/magic_links#show",
|
163
|
-
as: "users_magic_link"
|
164
|
-
end
|
165
359
|
devise_for :admins,
|
166
360
|
controllers: { sessions: "devise/passwordless/sessions" }
|
167
|
-
devise_scope :admin do
|
168
|
-
get "/admins/magic_link",
|
169
|
-
to: "devise/passwordless/magic_links#show",
|
170
|
-
as: "admins_magic_link"
|
171
|
-
end
|
172
361
|
end
|
173
362
|
```
|
174
363
|
|
@@ -183,9 +372,11 @@ en:
|
|
183
372
|
user:
|
184
373
|
not_found_in_database: "Could not find a USER for that email address"
|
185
374
|
magic_link_sent: "A USER login link has been sent to your email address. Please follow the link to log in to your account."
|
375
|
+
magic_link_sent_paranoid: "If your USER account exists, you will receive an email with a login link. Please follow the link to log in to your account."
|
186
376
|
admin:
|
187
377
|
not_found_in_database: "Could not find an ADMIN for that email address"
|
188
378
|
magic_link_sent: "An ADMIN login link has been sent to your email address. Please follow the link to log in to your account."
|
379
|
+
magic_link_sent_paranoid: "If your ADMIN account exists, you will receive an email with a login link. Please follow the link to log in to your account."
|
189
380
|
failure:
|
190
381
|
user:
|
191
382
|
magic_link_invalid: "Invalid or expired USER login link."
|
@@ -197,7 +388,7 @@ en:
|
|
197
388
|
admin_subject: "Here's your ADMIN magic login link ✨"
|
198
389
|
```
|
199
390
|
|
200
|
-
|
391
|
+
### Scoped views
|
201
392
|
|
202
393
|
If you have multiple Devise models, some that are passwordless and some that aren't, you will probably want to enable [Devise's `scoped_views` setting](https://henrytabima.github.io/rails-setup/docs/devise/configuring-views) so that the models have different signup and login pages (since some models will need password fields and others won't).
|
203
394
|
|
@@ -215,7 +406,123 @@ app/views/users/
|
|
215
406
|
app/views/admins/
|
216
407
|
```
|
217
408
|
|
218
|
-
|
409
|
+
## Combining password and passwordless auth in the same model
|
410
|
+
|
411
|
+
It is possible to use both traditional password authentication (i.e. the
|
412
|
+
`:database_authenticatable` strategy) alongside magic link authentication in
|
413
|
+
the same model:
|
414
|
+
|
415
|
+
```ruby
|
416
|
+
# app/models/user.rb
|
417
|
+
class User < ApplicationRecord
|
418
|
+
devise :database_authenticatable, :magic_link_authenticatable, :registerable,
|
419
|
+
:recoverable, :rememberable, :validatable
|
420
|
+
end
|
421
|
+
```
|
422
|
+
|
423
|
+
How you end up implementing it will be highly dependent on your use case. By
|
424
|
+
default, all password validations will still run - so on registration, users
|
425
|
+
will have to provide passwords - but they'll be able to log in via either
|
426
|
+
password OR magic link (you'll have to customize your routes and views to
|
427
|
+
make the separate paths accessible).
|
428
|
+
|
429
|
+
Here's an example routes file of that scenario (a separate namespace is
|
430
|
+
needed because the password vs. passwordless paths use different sessions
|
431
|
+
controllers):
|
432
|
+
|
433
|
+
```ruby
|
434
|
+
devise_for :users
|
435
|
+
namespace "passwordless" do
|
436
|
+
devise_for :users,
|
437
|
+
controllers: { sessions: "devise/passwordless/sessions" }
|
438
|
+
end
|
439
|
+
```
|
440
|
+
|
441
|
+
Visiting `/users/sign_in` will lead to a password sign in, while
|
442
|
+
`/passwordless/users/sign_in` will lead to the magic link sign in flow
|
443
|
+
(you'll need to [generate the necessary Devise views](#scoped-views)
|
444
|
+
to support the different sign-in forms).
|
445
|
+
|
446
|
+
### Disabling password authentication or magic link authentication
|
447
|
+
|
448
|
+
Rather than *all* your users having access to *both* authentication methods,
|
449
|
+
it may be the case that you want *some* users to use magic links, *some*
|
450
|
+
to use passwords, or some combination between the two.
|
451
|
+
|
452
|
+
This can be managed by defining some methods that disable the relevant
|
453
|
+
authentication strategy and determine the failure message. Here are
|
454
|
+
examples for both:
|
455
|
+
|
456
|
+
### Disabling password authentication
|
457
|
+
|
458
|
+
Let's say you want to disable password authentication for everyone except
|
459
|
+
people named Bob:
|
460
|
+
|
461
|
+
```ruby
|
462
|
+
class User < ApplicationRecord
|
463
|
+
# devise :database_authenticatable, :magic_link_authenticatable, ...
|
464
|
+
|
465
|
+
def first_name_bob?
|
466
|
+
self.first_name.downcase == "bob"
|
467
|
+
end
|
468
|
+
|
469
|
+
# The `super` is important in the following two methods as other
|
470
|
+
# auth strategies chain onto these methods:
|
471
|
+
|
472
|
+
def active_for_authentication?
|
473
|
+
super && first_name_bob?
|
474
|
+
end
|
475
|
+
|
476
|
+
def inactive_message
|
477
|
+
first_name_bob? ? super : :first_name_not_bob
|
478
|
+
end
|
479
|
+
end
|
480
|
+
```
|
481
|
+
|
482
|
+
Then, you add this to your `devise.yml` to customize the error message:
|
483
|
+
|
484
|
+
```yaml
|
485
|
+
devise:
|
486
|
+
failure:
|
487
|
+
first_name_not_bob: "Sorry, only Bobs may log in using their password. Try magic link login instead."
|
488
|
+
```
|
489
|
+
|
490
|
+
Now, when users not named Bob try to log in with their password, it'll fail with
|
491
|
+
your custom failure message.
|
492
|
+
|
493
|
+
### Disabling passwordless / magic link authentication
|
494
|
+
|
495
|
+
Disabling magic link authentication is a similar process, just with different
|
496
|
+
method names:
|
497
|
+
|
498
|
+
```ruby
|
499
|
+
class User < ApplicationRecord
|
500
|
+
# devise :database_authenticatable, :magic_link_authenticatable, ...
|
501
|
+
|
502
|
+
def first_name_alice?
|
503
|
+
self.first_name.downcase == "alice"
|
504
|
+
end
|
505
|
+
|
506
|
+
# The `super` is actually not important at the moment for these, but if
|
507
|
+
# any future Devise strategies were to extend this one, they will be.
|
508
|
+
|
509
|
+
def active_for_magic_link_authentication?
|
510
|
+
super && first_name_alice?
|
511
|
+
end
|
512
|
+
|
513
|
+
def magic_link_inactive_message
|
514
|
+
first_name_alice? ? super : :first_name_not_alice_magic_link
|
515
|
+
end
|
516
|
+
end
|
517
|
+
```
|
518
|
+
|
519
|
+
```yaml
|
520
|
+
devise:
|
521
|
+
failure:
|
522
|
+
first_name_not_alice_magic_link: "Sorry, only Alices may log in using magic links. Try password login instead."
|
523
|
+
```
|
524
|
+
|
525
|
+
## Compatibility with other Devise strategies
|
219
526
|
|
220
527
|
If using the `:rememberable` strategy for "remember me" functionality, you'll need to add a `remember_token` column to your resource, as by default that strategy assumes you're using a password auth strategy and relies on comparing the password's salt to validate cookies:
|
221
528
|
|
@@ -227,6 +534,54 @@ end
|
|
227
534
|
|
228
535
|
If using the `:confirmable` strategy, you may want to override the default Devise behavior of requiring a fresh login after email confirmation (e.g. [this](https://stackoverflow.com/a/39010334/215168) or [this](https://stackoverflow.com/a/25865526/215168) approach). Otherwise, users will have to get a fresh login link after confirming their email, which makes little sense if they just confirmed they own the email address.
|
229
536
|
|
537
|
+
## Hotwire/Turbo support
|
538
|
+
|
539
|
+
If you're using Hotwire/Turbo, be sure that you're on Devise >= 4.9 and that you're
|
540
|
+
setting the `config.responder` config value in your Devise initializer to appropriate
|
541
|
+
values.
|
542
|
+
|
543
|
+
See the [Devise 4.9 Turbo upgrade guide][] for more info.
|
544
|
+
|
545
|
+
[Devise 4.9 Turbo upgrade guide]: https://github.com/heartcombo/devise/wiki/How-To:-Upgrade-to-Devise-4.9.0-%5BHotwire-Turbo-integration%5D
|
546
|
+
|
547
|
+
## ActiveJob support
|
548
|
+
|
549
|
+
If you want to use ActiveJob to send magic link emails asynchronously through
|
550
|
+
a queuing backend, you can accomplish it the same way you
|
551
|
+
[enable this functionality in any Devise install][devise-activejob]:
|
552
|
+
|
553
|
+
```ruby
|
554
|
+
class User
|
555
|
+
def send_devise_notification(notification, *args)
|
556
|
+
devise_mailer.send(notification, self, *args).deliver_later
|
557
|
+
end
|
558
|
+
end
|
559
|
+
```
|
560
|
+
|
561
|
+
[devise-activejob]: https://github.com/heartcombo/devise/blob/main/README.md#activejob-integration
|
562
|
+
|
563
|
+
## Rails logs security
|
564
|
+
|
565
|
+
Rails's default configuration filters `:token` parameters out of request logs (and
|
566
|
+
`Devise::Passwordless` will issue a warning if it detects the configuration doesn't). So request
|
567
|
+
logs shouldn't link magic link tokens.
|
568
|
+
|
569
|
+
However, there are some other default Rails logging behaviors that may cause plaintext magic
|
570
|
+
link tokens to leak into log files:
|
571
|
+
|
572
|
+
1. Action Mailer logs the entire contents of all outgoing emails to the DEBUG level. Magic link tokens delivered to users in email will be leaked.
|
573
|
+
2. Active Job logs all arguments to every enqueued job at the INFO level. If you configure Devise to use `deliver_later` to send passwordless emails, magic link tokens will be leaked.
|
574
|
+
|
575
|
+
Rails sets the production logger level to INFO by default. Consider changing your production logger level to WARN if you wish to prevent tokens from being leaked into your logs. In `config/environments/production.rb`:
|
576
|
+
|
577
|
+
```ruby
|
578
|
+
config.log_level = :warn
|
579
|
+
```
|
580
|
+
|
581
|
+
(Partially adapted from the [Devise guide on password reset tokens][], which this section also applies to)
|
582
|
+
|
583
|
+
[Devise guide on password reset tokens]: https://github.com/heartcombo/devise/blob/main/README.md#password-reset-tokens-and-rails-logs
|
584
|
+
|
230
585
|
## Alternatives
|
231
586
|
|
232
587
|
Other Ruby libraries that offer passwordless authentication:
|