passwordless 0.7.0 → 0.10.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 +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: []
|