passwordless 0.8.2 → 0.11.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/README.md +166 -63
- data/app/controllers/passwordless/sessions_controller.rb +38 -18
- data/app/mailers/passwordless/mailer.rb +2 -3
- data/app/models/passwordless/session.rb +12 -6
- data/config/routes.rb +4 -4
- data/db/migrate/20171104221735_create_passwordless_sessions.rb +8 -7
- data/lib/generators/passwordless/views_generator.rb +15 -0
- data/lib/passwordless/controller_helpers.rb +15 -9
- data/lib/passwordless/engine.rb +4 -2
- data/lib/passwordless/errors.rb +4 -2
- data/lib/passwordless/model_helpers.rb +3 -1
- data/lib/passwordless/router_helpers.rb +4 -2
- data/lib/passwordless/test_helpers.rb +43 -0
- data/lib/passwordless/version.rb +2 -1
- data/lib/passwordless.rb +4 -0
- metadata +9 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bfea8774f73a80e003f7257caa02afc7c1475a87077a4881b2c1e85442ebf8e9
|
4
|
+
data.tar.gz: 4882a066aa2ecc18a4a170e697014b5f09b9bde58c32e821eb60944c9fb90b13
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f71185082eb25883c1a7778276c244cd8e4ada2ee1c76137b2838c08b40ed8687b1db3eae2ed869f376be762d03d0ad7978c481d73f42c2338be0b1d200cb07c
|
7
|
+
data.tar.gz: 4ad367839721156af66ee6a76786acae842636eda04841ce1a92012dcc245ddd20414d00108908cd27190fa01e4636c88c4668012b893700f6e67ce604ca979f
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
<br />
|
5
5
|
</p>
|
6
6
|
|
7
|
-
[](https://github.com/mikker/passwordless/actions/workflows/ci.yml) [](https://rubygems.org/gems/passwordless) [](https://codecov.io/gh/mikker/passwordless)
|
8
8
|
|
9
9
|
Add authentication to your Rails app without all the icky-ness of passwords.
|
10
10
|
|
@@ -14,17 +14,21 @@ Add authentication to your Rails app without all the icky-ness of passwords.
|
|
14
14
|
|
15
15
|
* [Installation](#installation)
|
16
16
|
* [Usage](#usage)
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
17
|
+
* [Getting the current user, restricting access, the usual](#getting-the-current-user-restricting-access-the-usual)
|
18
|
+
* [Providing your own templates](#providing-your-own-templates)
|
19
|
+
* [Registering new users](#registering-new-users)
|
20
|
+
* [URLs and links](#urls-and-links)
|
21
|
+
* [Customize the way to send magic link](#customize-the-way-to-send-magic-link)
|
22
|
+
* [Generate your own magic links](#generate-your-own-magic-links)
|
23
|
+
* [Overrides](#overrides)
|
24
|
+
* [Configuration](#configuration)
|
25
|
+
* [Customising token generation](#generating-tokens)
|
26
|
+
* [Token and Session Expiry](#token-and-session-expiry)
|
27
|
+
* [Redirecting back after sign-in](#redirecting-back-after-sign-in)
|
28
|
+
* [Claiming tokens](#claiming-tokens)
|
29
|
+
* [Supporting UUID primary keys](#supporting-uuid-primary-keys)
|
30
|
+
* [Testing helpers](#testing-helpers)
|
31
|
+
* [E-mail security](#e-mail-security)
|
28
32
|
* [License](#license)
|
29
33
|
|
30
34
|
## Installation
|
@@ -46,7 +50,7 @@ $ bin/rails passwordless:install:migrations
|
|
46
50
|
|
47
51
|
Passwordless creates a single model called `Passwordless::Session`. It doesn't come with its own `User` model, it expects you to create one:
|
48
52
|
|
49
|
-
```
|
53
|
+
```sh
|
50
54
|
$ bin/rails generate model User email
|
51
55
|
```
|
52
56
|
|
@@ -90,7 +94,7 @@ class ApplicationController < ActionController::Base
|
|
90
94
|
|
91
95
|
def require_user!
|
92
96
|
return if current_user
|
93
|
-
redirect_to root_path, flash: {error: 'You are not worthy!'}
|
97
|
+
redirect_to root_path, flash: { error: 'You are not worthy!' }
|
94
98
|
end
|
95
99
|
end
|
96
100
|
```
|
@@ -109,7 +113,9 @@ end
|
|
109
113
|
|
110
114
|
### Providing your own templates
|
111
115
|
|
112
|
-
Override `passwordless`' bundled views by adding your own.
|
116
|
+
Override `passwordless`' bundled views by adding your own. You can manually copy the specific views that you need or copy them to your application with `rails generate passwordless:views`.
|
117
|
+
|
118
|
+
`passwordless` has 2 action views and 1 mailer view:
|
113
119
|
|
114
120
|
```sh
|
115
121
|
# the form where the user inputs their email address
|
@@ -120,6 +126,17 @@ app/views/passwordless/sessions/create.html.erb
|
|
120
126
|
app/views/passwordless/mailer/magic_link.text.erb
|
121
127
|
```
|
122
128
|
|
129
|
+
If you'd like to let the user know whether or not a record was found, `@resource` is provided to the view. You may override `app/views/passwordless/session/create.html.erb` for example like so:
|
130
|
+
```erb
|
131
|
+
<% if @resource.present? %>
|
132
|
+
<p>User found, check your inbox</p>
|
133
|
+
<% else %>
|
134
|
+
<p>No user found with the provided email address</p>
|
135
|
+
<% end %>
|
136
|
+
```
|
137
|
+
|
138
|
+
Please note that, from a security standpoint, this is a **bad practice** because you'd be giving information about which users are registered on your system. It is recommended to use a single message similar to the default one: "If we found you in the system, we've sent you an email". The **best practice** is to never expose which emails are registered on your system.
|
139
|
+
|
123
140
|
See [the bundled views](https://github.com/mikker/passwordless/tree/master/app/views/passwordless).
|
124
141
|
|
125
142
|
### Registering new users
|
@@ -136,7 +153,7 @@ class UsersController < ApplicationController
|
|
136
153
|
|
137
154
|
if @user.save
|
138
155
|
sign_in @user # <-- This!
|
139
|
-
redirect_to @user, flash: {notice: 'Welcome!'}
|
156
|
+
redirect_to @user, flash: { notice: 'Welcome!' }
|
140
157
|
else
|
141
158
|
render :new
|
142
159
|
end
|
@@ -146,7 +163,100 @@ class UsersController < ApplicationController
|
|
146
163
|
end
|
147
164
|
```
|
148
165
|
|
149
|
-
###
|
166
|
+
### URLs and links
|
167
|
+
|
168
|
+
By default, Passwordless uses the resource name given to `passwordless_for` to generate its routes and helpers.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
passwordless_for :users
|
172
|
+
# <%= users.sign_in_path %> # => /users/sign_in
|
173
|
+
|
174
|
+
passwordless_for :users, at: '/', as: :auth
|
175
|
+
# <%= auth.sign_in_path %> # => /sign_in
|
176
|
+
```
|
177
|
+
|
178
|
+
Also be sure to [specify ActionMailer's `default_url_options.host`](http://guides.rubyonrails.org/action_mailer_basics.html#generating-urls-in-action-mailer-views).
|
179
|
+
|
180
|
+
### Customize the way to send magic link
|
181
|
+
|
182
|
+
By default, magic link will send by email. You can customize this method. For example, you can send magic link via SMS.
|
183
|
+
|
184
|
+
config/initializers/passwordless.rb
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
Passwordless.after_session_save = lambda do |session, request|
|
188
|
+
# Default behavior is
|
189
|
+
# Passwordless::Mailer.magic_link(session).deliver_now
|
190
|
+
|
191
|
+
# You can change behavior to do something with session model. For example,
|
192
|
+
# session.authenticatable.send_sms
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
You can access user model through authenticatable.
|
197
|
+
|
198
|
+
### Generate your own magic links
|
199
|
+
|
200
|
+
Currently there is not an officially supported way to generate your own magic links to send in your own mailers.
|
201
|
+
|
202
|
+
However, you can accomplish this with the following snippet of code.
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
session = Passwordless::Session.new({
|
206
|
+
authenticatable: @manager,
|
207
|
+
user_agent: 'Command Line',
|
208
|
+
remote_addr: 'unknown',
|
209
|
+
})
|
210
|
+
session.save!
|
211
|
+
@magic_link = send(Passwordless.mounted_as).token_sign_in_url(session.token)
|
212
|
+
```
|
213
|
+
|
214
|
+
You can further customize this URL by specifying the destination path to be redirected to after the user has logged in. You can do this by adding the `destination_path` query parameter to the end of the URL. For example
|
215
|
+
```
|
216
|
+
@magic_link = "#{@magic_link}?destination_path=/your-custom-path"
|
217
|
+
```
|
218
|
+
|
219
|
+
### Overrides
|
220
|
+
|
221
|
+
By default `passwordless` uses the `passwordless_with` column to _case insensitively_ fetch the resource.
|
222
|
+
|
223
|
+
You can override this and provide your own customer fetcher by defining a class method `fetch_resource_for_passwordless` in your passwordless model. The method will be called with the downcased email and should return an `ActiveRecord` instance of the model.
|
224
|
+
|
225
|
+
Example time:
|
226
|
+
|
227
|
+
Let's say we would like to fetch the record and if it doesn't exist, create automatically.
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
class User < ApplicationRecord
|
231
|
+
def self.fetch_resource_for_passwordless(email)
|
232
|
+
find_or_create_by(email: email)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
```
|
236
|
+
|
237
|
+
## Configuration
|
238
|
+
|
239
|
+
The following configuration parameters are supported. You can override these for example in `initializers/passwordless.rb`.
|
240
|
+
|
241
|
+
The default values are shown below. It's recommended to only include the ones that you specifically want to override.
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
Passwordless.default_from_address = "CHANGE_ME@example.com"
|
245
|
+
Passwordless.parent_mailer = "ActionMailer::Base"
|
246
|
+
Passwordless.token_generator = Passwordless::UrlSafeBase64Generator.new # Used to generate magic link tokens.
|
247
|
+
Passwordless.restrict_token_reuse = false # By default a magic link token can be used multiple times.
|
248
|
+
Passwordless.redirect_back_after_sign_in = true # When enabled the user will be redirected to their previous page, or a page specified by the `destination_path` query parameter, if available.
|
249
|
+
|
250
|
+
Passwordless.expires_at = lambda { 1.year.from_now } # How long until a passwordless session expires.
|
251
|
+
Passwordless.timeout_at = lambda { 1.hour.from_now } # How long until a magic link expires.
|
252
|
+
|
253
|
+
# Default redirection paths
|
254
|
+
Passwordless.success_redirect_path = '/' # When a user succeeds in logging in.
|
255
|
+
Passwordless.failure_redirect_path = '/' # When a a login is failed for any reason.
|
256
|
+
Passwordless.sign_out_redirect_path = '/' # When a user logs out.
|
257
|
+
```
|
258
|
+
|
259
|
+
### Customizing token generation
|
150
260
|
|
151
261
|
By default Passwordless generates tokens using `SecureRandom.urlsafe_base64` but you can change that by setting `Passwordless.token_generator` to something else that responds to `call(session)` eg.:
|
152
262
|
|
@@ -204,39 +314,6 @@ end
|
|
204
314
|
|
205
315
|
This can be turned off with `Passwordless.redirect_back_after_sign_in = false` but if you just don't save the previous destination, you'll be fine.
|
206
316
|
|
207
|
-
### URLs and links
|
208
|
-
|
209
|
-
By default, Passwordless uses the resource name given to `passwordless_for` to generate its routes and helpers.
|
210
|
-
|
211
|
-
```ruby
|
212
|
-
passwordless_for :users
|
213
|
-
# <%= users.sign_in_path %> # => /users/sign_in
|
214
|
-
|
215
|
-
passwordless_for :users, at: '/', as: :auth
|
216
|
-
# <%= auth.sign_in_path %> # => /sign_in
|
217
|
-
```
|
218
|
-
|
219
|
-
Also be sure to [specify ActionMailer's `default_url_options.host`](http://guides.rubyonrails.org/action_mailer_basics.html#generating-urls-in-action-mailer-views).
|
220
|
-
|
221
|
-
|
222
|
-
### Customize the way to send magic link
|
223
|
-
|
224
|
-
By default, magic link will send by email. You can customize this method. For example, you can send magic link via SMS.
|
225
|
-
|
226
|
-
config/initializers/passwordless.rb
|
227
|
-
|
228
|
-
```
|
229
|
-
Passwordless.after_session_save = lambda do |session, request|
|
230
|
-
# Default behavior is
|
231
|
-
# Passwordless::Mailer.magic_link(session).deliver_now
|
232
|
-
|
233
|
-
# You can change behavior to do something with session model. For example,
|
234
|
-
# session.authenticatable.send_sms
|
235
|
-
end
|
236
|
-
```
|
237
|
-
|
238
|
-
You can access user model through authenticatable.
|
239
|
-
|
240
317
|
### Claiming tokens
|
241
318
|
|
242
319
|
Opt-in for marking tokens as `claimed` so they can only be used once.
|
@@ -248,7 +325,7 @@ config/initializers/passwordless.rb
|
|
248
325
|
Passwordless.restrict_token_reuse = true
|
249
326
|
```
|
250
327
|
|
251
|
-
#### Upgrading an existing Rails app
|
328
|
+
#### Upgrading an existing Rails app to use claim token
|
252
329
|
|
253
330
|
The simplest way to update your sessions table is with a single migration:
|
254
331
|
|
@@ -269,29 +346,55 @@ end
|
|
269
346
|
```
|
270
347
|
</details>
|
271
348
|
|
272
|
-
###
|
349
|
+
### Supporting UUID primary keys
|
273
350
|
|
274
|
-
|
351
|
+
If your `users` table uses UUIDs for its primary keys, you will need to add a migration
|
352
|
+
to change the type of `passwordless`' `authenticatable_id` field to match your primary key type (this will also involve dropping and recreating associated indices).
|
275
353
|
|
276
|
-
|
354
|
+
Here is an example migration you can use:
|
355
|
+
```ruby
|
356
|
+
class SupportUuidInPasswordlessSessions < ActiveRecord::Migration[6.0]
|
357
|
+
def change
|
358
|
+
remove_index :passwordless_sessions, column: [:authenticatable_type, :authenticatable_id] if index_exists? :authenticatable_type, :authenticatable_id
|
359
|
+
remove_column :passwordless_sessions, :authenticatable_id
|
360
|
+
add_column :passwordless_sessions, :authenticatable_id, :uuid
|
361
|
+
add_index :passwordless_sessions, [:authenticatable_type, :authenticatable_id], name: 'authenticatable'
|
362
|
+
end
|
363
|
+
end
|
364
|
+
```
|
277
365
|
|
278
|
-
|
366
|
+
Alternatively, you can use `add_reference` with `type: :uuid` in your migration (see docs [here](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference)).
|
279
367
|
|
280
|
-
|
368
|
+
## Testing helpers
|
369
|
+
|
370
|
+
To help with testing, a set of test helpers are provided.
|
371
|
+
|
372
|
+
If you are using RSpec, add the following line to your `spec/rails_helper.rb` or
|
373
|
+
`spec/spec_helper.rb` if `rails_helper.rb` does not exist:
|
281
374
|
|
282
375
|
```ruby
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
376
|
+
require "passwordless/test_helpers"
|
377
|
+
```
|
378
|
+
|
379
|
+
If you are using TestUnit, add this line to your `test/test_helper.rb`:
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
require "passwordless/test_helpers"
|
383
|
+
```
|
384
|
+
|
385
|
+
|
386
|
+
Then in your controller, request, and system tests/specs, you can utilize the following methods:
|
387
|
+
|
388
|
+
```ruby
|
389
|
+
passwordless_sign_in(user) # signs you in as a user
|
390
|
+
passwordless_sign_out # signs out user
|
288
391
|
```
|
289
392
|
|
290
|
-
|
393
|
+
## E-mail security
|
291
394
|
|
292
395
|
There's no reason that this approach should be less secure than the usual username/password combo. In fact this is most often a more secure option, as users don't get to choose the weak passwords they still use. In a way this is just the same as having each user go through "Forgot password" on every login.
|
293
396
|
|
294
|
-
But be aware that when everyone authenticates via emails you send, the way you send those mails becomes a weak spot. Email services usually provide a log of all the mails you send so if your app's account is compromised, every user in the system is as well. (This is the same for "Forgot password".) [Reddit was compromised](https://thenextweb.com/hardfork/2018/01/05/reddit-bitcoin-cash-hack/) using this method.
|
397
|
+
But be aware that when everyone authenticates via emails you send, the way you send those mails becomes a weak spot. Email services usually provide a log of all the mails you send so if your app's account is compromised, every user in the system is as well. (This is the same for "Forgot password".) [Reddit was compromised](https://thenextweb.com/hardfork/2018/01/05/reddit-bitcoin-cash-stolen-hack/) using this method.
|
295
398
|
|
296
399
|
Ideally you should set up your email provider to not log these mails. And be sure to turn on 2-factor auth if your provider supports it.
|
297
400
|
|
@@ -20,7 +20,8 @@ module Passwordless
|
|
20
20
|
# renders sessions/create.html.erb.
|
21
21
|
# @see Mailer#magic_link Mailer#magic_link
|
22
22
|
def create
|
23
|
-
|
23
|
+
@resource = find_authenticatable
|
24
|
+
session = build_passwordless_session(@resource)
|
24
25
|
|
25
26
|
if session.save
|
26
27
|
if Passwordless.after_session_save.arity == 2
|
@@ -28,9 +29,11 @@ module Passwordless
|
|
28
29
|
else
|
29
30
|
Passwordless.after_session_save.call(session)
|
30
31
|
end
|
31
|
-
end
|
32
32
|
|
33
|
-
|
33
|
+
render :create, status: :ok
|
34
|
+
else
|
35
|
+
render :create, status: :unprocessable_entity
|
36
|
+
end
|
34
37
|
end
|
35
38
|
|
36
39
|
# get '/sign_in/:token'
|
@@ -42,28 +45,47 @@ module Passwordless
|
|
42
45
|
def show
|
43
46
|
# Make it "slow" on purpose to make brute-force attacks more of a hassle
|
44
47
|
BCrypt::Password.create(params[:token])
|
48
|
+
sign_in(passwordless_session)
|
45
49
|
|
46
|
-
|
47
|
-
Passwordless.redirect_back_after_sign_in &&
|
48
|
-
reset_passwordless_redirect_location!(authenticatable_class)
|
49
|
-
|
50
|
-
sign_in passwordless_session
|
51
|
-
|
52
|
-
redirect_to destination || main_app.root_path
|
50
|
+
redirect_to(passwordless_success_redirect_path)
|
53
51
|
rescue Errors::TokenAlreadyClaimedError
|
54
52
|
flash[:error] = I18n.t(".passwordless.sessions.create.token_claimed")
|
55
|
-
redirect_to
|
53
|
+
redirect_to(passwordless_failure_redirect_path)
|
56
54
|
rescue Errors::SessionTimedOutError
|
57
55
|
flash[:error] = I18n.t(".passwordless.sessions.create.session_expired")
|
58
|
-
redirect_to
|
56
|
+
redirect_to(passwordless_failure_redirect_path)
|
59
57
|
end
|
60
58
|
|
61
59
|
# match '/sign_out', via: %i[get delete].
|
62
60
|
# Signs user out. Redirects to root_path
|
63
61
|
# @see ControllerHelpers#sign_out
|
64
62
|
def destroy
|
65
|
-
sign_out
|
66
|
-
redirect_to
|
63
|
+
sign_out(authenticatable_class)
|
64
|
+
redirect_to(passwordless_sign_out_redirect_path)
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
def passwordless_sign_out_redirect_path
|
70
|
+
Passwordless.sign_out_redirect_path
|
71
|
+
end
|
72
|
+
|
73
|
+
def passwordless_failure_redirect_path
|
74
|
+
Passwordless.failure_redirect_path
|
75
|
+
end
|
76
|
+
|
77
|
+
def passwordless_query_redirect_path
|
78
|
+
query_redirect_uri = URI(params[:destination_path])
|
79
|
+
query_redirect_uri.to_s if query_redirect_uri.host.nil? || query_redirect_uri.host == URI(request.url).host
|
80
|
+
rescue URI::InvalidURIError, ArgumentError
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def passwordless_success_redirect_path
|
85
|
+
return Passwordless.success_redirect_path unless Passwordless.redirect_back_after_sign_in
|
86
|
+
|
87
|
+
session_redirect_url = reset_passwordless_redirect_location!(authenticatable_class)
|
88
|
+
passwordless_query_redirect_path || session_redirect_url || Passwordless.success_redirect_path
|
67
89
|
end
|
68
90
|
|
69
91
|
private
|
@@ -85,14 +107,12 @@ module Passwordless
|
|
85
107
|
end
|
86
108
|
|
87
109
|
def find_authenticatable
|
88
|
-
email = params[:passwordless][email_field].downcase
|
110
|
+
email = params[:passwordless][email_field].downcase.strip
|
89
111
|
|
90
112
|
if authenticatable_class.respond_to?(:fetch_resource_for_passwordless)
|
91
113
|
authenticatable_class.fetch_resource_for_passwordless(email)
|
92
114
|
else
|
93
|
-
authenticatable_class.where(
|
94
|
-
"lower(#{email_field}) = ?", params[:passwordless][email_field].downcase
|
95
|
-
).first
|
115
|
+
authenticatable_class.where("lower(#{email_field}) = ?", email).first
|
96
116
|
end
|
97
117
|
end
|
98
118
|
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Passwordless
|
4
4
|
# The mailer responsible for sending Passwordless' mails.
|
5
|
-
class Mailer <
|
5
|
+
class Mailer < Passwordless.parent_mailer.constantize
|
6
6
|
default from: Passwordless.default_from_address
|
7
7
|
|
8
8
|
# Sends a magic link (secret token) email.
|
@@ -10,8 +10,7 @@ module Passwordless
|
|
10
10
|
def magic_link(session)
|
11
11
|
@session = session
|
12
12
|
|
13
|
-
@magic_link = send(Passwordless.mounted_as)
|
14
|
-
.token_sign_in_url(session.token)
|
13
|
+
@magic_link = send(Passwordless.mounted_as).token_sign_in_url(session.token)
|
15
14
|
|
16
15
|
email_field = @session.authenticatable.class.passwordless_email_field
|
17
16
|
mail(
|
@@ -4,10 +4,13 @@ module Passwordless
|
|
4
4
|
# The session responsible for holding the connection between the record
|
5
5
|
# trying to log in and the unique tokens.
|
6
6
|
class Session < ApplicationRecord
|
7
|
-
belongs_to
|
8
|
-
|
7
|
+
belongs_to(
|
8
|
+
:authenticatable,
|
9
|
+
polymorphic: true,
|
10
|
+
inverse_of: :passwordless_sessions
|
11
|
+
)
|
9
12
|
|
10
|
-
validates
|
13
|
+
validates(
|
11
14
|
:authenticatable,
|
12
15
|
:timeout_at,
|
13
16
|
:expires_at,
|
@@ -15,16 +18,19 @@ module Passwordless
|
|
15
18
|
:remote_addr,
|
16
19
|
:token,
|
17
20
|
presence: true
|
21
|
+
)
|
18
22
|
|
19
23
|
before_validation :set_defaults
|
20
24
|
|
21
|
-
scope
|
22
|
-
|
23
|
-
|
25
|
+
scope(
|
26
|
+
:available,
|
27
|
+
lambda { where("expires_at > ?", Time.current) }
|
28
|
+
)
|
24
29
|
|
25
30
|
def self.valid
|
26
31
|
available
|
27
32
|
end
|
33
|
+
|
28
34
|
class << self
|
29
35
|
deprecate :valid, deprecator: SessionValidDeprecation
|
30
36
|
end
|
data/config/routes.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
Passwordless::Engine.routes.draw do
|
4
|
-
get
|
5
|
-
post
|
6
|
-
get
|
7
|
-
match
|
4
|
+
get("/sign_in", to: "sessions#new", as: :sign_in)
|
5
|
+
post("/sign_in", to: "sessions#create")
|
6
|
+
get("/sign_in/:token", to: "sessions#show", as: :token_sign_in)
|
7
|
+
match("/sign_out", to: "sessions#destroy", via: %i[get delete], as: :sign_out)
|
8
8
|
end
|
@@ -2,18 +2,19 @@
|
|
2
2
|
|
3
3
|
class CreatePasswordlessSessions < ActiveRecord::Migration[5.1]
|
4
4
|
def change
|
5
|
-
create_table
|
5
|
+
create_table(:passwordless_sessions) do |t|
|
6
6
|
t.belongs_to(
|
7
7
|
:authenticatable,
|
8
8
|
polymorphic: true,
|
9
9
|
index: {name: "authenticatable"}
|
10
10
|
)
|
11
|
-
|
12
|
-
t.datetime
|
13
|
-
t.datetime :
|
14
|
-
t.
|
15
|
-
t.
|
16
|
-
t.string
|
11
|
+
|
12
|
+
t.datetime(:timeout_at, null: false)
|
13
|
+
t.datetime(:expires_at, null: false)
|
14
|
+
t.datetime(:claimed_at)
|
15
|
+
t.text(:user_agent, null: false)
|
16
|
+
t.string(:remote_addr, null: false)
|
17
|
+
t.string(:token, null: false)
|
17
18
|
|
18
19
|
t.timestamps
|
19
20
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module Passwordless
|
4
|
+
module Generators
|
5
|
+
class ViewsGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path('../../../app/views/passwordless', __dir__)
|
7
|
+
|
8
|
+
def install
|
9
|
+
copy_file 'mailer/magic_link.text.erb', 'app/views/passwordless/mailer/magic_link.text.erb'
|
10
|
+
copy_file 'sessions/new.html.erb', 'app/views/passwordless/sessions/new.html.erb'
|
11
|
+
copy_file 'sessions/create.html.erb', 'app/views/passwordless/sessions.create.html.erb'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -39,6 +39,7 @@ module Passwordless
|
|
39
39
|
|
40
40
|
authenticate_by_session(authenticatable_class)
|
41
41
|
end
|
42
|
+
|
42
43
|
deprecate :authenticate_by_cookie, deprecator: CookieDeprecation
|
43
44
|
|
44
45
|
def upgrade_passwordless_cookie(authenticatable_class)
|
@@ -51,7 +52,7 @@ module Passwordless
|
|
51
52
|
return unless (record = authenticatable_class.find_by(id: authenticatable_id))
|
52
53
|
new_session = build_passwordless_session(record).tap { |s| s.save! }
|
53
54
|
|
54
|
-
sign_in
|
55
|
+
sign_in(new_session)
|
55
56
|
|
56
57
|
new_session.authenticatable
|
57
58
|
end
|
@@ -73,20 +74,25 @@ module Passwordless
|
|
73
74
|
# to sign in
|
74
75
|
# @return [ActiveRecord::Base] the record that is passed in.
|
75
76
|
def sign_in(record)
|
76
|
-
passwordless_session =
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
77
|
+
passwordless_session = if record.is_a?(Passwordless::Session)
|
78
|
+
record
|
79
|
+
else
|
80
|
+
warn(
|
81
|
+
"Passwordless::ControllerHelpers#sign_in with authenticatable " \
|
81
82
|
"(`#{record.class}') is deprecated. Falling back to creating a " \
|
82
83
|
"new Passwordless::Session"
|
83
|
-
|
84
|
-
|
84
|
+
)
|
85
|
+
build_passwordless_session(record).tap { |s| s.save! }
|
86
|
+
end
|
85
87
|
|
86
88
|
passwordless_session.claim! if Passwordless.restrict_token_reuse
|
87
89
|
|
88
90
|
raise Passwordless::Errors::SessionTimedOutError if passwordless_session.timed_out?
|
89
91
|
|
92
|
+
old_session = session.dup.to_hash
|
93
|
+
reset_session
|
94
|
+
old_session.each_pair { |k, v| session[k.to_sym] = v }
|
95
|
+
|
90
96
|
key = session_key(passwordless_session.authenticatable_type)
|
91
97
|
session[key] = passwordless_session.id
|
92
98
|
|
@@ -105,8 +111,8 @@ module Passwordless
|
|
105
111
|
key = cookie_name(authenticatable_class)
|
106
112
|
cookies.encrypted.permanent[key] = {value: nil}
|
107
113
|
cookies.delete(key)
|
108
|
-
# /deprecated
|
109
114
|
|
115
|
+
# /deprecated
|
110
116
|
reset_session
|
111
117
|
true
|
112
118
|
end
|
data/lib/passwordless/engine.rb
CHANGED
@@ -7,15 +7,17 @@ module Passwordless
|
|
7
7
|
|
8
8
|
config.to_prepare do
|
9
9
|
require "passwordless/router_helpers"
|
10
|
+
|
10
11
|
ActionDispatch::Routing::Mapper.include RouterHelpers
|
11
12
|
require "passwordless/model_helpers"
|
13
|
+
|
12
14
|
ActiveRecord::Base.extend ModelHelpers
|
13
15
|
require "passwordless/controller_helpers"
|
16
|
+
|
14
17
|
end
|
15
18
|
|
16
19
|
config.before_initialize do |app|
|
17
|
-
app.config.i18n.load_path +=
|
18
|
-
Dir[Engine.root.join("config", "locales", "*.yml")]
|
20
|
+
app.config.i18n.load_path += Dir[Engine.root.join("config", "locales", "*.yml")]
|
19
21
|
end
|
20
22
|
end
|
21
23
|
end
|
data/lib/passwordless/errors.rb
CHANGED
@@ -3,9 +3,11 @@
|
|
3
3
|
module Passwordless
|
4
4
|
module Errors
|
5
5
|
# Raise this exception when a session is expired.
|
6
|
-
class SessionTimedOutError < StandardError
|
6
|
+
class SessionTimedOutError < StandardError
|
7
|
+
end
|
7
8
|
|
8
9
|
# Raise this exception when the token has been previously claimed
|
9
|
-
class TokenAlreadyClaimedError < StandardError
|
10
|
+
class TokenAlreadyClaimedError < StandardError
|
11
|
+
end
|
10
12
|
end
|
11
13
|
end
|
@@ -8,9 +8,11 @@ module Passwordless
|
|
8
8
|
# field name (e.g. `:email`)
|
9
9
|
# @param field [string] email submitted by user.
|
10
10
|
def passwordless_with(field)
|
11
|
-
has_many
|
11
|
+
has_many(
|
12
|
+
:passwordless_sessions,
|
12
13
|
class_name: "Passwordless::Session",
|
13
14
|
as: :authenticatable
|
15
|
+
)
|
14
16
|
|
15
17
|
define_singleton_method(:passwordless_email_field) { field }
|
16
18
|
end
|
@@ -20,8 +20,10 @@ module Passwordless
|
|
20
20
|
mount_at = at || resource.to_s
|
21
21
|
mount_as = as || resource.to_s
|
22
22
|
mount(
|
23
|
-
Passwordless::Engine,
|
24
|
-
|
23
|
+
Passwordless::Engine,
|
24
|
+
at: mount_at,
|
25
|
+
as: mount_as,
|
26
|
+
defaults: {authenticatable: resource.to_s.singularize}
|
25
27
|
)
|
26
28
|
|
27
29
|
Passwordless.mounted_as = mount_as
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Passwordless
|
2
|
+
module TestHelpers
|
3
|
+
module TestCase
|
4
|
+
def passwordless_sign_out
|
5
|
+
delete Passwordless::Engine.routes.url_helpers.sign_out_path
|
6
|
+
follow_redirect!
|
7
|
+
end
|
8
|
+
|
9
|
+
def passwordless_sign_in(resource)
|
10
|
+
session = Passwordless::Session.create!(authenticatable: resource, user_agent: "TestAgent", remote_addr: "unknown")
|
11
|
+
get Passwordless::Engine.routes.url_helpers.token_sign_in_path(session.token)
|
12
|
+
follow_redirect!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module SystemTestCase
|
17
|
+
def passwordless_sign_out
|
18
|
+
visit Passwordless::Engine.routes.url_helpers.sign_out_path
|
19
|
+
end
|
20
|
+
|
21
|
+
def passwordless_sign_in(resource)
|
22
|
+
session = Passwordless::Session.create!(authenticatable: resource, user_agent: "TestAgent", remote_addr: "unknown")
|
23
|
+
visit Passwordless::Engine.routes.url_helpers.token_sign_in_path(session.token)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
if defined?(ActiveSupport::TestCase)
|
30
|
+
ActiveSupport::TestCase.send(:include, ::Passwordless::TestHelpers::TestCase)
|
31
|
+
end
|
32
|
+
|
33
|
+
if defined?(ActionDispatch::SystemTestCase)
|
34
|
+
ActionDispatch::SystemTestCase.send(:include, ::Passwordless::TestHelpers::SystemTestCase)
|
35
|
+
end
|
36
|
+
|
37
|
+
if defined?(RSpec)
|
38
|
+
RSpec.configure do |config|
|
39
|
+
config.include ::Passwordless::TestHelpers::TestCase, type: :request
|
40
|
+
config.include ::Passwordless::TestHelpers::TestCase, type: :controller
|
41
|
+
config.include ::Passwordless::TestHelpers::SystemTestCase, type: :system
|
42
|
+
end
|
43
|
+
end
|
data/lib/passwordless/version.rb
CHANGED
data/lib/passwordless.rb
CHANGED
@@ -7,6 +7,7 @@ require "passwordless/url_safe_base_64_generator"
|
|
7
7
|
|
8
8
|
# The main Passwordless module
|
9
9
|
module Passwordless
|
10
|
+
mattr_accessor(:parent_mailer) { "ActionMailer::Base" }
|
10
11
|
mattr_accessor(:default_from_address) { "CHANGE_ME@example.com" }
|
11
12
|
mattr_accessor(:token_generator) { UrlSafeBase64Generator.new }
|
12
13
|
mattr_accessor(:restrict_token_reuse) { false }
|
@@ -15,6 +16,9 @@ module Passwordless
|
|
15
16
|
|
16
17
|
mattr_accessor(:expires_at) { lambda { 1.year.from_now } }
|
17
18
|
mattr_accessor(:timeout_at) { lambda { 1.hour.from_now } }
|
19
|
+
mattr_accessor(:success_redirect_path) { "/" }
|
20
|
+
mattr_accessor(:failure_redirect_path) { "/" }
|
21
|
+
mattr_accessor(:sign_out_redirect_path) { "/" }
|
18
22
|
|
19
23
|
mattr_accessor(:after_session_save) do
|
20
24
|
lambda { |session, _request| Mailer.magic_link(session).deliver_now }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: passwordless
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mikkel Malmberg
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-08-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -80,7 +80,7 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
-
description:
|
83
|
+
description:
|
84
84
|
email:
|
85
85
|
- mikkel@brnbw.com
|
86
86
|
executables: []
|
@@ -101,19 +101,21 @@ files:
|
|
101
101
|
- config/locales/en.yml
|
102
102
|
- config/routes.rb
|
103
103
|
- db/migrate/20171104221735_create_passwordless_sessions.rb
|
104
|
+
- lib/generators/passwordless/views_generator.rb
|
104
105
|
- lib/passwordless.rb
|
105
106
|
- lib/passwordless/controller_helpers.rb
|
106
107
|
- lib/passwordless/engine.rb
|
107
108
|
- lib/passwordless/errors.rb
|
108
109
|
- lib/passwordless/model_helpers.rb
|
109
110
|
- lib/passwordless/router_helpers.rb
|
111
|
+
- lib/passwordless/test_helpers.rb
|
110
112
|
- lib/passwordless/url_safe_base_64_generator.rb
|
111
113
|
- lib/passwordless/version.rb
|
112
114
|
homepage: https://github.com/mikker/passwordless
|
113
115
|
licenses:
|
114
116
|
- MIT
|
115
117
|
metadata: {}
|
116
|
-
post_install_message:
|
118
|
+
post_install_message:
|
117
119
|
rdoc_options: []
|
118
120
|
require_paths:
|
119
121
|
- lib
|
@@ -128,8 +130,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
130
|
- !ruby/object:Gem::Version
|
129
131
|
version: '0'
|
130
132
|
requirements: []
|
131
|
-
rubygems_version: 3.
|
132
|
-
signing_key:
|
133
|
+
rubygems_version: 3.3.7
|
134
|
+
signing_key:
|
133
135
|
specification_version: 4
|
134
136
|
summary: Add authentication to your app without all the ickyness of passwords.
|
135
137
|
test_files: []
|