passwordless 0.8.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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: []