passwordless 0.7.0 → 0.10.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 +149 -58
- data/app/controllers/passwordless/sessions_controller.rb +43 -27
- data/app/mailers/passwordless/mailer.rb +2 -2
- data/app/models/passwordless/session.rb +22 -2
- data/config/locales/en.yml +2 -1
- data/db/migrate/20171104221735_create_passwordless_sessions.rb +1 -0
- data/lib/passwordless.rb +13 -1
- data/lib/passwordless/controller_helpers.rb +89 -17
- data/lib/passwordless/errors.rb +11 -0
- data/lib/passwordless/version.rb +1 -1
- metadata +10 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c45a41b4be30c959c37b50ef8013f4055c97ae516e4b8297b4f56607836cc09b
|
4
|
+
data.tar.gz: 7b0c50e087c5be36e5e89476764a407767aee4851fcb38c0b744bd532be8e982
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b35ef690ecd6b37106d785ccb71693606387a8fcc48d5ea115b9d035211b8225d551291d3c64abdb89517cb0ceea17d31807d40ef6f168a9f5df742fd27b8d65
|
7
|
+
data.tar.gz: 8d8ba39b3711905ea36f5f20c009a26b42bcea43ed1bd1185ac56c0053e7afdb6fdf786f123fa8c01b2c095ec3104637d5017791056960293869053ff0b248a3
|
data/README.md
CHANGED
@@ -14,16 +14,19 @@ 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
|
-
|
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
|
+
* [E-mail security](#e-mail-security)
|
27
30
|
* [License](#license)
|
28
31
|
|
29
32
|
## Installation
|
@@ -43,7 +46,7 @@ $ bin/rails passwordless:install:migrations
|
|
43
46
|
|
44
47
|
## Usage
|
45
48
|
|
46
|
-
Passwordless creates a single model called `Passwordless::Session`. It doesn't come with its own `User` model, it expects you to create one
|
49
|
+
Passwordless creates a single model called `Passwordless::Session`. It doesn't come with its own `User` model, it expects you to create one:
|
47
50
|
|
48
51
|
```
|
49
52
|
$ bin/rails generate model User email
|
@@ -71,7 +74,7 @@ end
|
|
71
74
|
|
72
75
|
### Getting the current user, restricting access, the usual
|
73
76
|
|
74
|
-
Passwordless doesn't give you `current_user` automatically
|
77
|
+
Passwordless doesn't give you `current_user` automatically. Here's how you could add it:
|
75
78
|
|
76
79
|
```ruby
|
77
80
|
class ApplicationController < ActionController::Base
|
@@ -84,12 +87,12 @@ class ApplicationController < ActionController::Base
|
|
84
87
|
private
|
85
88
|
|
86
89
|
def current_user
|
87
|
-
@current_user ||=
|
90
|
+
@current_user ||= authenticate_by_session(User)
|
88
91
|
end
|
89
92
|
|
90
93
|
def require_user!
|
91
94
|
return if current_user
|
92
|
-
redirect_to root_path, flash: {error: 'You are not worthy!'}
|
95
|
+
redirect_to root_path, flash: { error: 'You are not worthy!' }
|
93
96
|
end
|
94
97
|
end
|
95
98
|
```
|
@@ -119,39 +122,32 @@ app/views/passwordless/sessions/create.html.erb
|
|
119
122
|
app/views/passwordless/mailer/magic_link.text.erb
|
120
123
|
```
|
121
124
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
Let's say we would like to fetch the record and if it doesn't exist, create automatically.
|
131
|
-
|
132
|
-
```ruby
|
133
|
-
class User < ApplicationRecord
|
134
|
-
def self.fetch_resource_for_passwordless(email)
|
135
|
-
find_or_create_by(email: email)
|
136
|
-
end
|
137
|
-
end
|
125
|
+
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:
|
126
|
+
```erb
|
127
|
+
<% if @resource.present? %>
|
128
|
+
<p>User found, check your inbox</p>
|
129
|
+
<% else %>
|
130
|
+
<p>No user found with the provided email address</p>
|
131
|
+
<% end %>
|
138
132
|
```
|
139
133
|
|
134
|
+
See [the bundled views](https://github.com/mikker/passwordless/tree/master/app/views/passwordless).
|
135
|
+
|
140
136
|
### Registering new users
|
141
137
|
|
142
|
-
Because your `User` record is like any other record, you create one like you normally would. Passwordless provides a helper method
|
138
|
+
Because your `User` record is like any other record, you create one like you normally would. Passwordless provides a helper method to sign in the created user after it is saved – like so:
|
143
139
|
|
144
140
|
```ruby
|
145
141
|
class UsersController < ApplicationController
|
146
142
|
include Passwordless::ControllerHelpers # <-- This!
|
147
|
-
#
|
143
|
+
# (unless you already have it in your ApplicationController)
|
148
144
|
|
149
145
|
def create
|
150
146
|
@user = User.new user_params
|
151
147
|
|
152
148
|
if @user.save
|
153
|
-
sign_in @user # <--
|
154
|
-
redirect_to @user, flash: {notice: 'Welcome!'}
|
149
|
+
sign_in @user # <-- This!
|
150
|
+
redirect_to @user, flash: { notice: 'Welcome!' }
|
155
151
|
else
|
156
152
|
render :new
|
157
153
|
end
|
@@ -161,9 +157,102 @@ class UsersController < ApplicationController
|
|
161
157
|
end
|
162
158
|
```
|
163
159
|
|
164
|
-
###
|
160
|
+
### URLs and links
|
165
161
|
|
166
|
-
By default Passwordless
|
162
|
+
By default, Passwordless uses the resource name given to `passwordless_for` to generate its routes and helpers.
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
passwordless_for :users
|
166
|
+
# <%= users.sign_in_path %> # => /users/sign_in
|
167
|
+
|
168
|
+
passwordless_for :users, at: '/', as: :auth
|
169
|
+
# <%= auth.sign_in_path %> # => /sign_in
|
170
|
+
```
|
171
|
+
|
172
|
+
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).
|
173
|
+
|
174
|
+
### Customize the way to send magic link
|
175
|
+
|
176
|
+
By default, magic link will send by email. You can customize this method. For example, you can send magic link via SMS.
|
177
|
+
|
178
|
+
config/initializers/passwordless.rb
|
179
|
+
|
180
|
+
```
|
181
|
+
Passwordless.after_session_save = lambda do |session, request|
|
182
|
+
# Default behavior is
|
183
|
+
# Passwordless::Mailer.magic_link(session).deliver_now
|
184
|
+
|
185
|
+
# You can change behavior to do something with session model. For example,
|
186
|
+
# session.authenticatable.send_sms
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
You can access user model through authenticatable.
|
191
|
+
|
192
|
+
### Generate your own magic links
|
193
|
+
|
194
|
+
Currently there is not an officially supported way to generate your own magic links to send in your own mailers.
|
195
|
+
|
196
|
+
However, you can accomplish this with the following snippet of code.
|
197
|
+
|
198
|
+
```
|
199
|
+
session = Passwordless::Session.new({
|
200
|
+
authenticatable: @manager,
|
201
|
+
user_agent: 'Command Line',
|
202
|
+
remote_addr: 'unknown',
|
203
|
+
})
|
204
|
+
session.save!
|
205
|
+
@magic_link = send(Passwordless.mounted_as).token_sign_in_url(session.token)
|
206
|
+
```
|
207
|
+
|
208
|
+
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
|
209
|
+
```
|
210
|
+
@magic_link = "#{@magic_link}?destination_path=/your-custom-path"
|
211
|
+
```
|
212
|
+
|
213
|
+
### Overrides
|
214
|
+
|
215
|
+
By default `passwordless` uses the `passwordless_with` column to _case insensitively_ fetch the resource.
|
216
|
+
|
217
|
+
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.
|
218
|
+
|
219
|
+
Example time:
|
220
|
+
|
221
|
+
Let's say we would like to fetch the record and if it doesn't exist, create automatically.
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
class User < ApplicationRecord
|
225
|
+
def self.fetch_resource_for_passwordless(email)
|
226
|
+
find_or_create_by(email: email)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
## Configuration
|
232
|
+
|
233
|
+
The following configuration parameters are supported. You can override these for example in `initializers/passwordless.rb`.
|
234
|
+
|
235
|
+
The default values are shown below. It's recommended to only include the ones that you specifically want to override.
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
Passwordless.default_from_address = "CHANGE_ME@example.com"
|
239
|
+
Passwordless.parent_mailer = "ActionMailer::Base"
|
240
|
+
Passwordless.token_generator = Passwordless::UrlSafeBase64Generator.new # Used to generate magic link tokens.
|
241
|
+
Passwordless.restrict_token_reuse = false # By default a magic link token can be used multiple times.
|
242
|
+
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.
|
243
|
+
|
244
|
+
Passwordless.expires_at = lambda { 1.year.from_now } # How long until a passwordless session expires.
|
245
|
+
Passwordless.timeout_at = lambda { 1.hour.from_now } # How long until a magic link expires.
|
246
|
+
|
247
|
+
# Default redirection paths
|
248
|
+
Passwordless.success_redirect_path = '/' # When a user succeeds in logging in.
|
249
|
+
Passwordless.failure_redirect_path = '/' # When a a login is failed for any reason.
|
250
|
+
Passwordless.sign_out_redirect_path = '/' # When a user logs out.
|
251
|
+
```
|
252
|
+
|
253
|
+
### Customizing token generation
|
254
|
+
|
255
|
+
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.:
|
167
256
|
|
168
257
|
```ruby
|
169
258
|
Passwordless.token_generator = -> (session) {
|
@@ -219,41 +308,39 @@ end
|
|
219
308
|
|
220
309
|
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.
|
221
310
|
|
222
|
-
###
|
311
|
+
### Claiming tokens
|
223
312
|
|
224
|
-
|
313
|
+
Opt-in for marking tokens as `claimed` so they can only be used once.
|
225
314
|
|
226
|
-
|
227
|
-
passwordless_for :users
|
228
|
-
# <%= users.sign_in_path %> # => /users/sign_in
|
315
|
+
config/initializers/passwordless.rb
|
229
316
|
|
230
|
-
|
231
|
-
|
317
|
+
```ruby
|
318
|
+
# Default is `false`
|
319
|
+
Passwordless.restrict_token_reuse = true
|
232
320
|
```
|
233
321
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
### Customize the way to send magic link
|
322
|
+
#### Upgrading an existing Rails app
|
238
323
|
|
239
|
-
|
324
|
+
The simplest way to update your sessions table is with a single migration:
|
240
325
|
|
241
|
-
|
326
|
+
<details>
|
327
|
+
<summary>Example migration</summary>
|
242
328
|
|
329
|
+
```bash
|
330
|
+
bin/rails generate migration add_claimed_at_to_passwordless_sessions
|
243
331
|
```
|
244
|
-
Passwordless.after_session_save = lambda do |session|
|
245
|
-
# Default behavior is
|
246
|
-
# Mailer.magic_link(session).deliver_now
|
247
332
|
|
248
|
-
|
249
|
-
|
333
|
+
```ruby
|
334
|
+
class AddClaimedAtToPasswordlessSessions < ActiveRecord::Migration[5.2]
|
335
|
+
def change
|
336
|
+
add_column :passwordless_sessions, :claimed_at, :datetime
|
337
|
+
end
|
250
338
|
end
|
251
|
-
```
|
252
|
-
|
253
|
-
You can access user model through authenticatable.
|
254
339
|
|
340
|
+
```
|
341
|
+
</details>
|
255
342
|
|
256
|
-
|
343
|
+
## E-mail security
|
257
344
|
|
258
345
|
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.
|
259
346
|
|
@@ -261,6 +348,10 @@ But be aware that when everyone authenticates via emails you send, the way you s
|
|
261
348
|
|
262
349
|
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.
|
263
350
|
|
351
|
+
# Alternatives
|
352
|
+
|
353
|
+
- [OTP JWT](https://github.com/stas/otp-jwt) -- Passwordless JSON Web Tokens
|
354
|
+
|
264
355
|
# License
|
265
356
|
|
266
357
|
MIT
|
@@ -5,9 +5,6 @@ require "bcrypt"
|
|
5
5
|
module Passwordless
|
6
6
|
# Controller for managing Passwordless sessions
|
7
7
|
class SessionsController < ApplicationController
|
8
|
-
# Raise this exception when a session is expired.
|
9
|
-
class SessionTimedOutError < StandardError; end
|
10
|
-
|
11
8
|
include ControllerHelpers
|
12
9
|
|
13
10
|
# get '/sign_in'
|
@@ -23,10 +20,15 @@ module Passwordless
|
|
23
20
|
# renders sessions/create.html.erb.
|
24
21
|
# @see Mailer#magic_link Mailer#magic_link
|
25
22
|
def create
|
26
|
-
|
23
|
+
@resource = find_authenticatable
|
24
|
+
session = build_passwordless_session(@resource)
|
27
25
|
|
28
26
|
if session.save
|
29
|
-
Passwordless.after_session_save.
|
27
|
+
if Passwordless.after_session_save.arity == 2
|
28
|
+
Passwordless.after_session_save.call(session, request)
|
29
|
+
else
|
30
|
+
Passwordless.after_session_save.call(session)
|
31
|
+
end
|
30
32
|
end
|
31
33
|
|
32
34
|
render
|
@@ -41,23 +43,15 @@ module Passwordless
|
|
41
43
|
def show
|
42
44
|
# Make it "slow" on purpose to make brute-force attacks more of a hassle
|
43
45
|
BCrypt::Password.create(params[:token])
|
46
|
+
sign_in passwordless_session
|
44
47
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
redirect_enabled = Passwordless.redirect_back_after_sign_in
|
51
|
-
destination = reset_passwordless_redirect_location!(User)
|
52
|
-
|
53
|
-
if redirect_enabled && destination
|
54
|
-
redirect_to destination
|
55
|
-
else
|
56
|
-
redirect_to main_app.root_path
|
57
|
-
end
|
58
|
-
rescue SessionTimedOutError
|
48
|
+
redirect_to passwordless_success_redirect_path
|
49
|
+
rescue Errors::TokenAlreadyClaimedError
|
50
|
+
flash[:error] = I18n.t(".passwordless.sessions.create.token_claimed")
|
51
|
+
redirect_to passwordless_failure_redirect_path
|
52
|
+
rescue Errors::SessionTimedOutError
|
59
53
|
flash[:error] = I18n.t(".passwordless.sessions.create.session_expired")
|
60
|
-
redirect_to
|
54
|
+
redirect_to passwordless_failure_redirect_path
|
61
55
|
end
|
62
56
|
|
63
57
|
# match '/sign_out', via: %i[get delete].
|
@@ -65,7 +59,31 @@ module Passwordless
|
|
65
59
|
# @see ControllerHelpers#sign_out
|
66
60
|
def destroy
|
67
61
|
sign_out authenticatable_class
|
68
|
-
redirect_to
|
62
|
+
redirect_to passwordless_sign_out_redirect_path
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def passwordless_sign_out_redirect_path
|
68
|
+
Passwordless.sign_out_redirect_path
|
69
|
+
end
|
70
|
+
|
71
|
+
def passwordless_failure_redirect_path
|
72
|
+
Passwordless.failure_redirect_path
|
73
|
+
end
|
74
|
+
|
75
|
+
def passwordless_query_redirect_path
|
76
|
+
query_redirect_uri = URI(params[:destination_path])
|
77
|
+
query_redirect_uri.to_s if query_redirect_uri.host.nil? || query_redirect_uri.host == URI(request.url).host
|
78
|
+
rescue URI::InvalidURIError, ArgumentError
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def passwordless_success_redirect_path
|
83
|
+
return Passwordless.success_redirect_path unless Passwordless.redirect_back_after_sign_in
|
84
|
+
|
85
|
+
session_redirect_url = reset_passwordless_redirect_location!(authenticatable_class)
|
86
|
+
passwordless_query_redirect_path || session_redirect_url || Passwordless.success_redirect_path
|
69
87
|
end
|
70
88
|
|
71
89
|
private
|
@@ -87,19 +105,17 @@ module Passwordless
|
|
87
105
|
end
|
88
106
|
|
89
107
|
def find_authenticatable
|
90
|
-
email = params[:passwordless][email_field].downcase
|
108
|
+
email = params[:passwordless][email_field].downcase.strip
|
91
109
|
|
92
110
|
if authenticatable_class.respond_to?(:fetch_resource_for_passwordless)
|
93
111
|
authenticatable_class.fetch_resource_for_passwordless(email)
|
94
112
|
else
|
95
|
-
authenticatable_class.where(
|
96
|
-
"lower(#{email_field}) = ?", params[:passwordless][email_field].downcase
|
97
|
-
).first
|
113
|
+
authenticatable_class.where("lower(#{email_field}) = ?", email).first
|
98
114
|
end
|
99
115
|
end
|
100
116
|
|
101
|
-
def
|
102
|
-
Session.find_by!(
|
117
|
+
def passwordless_session
|
118
|
+
@passwordless_session ||= Session.find_by!(
|
103
119
|
authenticatable_type: authenticatable_classname,
|
104
120
|
token: params[:token]
|
105
121
|
)
|
@@ -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.
|
@@ -11,7 +11,7 @@ module Passwordless
|
|
11
11
|
@session = session
|
12
12
|
|
13
13
|
@magic_link = send(Passwordless.mounted_as)
|
14
|
-
|
14
|
+
.token_sign_in_url(session.token)
|
15
15
|
|
16
16
|
email_field = @session.authenticatable.class.passwordless_email_field
|
17
17
|
mail(
|
@@ -18,10 +18,17 @@ module Passwordless
|
|
18
18
|
|
19
19
|
before_validation :set_defaults
|
20
20
|
|
21
|
-
scope :
|
22
|
-
where("
|
21
|
+
scope :available, lambda {
|
22
|
+
where("expires_at > ?", Time.current)
|
23
23
|
}
|
24
24
|
|
25
|
+
def self.valid
|
26
|
+
available
|
27
|
+
end
|
28
|
+
class << self
|
29
|
+
deprecate :valid, deprecator: SessionValidDeprecation
|
30
|
+
end
|
31
|
+
|
25
32
|
def expired?
|
26
33
|
expires_at <= Time.current
|
27
34
|
end
|
@@ -30,6 +37,19 @@ module Passwordless
|
|
30
37
|
timeout_at <= Time.current
|
31
38
|
end
|
32
39
|
|
40
|
+
def claim!
|
41
|
+
raise Errors::TokenAlreadyClaimedError if claimed?
|
42
|
+
touch(:claimed_at)
|
43
|
+
end
|
44
|
+
|
45
|
+
def claimed?
|
46
|
+
!!claimed_at
|
47
|
+
end
|
48
|
+
|
49
|
+
def available?
|
50
|
+
!expired?
|
51
|
+
end
|
52
|
+
|
33
53
|
private
|
34
54
|
|
35
55
|
def set_defaults
|
data/config/locales/en.yml
CHANGED
@@ -5,8 +5,9 @@ en:
|
|
5
5
|
create:
|
6
6
|
session_expired: 'Your session has expired, please sign in again.'
|
7
7
|
email_sent_if_record_found: "If we found you in the system, we've sent you an email."
|
8
|
+
token_claimed: "This link has already been used, try requesting the link again"
|
8
9
|
new:
|
9
10
|
submit: 'Send magic link'
|
10
11
|
mailer:
|
11
|
-
subject: "Your magic link ✨
|
12
|
+
subject: "Your magic link ✨"
|
12
13
|
magic_link: "Here's your link: %{link}"
|
@@ -10,6 +10,7 @@ class CreatePasswordlessSessions < ActiveRecord::Migration[5.1]
|
|
10
10
|
)
|
11
11
|
t.datetime :timeout_at, null: false
|
12
12
|
t.datetime :expires_at, null: false
|
13
|
+
t.datetime :claimed_at
|
13
14
|
t.text :user_agent, null: false
|
14
15
|
t.string :remote_addr, null: false
|
15
16
|
t.string :token, null: false
|
data/lib/passwordless.rb
CHANGED
@@ -1,17 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_support"
|
4
|
+
require "passwordless/errors"
|
3
5
|
require "passwordless/engine"
|
4
6
|
require "passwordless/url_safe_base_64_generator"
|
5
7
|
|
6
8
|
# The main Passwordless module
|
7
9
|
module Passwordless
|
10
|
+
mattr_accessor(:parent_mailer) { "ActionMailer::Base" }
|
8
11
|
mattr_accessor(:default_from_address) { "CHANGE_ME@example.com" }
|
9
12
|
mattr_accessor(:token_generator) { UrlSafeBase64Generator.new }
|
13
|
+
mattr_accessor(:restrict_token_reuse) { false }
|
10
14
|
mattr_accessor(:redirect_back_after_sign_in) { true }
|
11
15
|
mattr_accessor(:mounted_as) { :configured_when_mounting_passwordless }
|
12
16
|
|
13
17
|
mattr_accessor(:expires_at) { lambda { 1.year.from_now } }
|
14
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) { "/" }
|
15
22
|
|
16
|
-
mattr_accessor(:after_session_save)
|
23
|
+
mattr_accessor(:after_session_save) do
|
24
|
+
lambda { |session, _request| Mailer.magic_link(session).deliver_now }
|
25
|
+
end
|
26
|
+
|
27
|
+
CookieDeprecation = ActiveSupport::Deprecation.new("0.9", "passwordless")
|
28
|
+
SessionValidDeprecation = ActiveSupport::Deprecation.new("0.9", "passwordless")
|
17
29
|
end
|
@@ -3,6 +3,12 @@
|
|
3
3
|
module Passwordless
|
4
4
|
# Helpers to work with Passwordless sessions from controllers
|
5
5
|
module ControllerHelpers
|
6
|
+
# Returns the {Passwordless::Session} (if set) from the session.
|
7
|
+
# @return [Session, nil]
|
8
|
+
def find_passwordless_session_for(authenticatable_class)
|
9
|
+
Passwordless::Session.find_by(id: session[session_key(authenticatable_class)])
|
10
|
+
end
|
11
|
+
|
6
12
|
# Build a new Passwordless::Session from an _authenticatable_ record.
|
7
13
|
# Set's `user_agent` and `remote_addr` from Rails' `request`.
|
8
14
|
# @param authenticatable [ActiveRecord::Base] Instance of an
|
@@ -17,6 +23,7 @@ module Passwordless
|
|
17
23
|
end
|
18
24
|
end
|
19
25
|
|
26
|
+
# @deprecated Use {ControllerHelpers#authenticate_by_session}
|
20
27
|
# Authenticate a record using cookies. Looks for a cookie corresponding to
|
21
28
|
# the _authenticatable_class_. If found try to find it in the database.
|
22
29
|
# @param authenticatable_class [ActiveRecord::Base] any Model connected to
|
@@ -27,54 +34,119 @@ module Passwordless
|
|
27
34
|
def authenticate_by_cookie(authenticatable_class)
|
28
35
|
key = cookie_name(authenticatable_class)
|
29
36
|
authenticatable_id = cookies.encrypted[key]
|
30
|
-
return unless authenticatable_id
|
31
37
|
|
32
|
-
authenticatable_class.find_by(id: authenticatable_id)
|
38
|
+
return authenticatable_class.find_by(id: authenticatable_id) if authenticatable_id
|
39
|
+
|
40
|
+
authenticate_by_session(authenticatable_class)
|
41
|
+
end
|
42
|
+
deprecate :authenticate_by_cookie, deprecator: CookieDeprecation
|
43
|
+
|
44
|
+
def upgrade_passwordless_cookie(authenticatable_class)
|
45
|
+
key = cookie_name(authenticatable_class)
|
46
|
+
|
47
|
+
return unless (authenticatable_id = cookies.encrypted[key])
|
48
|
+
cookies.encrypted.permanent[key] = {value: nil}
|
49
|
+
cookies.delete(key)
|
50
|
+
|
51
|
+
return unless (record = authenticatable_class.find_by(id: authenticatable_id))
|
52
|
+
new_session = build_passwordless_session(record).tap { |s| s.save! }
|
53
|
+
|
54
|
+
sign_in new_session
|
55
|
+
|
56
|
+
new_session.authenticatable
|
57
|
+
end
|
58
|
+
|
59
|
+
# Authenticate a record using the session. Looks for a session key corresponding to
|
60
|
+
# the _authenticatable_class_. If found try to find it in the database.
|
61
|
+
# @param authenticatable_class [ActiveRecord::Base] any Model connected to
|
62
|
+
# passwordless. (e.g - _User_ or _Admin_).
|
63
|
+
# @return [ActiveRecord::Base|nil] an instance of Model found by id stored
|
64
|
+
# in cookies.encrypted or nil if nothing is found.
|
65
|
+
# @see ModelHelpers#passwordless_with
|
66
|
+
def authenticate_by_session(authenticatable_class)
|
67
|
+
return unless find_passwordless_session_for(authenticatable_class)&.available?
|
68
|
+
find_passwordless_session_for(authenticatable_class).authenticatable
|
33
69
|
end
|
34
70
|
|
35
|
-
# Signs in
|
36
|
-
# @param authenticatable [
|
37
|
-
#
|
71
|
+
# Signs in session
|
72
|
+
# @param authenticatable [Passwordless::Session] Instance of {Passwordless::Session}
|
73
|
+
# to sign in
|
38
74
|
# @return [ActiveRecord::Base] the record that is passed in.
|
39
|
-
def sign_in(
|
40
|
-
|
41
|
-
|
42
|
-
|
75
|
+
def sign_in(record)
|
76
|
+
passwordless_session =
|
77
|
+
if record.is_a?(Passwordless::Session)
|
78
|
+
record
|
79
|
+
else
|
80
|
+
warn "Passwordless::ControllerHelpers#sign_in with authenticatable " \
|
81
|
+
"(`#{record.class}') is deprecated. Falling back to creating a " \
|
82
|
+
"new Passwordless::Session"
|
83
|
+
build_passwordless_session(record).tap { |s| s.save! }
|
84
|
+
end
|
85
|
+
|
86
|
+
passwordless_session.claim! if Passwordless.restrict_token_reuse
|
87
|
+
|
88
|
+
raise Passwordless::Errors::SessionTimedOutError if passwordless_session.timed_out?
|
89
|
+
|
90
|
+
key = session_key(passwordless_session.authenticatable_type)
|
91
|
+
session[key] = passwordless_session.id
|
92
|
+
|
93
|
+
if record.is_a?(Passwordless::Session)
|
94
|
+
passwordless_session
|
95
|
+
else
|
96
|
+
passwordless_session.authenticatable
|
97
|
+
end
|
43
98
|
end
|
44
99
|
|
45
|
-
# Signs out user by deleting
|
46
|
-
# @param (see #
|
100
|
+
# Signs out user by deleting the session key.
|
101
|
+
# @param (see #authenticate_by_session)
|
47
102
|
# @return [boolean] Always true
|
48
103
|
def sign_out(authenticatable_class)
|
104
|
+
# Deprecated - cookies
|
49
105
|
key = cookie_name(authenticatable_class)
|
50
106
|
cookies.encrypted.permanent[key] = {value: nil}
|
51
107
|
cookies.delete(key)
|
108
|
+
# /deprecated
|
109
|
+
|
110
|
+
reset_session
|
52
111
|
true
|
53
112
|
end
|
54
113
|
|
55
114
|
# Saves request.original_url as the redirect location for a
|
56
115
|
# passwordless Model.
|
57
|
-
# @param (see #
|
116
|
+
# @param (see #authenticate_by_session)
|
58
117
|
# @return [String] the redirect url that was just saved.
|
59
118
|
def save_passwordless_redirect_location!(authenticatable_class)
|
60
|
-
session[
|
119
|
+
session[redirect_session_key(authenticatable_class)] = request.original_url
|
61
120
|
end
|
62
121
|
|
63
122
|
# Resets the redirect_location to root_path by deleting the redirect_url
|
64
123
|
# from session.
|
65
|
-
# @param (see #
|
124
|
+
# @param (see #authenticate_by_session)
|
66
125
|
# @return [String, nil] the redirect url that was just deleted,
|
67
126
|
# or nil if no url found for given Model.
|
68
127
|
def reset_passwordless_redirect_location!(authenticatable_class)
|
69
|
-
session.delete
|
128
|
+
session.delete(redirect_session_key(authenticatable_class))
|
129
|
+
end
|
130
|
+
|
131
|
+
def session_key(authenticatable_class)
|
132
|
+
:"passwordless_session_id--#{authenticatable_class_parameterized(authenticatable_class)}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def redirect_session_key(authenticatable_class)
|
136
|
+
:"passwordless_prev_location--#{authenticatable_class_parameterized(authenticatable_class)}"
|
70
137
|
end
|
71
138
|
|
72
139
|
private
|
73
140
|
|
74
|
-
def
|
75
|
-
|
141
|
+
def authenticatable_class_parameterized(authenticatable_class)
|
142
|
+
if authenticatable_class.is_a?(String)
|
143
|
+
authenticatable_class = authenticatable_class.constantize
|
144
|
+
end
|
145
|
+
|
146
|
+
authenticatable_class.base_class.to_s.parameterize
|
76
147
|
end
|
77
148
|
|
149
|
+
# Deprecated
|
78
150
|
def cookie_name(authenticatable_class)
|
79
151
|
:"#{authenticatable_class.base_class.to_s.underscore}_id"
|
80
152
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Passwordless
|
4
|
+
module Errors
|
5
|
+
# Raise this exception when a session is expired.
|
6
|
+
class SessionTimedOutError < StandardError; end
|
7
|
+
|
8
|
+
# Raise this exception when the token has been previously claimed
|
9
|
+
class TokenAlreadyClaimedError < StandardError; end
|
10
|
+
end
|
11
|
+
end
|
data/lib/passwordless/version.rb
CHANGED
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.10.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: 2020-10-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 1.
|
47
|
+
version: 1.4.1
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 1.
|
54
|
+
version: 1.4.1
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: yard
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -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: []
|
@@ -104,6 +104,7 @@ files:
|
|
104
104
|
- lib/passwordless.rb
|
105
105
|
- lib/passwordless/controller_helpers.rb
|
106
106
|
- lib/passwordless/engine.rb
|
107
|
+
- lib/passwordless/errors.rb
|
107
108
|
- lib/passwordless/model_helpers.rb
|
108
109
|
- lib/passwordless/router_helpers.rb
|
109
110
|
- lib/passwordless/url_safe_base_64_generator.rb
|
@@ -112,7 +113,7 @@ homepage: https://github.com/mikker/passwordless
|
|
112
113
|
licenses:
|
113
114
|
- MIT
|
114
115
|
metadata: {}
|
115
|
-
post_install_message:
|
116
|
+
post_install_message:
|
116
117
|
rdoc_options: []
|
117
118
|
require_paths:
|
118
119
|
- lib
|
@@ -127,8 +128,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
128
|
- !ruby/object:Gem::Version
|
128
129
|
version: '0'
|
129
130
|
requirements: []
|
130
|
-
rubygems_version: 3.
|
131
|
-
signing_key:
|
131
|
+
rubygems_version: 3.1.4
|
132
|
+
signing_key:
|
132
133
|
specification_version: 4
|
133
134
|
summary: Add authentication to your app without all the ickyness of passwords.
|
134
135
|
test_files: []
|