passwordless 0.12.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/README.md +87 -194
- data/Rakefile +7 -7
- data/app/controllers/passwordless/sessions_controller.rb +121 -39
- data/app/mailers/passwordless/mailer.rb +13 -11
- data/app/models/passwordless/session.rb +25 -12
- data/app/views/passwordless/mailer/sign_in.text.erb +1 -0
- data/app/views/passwordless/sessions/new.html.erb +8 -4
- data/app/views/passwordless/sessions/show.html.erb +5 -0
- data/config/locales/en.yml +18 -6
- data/config/routes.rb +0 -4
- data/db/migrate/20171104221735_create_passwordless_sessions.rb +1 -3
- data/lib/generators/passwordless/views_generator.rb +5 -5
- data/lib/passwordless/config.rb +71 -0
- data/lib/passwordless/controller_helpers.rb +32 -70
- data/lib/passwordless/engine.rb +2 -6
- data/lib/passwordless/errors.rb +4 -0
- data/lib/passwordless/router_helpers.rb +24 -10
- data/lib/passwordless/short_token_generator.rb +9 -0
- data/lib/passwordless/test_helpers.rb +19 -9
- data/lib/passwordless/token_digest.rb +18 -0
- data/lib/passwordless/version.rb +1 -1
- data/lib/passwordless.rb +5 -19
- metadata +10 -49
- data/app/views/passwordless/mailer/magic_link.text.erb +0 -1
- data/lib/passwordless/url_safe_base_64_generator.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24d02d4dc7676adee968e3f922097f744ee41c2a1826d7622ffac01f3aaf76b8
|
4
|
+
data.tar.gz: 03624c6400113e2071eb038cd9406102870b3a56e1f8fa790bb9c190f17b95d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34e67e3b5a2be5cc657ee7193fa0ea76fc27ae7aad32413c66c4534bd2ce91540d7fe3ccdb2a2816a9d46053fd06d2f9c840d69e14cb7e5c49a45b3933be418e
|
7
|
+
data.tar.gz: ed550ba88a988109ad8192120fa25225c2c2fc161caff9beb9f51f5bd54baf72325c5baf02fb42da72a2a23ccb923687959aea517c2ce00879f580f82ab24559
|
data/README.md
CHANGED
@@ -10,42 +10,19 @@ Add authentication to your Rails app without all the icky-ness of passwords.
|
|
10
10
|
|
11
11
|
---
|
12
12
|
|
13
|
-
## Table of Contents
|
14
|
-
|
15
|
-
* [Installation](#installation)
|
16
|
-
* [Usage](#usage)
|
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)
|
32
|
-
* [License](#license)
|
33
|
-
|
34
13
|
## Installation
|
35
14
|
|
36
|
-
Add
|
37
|
-
|
38
|
-
```ruby
|
39
|
-
gem 'passwordless'
|
40
|
-
```
|
41
|
-
|
42
|
-
Install it and copy over the migrations:
|
15
|
+
Add to your bundle and copy over the migrations:
|
43
16
|
|
44
17
|
```sh
|
45
|
-
$ bundle
|
18
|
+
$ bundle add passwordless
|
46
19
|
$ bin/rails passwordless:install:migrations
|
47
20
|
```
|
48
21
|
|
22
|
+
### Upgrading
|
23
|
+
|
24
|
+
See [Upgrading to Passwordless 1.0](docs/upgrading_to_1_0.md) for more details.
|
25
|
+
|
49
26
|
## Usage
|
50
27
|
|
51
28
|
Passwordless creates a single model called `Passwordless::Session`. It doesn't come with its own `User` model, it expects you to create one:
|
@@ -97,6 +74,7 @@ class ApplicationController < ActionController::Base
|
|
97
74
|
|
98
75
|
def require_user!
|
99
76
|
return if current_user
|
77
|
+
save_passwordless_redirect_location!(User) # <-- optional, see below
|
100
78
|
redirect_to root_path, flash: { error: 'You are not worthy!' }
|
101
79
|
end
|
102
80
|
end
|
@@ -116,30 +94,19 @@ end
|
|
116
94
|
|
117
95
|
### Providing your own templates
|
118
96
|
|
119
|
-
|
97
|
+
To make Passwordless look like your app, override the 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`.
|
120
98
|
|
121
|
-
|
99
|
+
Passwordless has 2 action views and 1 mailer view:
|
122
100
|
|
123
101
|
```sh
|
124
102
|
# the form where the user inputs their email address
|
125
103
|
app/views/passwordless/sessions/new.html.erb
|
126
|
-
#
|
127
|
-
app/views/passwordless/sessions/
|
128
|
-
# the
|
129
|
-
app/views/passwordless/mailer/
|
130
|
-
```
|
131
|
-
|
132
|
-
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:
|
133
|
-
```erb
|
134
|
-
<% if @resource.present? %>
|
135
|
-
<p>User found, check your inbox</p>
|
136
|
-
<% else %>
|
137
|
-
<p>No user found with the provided email address</p>
|
138
|
-
<% end %>
|
104
|
+
# the form where the user inputs their just received token
|
105
|
+
app/views/passwordless/sessions/show.html.erb
|
106
|
+
# the email with the token and magic link
|
107
|
+
app/views/passwordless/mailer/sign_in.text.erb
|
139
108
|
```
|
140
109
|
|
141
|
-
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.
|
142
|
-
|
143
110
|
See [the bundled views](https://github.com/mikker/passwordless/tree/master/app/views/passwordless).
|
144
111
|
|
145
112
|
### Registering new users
|
@@ -152,13 +119,13 @@ class UsersController < ApplicationController
|
|
152
119
|
# (unless you already have it in your ApplicationController)
|
153
120
|
|
154
121
|
def create
|
155
|
-
@user = User.new
|
122
|
+
@user = User.new(user_params)
|
156
123
|
|
157
124
|
if @user.save
|
158
|
-
sign_in
|
159
|
-
redirect_to
|
125
|
+
sign_in(build_passwordless_session(@user)) # <-- This!
|
126
|
+
redirect_to(@user, flash: { notice: 'Welcome!' })
|
160
127
|
else
|
161
|
-
render
|
128
|
+
render(:new)
|
162
129
|
end
|
163
130
|
end
|
164
131
|
|
@@ -172,137 +139,90 @@ By default, Passwordless uses the resource name given to `passwordless_for` to g
|
|
172
139
|
|
173
140
|
```ruby
|
174
141
|
passwordless_for :users
|
175
|
-
# <%=
|
142
|
+
# <%= users_sign_in_path %> # => /users/sign_in
|
176
143
|
|
177
144
|
passwordless_for :users, at: '/', as: :auth
|
178
|
-
# <%=
|
145
|
+
# <%= auth_sign_in_path %> # => /sign_in
|
179
146
|
```
|
180
147
|
|
181
|
-
Also be sure to
|
148
|
+
Also be sure to
|
149
|
+
[specify ActionMailer's `default_url_options.host`](http://guides.rubyonrails.org/action_mailer_basics.html#generating-urls-in-action-mailer-views).
|
182
150
|
|
183
|
-
|
151
|
+
## Configuration
|
184
152
|
|
185
|
-
|
153
|
+
To customize Passwordless, create a file `config/initializers/passwordless.rb`.
|
186
154
|
|
187
|
-
|
155
|
+
The default values are shown below. It's recommended to only include the ones that you specifically want to modify.
|
188
156
|
|
189
157
|
```ruby
|
190
|
-
Passwordless.
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
158
|
+
Passwordless.configure do |config|
|
159
|
+
config.default_from_address = "CHANGE_ME@example.com"
|
160
|
+
config.parent_mailer = "ActionMailer::Base"
|
161
|
+
config.restrict_token_reuse = false # Can a token/link be used multiple times?
|
162
|
+
config.token_generator = Passwordless::ShortTokenGenerator.new # Used to generate magic link tokens.
|
163
|
+
|
164
|
+
config.expires_at = lambda { 1.year.from_now } # How long until a signed in session expires.
|
165
|
+
config.timeout_at = lambda { 10.minutes.from_now } # How long until a token/magic link times out.
|
166
|
+
|
167
|
+
config.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.
|
168
|
+
config.redirect_to_response_options = {} # Additional options for redirects.
|
169
|
+
config.success_redirect_path = '/' # After a user successfully signs in
|
170
|
+
config.failure_redirect_path = '/' # After a sign in fails
|
171
|
+
config.sign_out_redirect_path = '/' # After a user signs out
|
196
172
|
end
|
197
173
|
```
|
198
174
|
|
199
|
-
|
175
|
+
### Delivery method
|
200
176
|
|
201
|
-
|
177
|
+
By default, Passwordless sends emails. See [Providing your own templates](#providing-your-own-templates). If you need to customize this further, you can do so in the `after_session_save` callback.
|
202
178
|
|
203
|
-
|
204
|
-
|
205
|
-
However, you can accomplish this with the following snippet of code.
|
179
|
+
In `config/initializers/passwordless.rb`:
|
206
180
|
|
207
181
|
```ruby
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
})
|
213
|
-
session.save!
|
214
|
-
@magic_link = send(Passwordless.mounted_as).token_sign_in_url(session.token)
|
215
|
-
```
|
182
|
+
Passwordless.configure do |config|
|
183
|
+
config.after_session_save = lambda do |session, request|
|
184
|
+
# Default behavior is
|
185
|
+
# Passwordless::Mailer.sign_in(session).deliver_now
|
216
186
|
|
217
|
-
You can
|
218
|
-
|
219
|
-
@magic_link = "#{@magic_link}?destination_path=/your-custom-path"
|
220
|
-
```
|
221
|
-
|
222
|
-
### Overrides
|
223
|
-
|
224
|
-
By default `passwordless` uses the `passwordless_with` column to _case insensitively_ fetch the resource.
|
225
|
-
|
226
|
-
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.
|
227
|
-
|
228
|
-
Example time:
|
229
|
-
|
230
|
-
Let's say we would like to fetch the record and if it doesn't exist, create automatically.
|
231
|
-
|
232
|
-
```ruby
|
233
|
-
class User < ApplicationRecord
|
234
|
-
def self.fetch_resource_for_passwordless(email)
|
235
|
-
find_or_create_by(email: email)
|
187
|
+
# You can change behavior to do something with session model. For example,
|
188
|
+
# SmsApi.send_sms(session.authenticatable.phone_number, session.token)
|
236
189
|
end
|
237
190
|
end
|
238
191
|
```
|
239
192
|
|
240
|
-
|
241
|
-
|
242
|
-
The following configuration parameters are supported. You can override these for example in `initializers/passwordless.rb`.
|
193
|
+
### Token generation
|
243
194
|
|
244
|
-
|
195
|
+
By default Passwordless generates short, 6-digit, alpha numeric tokens. You can change the generator using `Passwordless.config.token_generator` to something else that responds to `call(session)` eg.:
|
245
196
|
|
246
197
|
```ruby
|
247
|
-
Passwordless.
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
Passwordless.expires_at = lambda { 1.year.from_now } # How long until a passwordless session expires.
|
254
|
-
Passwordless.timeout_at = lambda { 1.hour.from_now } # How long until a magic link expires.
|
255
|
-
|
256
|
-
# redirection session behavior
|
257
|
-
Passwordless.redirect_to_response_options = {} # any allowed response_options for redirect_to can go in here
|
258
|
-
|
259
|
-
# Default redirection paths
|
260
|
-
Passwordless.success_redirect_path = '/' # When a user succeeds in logging in.
|
261
|
-
Passwordless.failure_redirect_path = '/' # When a a login is failed for any reason.
|
262
|
-
Passwordless.sign_out_redirect_path = '/' # When a user logs out.
|
263
|
-
```
|
264
|
-
|
265
|
-
### Customizing token generation
|
266
|
-
|
267
|
-
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.:
|
268
|
-
|
269
|
-
```ruby
|
270
|
-
Passwordless.token_generator = -> (session) {
|
271
|
-
"probably-stupid-token-#{session.user_agent}-#{Time.current}"
|
272
|
-
}
|
198
|
+
Passwordless.configure do |config|
|
199
|
+
config.token_generator = lambda do |session|
|
200
|
+
"probably-stupid-token-#{session.user_agent}-#{Time.current}"
|
201
|
+
end
|
202
|
+
end
|
273
203
|
```
|
274
204
|
|
275
|
-
|
205
|
+
Passwordless will keep generating tokens until it finds one that hasn't been used yet. So be sure to use some kind of method where matches are unlikely.
|
276
206
|
|
277
|
-
###
|
207
|
+
### Timeout and Expiry
|
278
208
|
|
279
|
-
|
209
|
+
The _timeout_ is the time by which the generated token and magic link is invalidated. After this the token cannot be used to sign in to your app and the user will need to request a new token.
|
280
210
|
|
281
|
-
|
211
|
+
The _expiry_ is the expiration time of the session of a logged in user. Once this is expired, the user is signed out.
|
282
212
|
|
283
|
-
|
213
|
+
**Note:** Passwordless' session relies on Rails' own session and so will never live longer than that.
|
284
214
|
|
285
|
-
|
286
|
-
|
287
|
-
> Make sure to use a `.call`able object, like a proc or lambda as it will be called everytime a session is created.
|
215
|
+
To configure your Rails session, in `config/initializers/session_store.rb`:
|
288
216
|
|
289
217
|
```ruby
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
#### Session Expiry
|
294
|
-
|
295
|
-
Session expiry is the time when the actual session is itself expired, i.e. users will be logged out and has to sign back in post this expiry time. By default, sessions are valid for `1.year` from the time they are generated. You can override by providing your custom Proc function that returns a datetime object.
|
296
|
-
|
297
|
-
> Make sure to use a `.call`able object, like a proc or lambda as it will be called everytime a session is created.
|
298
|
-
|
299
|
-
```ruby
|
300
|
-
Passwordless.expires_at = lambda { 24.hours.from_now }
|
218
|
+
Rails.application.config.session_store :cookie_store,
|
219
|
+
expire_after: 1.year,
|
220
|
+
# ...
|
301
221
|
```
|
302
222
|
|
303
|
-
###
|
223
|
+
### Redirection after sign-in
|
304
224
|
|
305
|
-
By default Passwordless will redirect back to where the user wanted to go
|
225
|
+
By default Passwordless will redirect back to where the user wanted to go _if_ it knows where that is -- so you'll have to help it. `Passwordless::ControllerHelpers` provide a method:
|
306
226
|
|
307
227
|
```ruby
|
308
228
|
class ApplicationController < ActionController::Base
|
@@ -312,71 +232,45 @@ class ApplicationController < ActionController::Base
|
|
312
232
|
|
313
233
|
def require_user!
|
314
234
|
return if current_user
|
315
|
-
save_passwordless_redirect_location!(User) # <--
|
235
|
+
save_passwordless_redirect_location!(User) # <-- this one!
|
316
236
|
redirect_to root_path, flash: {error: 'You are not worthy!'}
|
317
237
|
end
|
318
238
|
end
|
319
239
|
```
|
320
240
|
|
321
|
-
This can be turned off with `Passwordless.redirect_back_after_sign_in = false
|
322
|
-
|
323
|
-
### Claiming tokens
|
324
|
-
|
325
|
-
Opt-in for marking tokens as `claimed` so they can only be used once.
|
326
|
-
|
327
|
-
config/initializers/passwordless.rb
|
328
|
-
|
329
|
-
```ruby
|
330
|
-
# Default is `false`
|
331
|
-
Passwordless.restrict_token_reuse = true
|
332
|
-
```
|
333
|
-
|
334
|
-
#### Upgrading an existing Rails app to use claim token
|
241
|
+
This can also be turned off with `Passwordless.config.redirect_back_after_sign_in = false`.
|
335
242
|
|
336
|
-
|
243
|
+
### Looking up the user
|
337
244
|
|
338
|
-
|
339
|
-
<summary>Example migration</summary>
|
245
|
+
By default Passwordless uses the `passwordless_with` column to _case insensitively_ fetch the user resource.
|
340
246
|
|
341
|
-
|
342
|
-
bin/rails generate migration add_claimed_at_to_passwordless_sessions
|
343
|
-
```
|
247
|
+
You can override this by defining a class method `fetch_resource_for_passwordless` in your user model. This method will be called with the down-cased, stripped `email` and should return an `ActiveRecord` instance.
|
344
248
|
|
345
249
|
```ruby
|
346
|
-
class
|
347
|
-
def
|
348
|
-
|
250
|
+
class User < ApplicationRecord
|
251
|
+
def self.fetch_resource_for_passwordless(email)
|
252
|
+
find_or_create_by(email: email)
|
349
253
|
end
|
350
254
|
end
|
351
|
-
|
352
255
|
```
|
353
|
-
</details>
|
354
256
|
|
355
|
-
###
|
257
|
+
### Claiming tokens
|
258
|
+
|
259
|
+
By default, a token/magic link **can** be used more than once.
|
356
260
|
|
357
|
-
|
358
|
-
to change the type of `passwordless`' `authenticatable_id` field to match your primary key type (this will also involve dropping and recreating associated indices).
|
261
|
+
To change, in `config/initializers/passwordless.rb`:
|
359
262
|
|
360
|
-
Here is an example migration you can use:
|
361
263
|
```ruby
|
362
|
-
|
363
|
-
|
364
|
-
remove_index :passwordless_sessions, column: [:authenticatable_type, :authenticatable_id] if index_exists? :authenticatable_type, :authenticatable_id
|
365
|
-
remove_column :passwordless_sessions, :authenticatable_id
|
366
|
-
add_column :passwordless_sessions, :authenticatable_id, :uuid
|
367
|
-
add_index :passwordless_sessions, [:authenticatable_type, :authenticatable_id], name: 'authenticatable'
|
368
|
-
end
|
264
|
+
Passwordless.configure do |config|
|
265
|
+
config.restrict_token_reuse = true
|
369
266
|
end
|
370
267
|
```
|
371
268
|
|
372
|
-
|
373
|
-
|
374
|
-
## Testing helpers
|
269
|
+
## Test helpers
|
375
270
|
|
376
271
|
To help with testing, a set of test helpers are provided.
|
377
272
|
|
378
|
-
If you are using RSpec, add the following line to your `spec/rails_helper.rb
|
379
|
-
`spec/spec_helper.rb` if `rails_helper.rb` does not exist:
|
273
|
+
If you are using RSpec, add the following line to your `spec/rails_helper.rb`:
|
380
274
|
|
381
275
|
```ruby
|
382
276
|
require "passwordless/test_helpers"
|
@@ -388,7 +282,6 @@ If you are using TestUnit, add this line to your `test/test_helper.rb`:
|
|
388
282
|
require "passwordless/test_helpers"
|
389
283
|
```
|
390
284
|
|
391
|
-
|
392
285
|
Then in your controller, request, and system tests/specs, you can utilize the following methods:
|
393
286
|
|
394
287
|
```ruby
|
@@ -396,18 +289,18 @@ passwordless_sign_in(user) # signs you in as a user
|
|
396
289
|
passwordless_sign_out # signs out user
|
397
290
|
```
|
398
291
|
|
399
|
-
##
|
292
|
+
## Security considerations
|
400
293
|
|
401
|
-
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
|
294
|
+
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 horrible passwords they can't seem to stop using. In a way, this is just the same as having each user go through "Forgot password" on every login.
|
402
295
|
|
403
|
-
But be aware that when everyone authenticates via emails
|
296
|
+
But be aware that when everyone authenticates via emails, 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 email delivery provider account is compromised, every user in the system is as well. (This is the same for "Forgot password".) [Reddit was once compromised](https://thenextweb.com/hardfork/2018/01/05/reddit-bitcoin-cash-stolen-hack/) using this method.
|
404
297
|
|
405
|
-
Ideally you should set up your email provider to not log these mails. And be sure to turn on 2-factor
|
298
|
+
Ideally you should set up your email provider to not log these mails. And be sure to turn on non-SMS 2-factor authentication if your provider supports it.
|
406
299
|
|
407
|
-
|
300
|
+
## Alternatives
|
408
301
|
|
409
302
|
- [OTP JWT](https://github.com/stas/otp-jwt) -- Passwordless JSON Web Tokens
|
410
303
|
|
411
|
-
|
304
|
+
## License
|
412
305
|
|
413
306
|
MIT
|
data/Rakefile
CHANGED
@@ -2,17 +2,19 @@
|
|
2
2
|
|
3
3
|
begin
|
4
4
|
require "bundler/setup"
|
5
|
+
|
5
6
|
rescue LoadError
|
6
|
-
puts
|
7
|
+
puts("You must `gem install bundler` and `bundle install` to run rake tasks")
|
7
8
|
end
|
8
9
|
|
9
10
|
require "yard"
|
11
|
+
|
10
12
|
YARD::Rake::YardocTask.new
|
11
|
-
task
|
13
|
+
task(docs: :yard)
|
12
14
|
|
13
15
|
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
14
|
-
load
|
15
|
-
load
|
16
|
+
load("rails/tasks/engine.rake")
|
17
|
+
load("rails/tasks/statistics.rake")
|
16
18
|
|
17
19
|
require "bundler/gem_tasks"
|
18
20
|
|
@@ -24,6 +26,4 @@ Rake::TestTask.new(:test) do |t|
|
|
24
26
|
t.verbose = false
|
25
27
|
end
|
26
28
|
|
27
|
-
task
|
28
|
-
|
29
|
-
require "standard/rake"
|
29
|
+
task(default: :test)
|