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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b19e22b9a7152b299c8f639339709ebeaa3616ecfd7a85f461af205d8f6ad635
4
- data.tar.gz: 8858c326d1457db832e08909957948d49d525bde1d45effa52e4d8e9b22f0374
3
+ metadata.gz: bfea8774f73a80e003f7257caa02afc7c1475a87077a4881b2c1e85442ebf8e9
4
+ data.tar.gz: 4882a066aa2ecc18a4a170e697014b5f09b9bde58c32e821eb60944c9fb90b13
5
5
  SHA512:
6
- metadata.gz: 146b741cd502a702a10ed5169575eb5d6e9c47b0be8469e74b4e2369d2ecb82c1845624afe04aac2cac4e701deb6ea726e916dc1019007bdacd48e510272c9b7
7
- data.tar.gz: f95c455c20f127c1a86c0df6763afdf2f90312afd429b81f96b8dd1e0901ac17c7cb3e8c1ff38d858d5208149eedf8257eff0b1e40809aa58c2189df58d1f90b
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
- [![Travis](https://travis-ci.org/mikker/passwordless.svg?branch=master)](https://travis-ci.org/mikker/passwordless) [![Rubygems](https://img.shields.io/gem/v/passwordless.svg)](https://rubygems.org/gems/passwordless) [![codecov](https://codecov.io/gh/mikker/passwordless/branch/master/graph/badge.svg)](https://codecov.io/gh/mikker/passwordless) [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
7
+ [![CI](https://github.com/mikker/passwordless/actions/workflows/ci.yml/badge.svg)](https://github.com/mikker/passwordless/actions/workflows/ci.yml) [![Rubygems](https://img.shields.io/gem/v/passwordless.svg)](https://rubygems.org/gems/passwordless) [![codecov](https://codecov.io/gh/mikker/passwordless/branch/master/graph/badge.svg)](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
- * [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
- * [Claming tokens](#claiming-tokens)
20
- * [Overrides](#overrides)
21
- * [Registering new users](#registering-new-users)
22
- * [Generating tokens](#generating-tokens)
23
- * [Token and Session Expiry](#token-and-session-expiry)
24
- * [Redirecting back after sign-in](#redirecting-back-after-sign-in)
25
- * [URLs and links](#urls-and-links)
26
- * [Customize the way to send magic link](#customize-the-way-to-send-magic-link)
27
- * [E-mail security](#e-mail-security)
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. `passwordless` has 2 action views and 1 mailer view:
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
- ### Generating tokens
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
- ### Overrides
349
+ ### Supporting UUID primary keys
273
350
 
274
- By default `passwordless` uses the `passwordless_with` column to _case insensitively_ fetch the resource.
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
- 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.
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
- Example time:
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
- Let's say we would like to fetch the record and if it doesn't exist, create automatically.
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
- class User < ApplicationRecord
284
- def self.fetch_resource_for_passwordless(email)
285
- find_or_create_by(email: email)
286
- end
287
- end
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
- ### E-mail security
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
- session = build_passwordless_session(find_authenticatable)
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
- render
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
- destination =
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 main_app.root_path
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 main_app.root_path
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 authenticatable_class
66
- redirect_to main_app.root_path
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 < ActionMailer::Base
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 :authenticatable,
8
- polymorphic: true, inverse_of: :passwordless_sessions
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 :available, lambda {
22
- where("expires_at > ?", Time.current)
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 "/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
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 :passwordless_sessions do |t|
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
- t.datetime :timeout_at, null: false
12
- t.datetime :expires_at, null: false
13
- t.datetime :claimed_at
14
- t.text :user_agent, null: false
15
- t.string :remote_addr, null: false
16
- t.string :token, null: false
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 new_session
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
- if record.is_a?(Passwordless::Session)
78
- record
79
- else
80
- warn "Passwordless::ControllerHelpers#sign_in with authenticatable " \
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
- build_passwordless_session(record).tap { |s| s.save! }
84
- end
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
@@ -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
@@ -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; end
6
+ class SessionTimedOutError < StandardError
7
+ end
7
8
 
8
9
  # Raise this exception when the token has been previously claimed
9
- class TokenAlreadyClaimedError < StandardError; end
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 :passwordless_sessions,
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, at: mount_at, as: mount_as,
24
- defaults: {authenticatable: resource.to_s.singularize}
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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Passwordless
4
- VERSION = "0.8.2" # :nodoc:
4
+ # :nodoc:
5
+ VERSION = "0.11.0"
5
6
  end
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.8.2
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: 2019-08-30 00:00:00.000000000 Z
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.0.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: []