passwordless 1.6.0 → 1.8.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: 44758dfedcb5f0b737a6892abdb754751f14d4da6d0c4e7e3ae9fa41d1b544ee
4
- data.tar.gz: 01e6534d9d3f8e11ff1f13e908c62a096fc32129a1f34df5a75defa97b7a0b31
3
+ metadata.gz: b0e00cb666dc0018cb5789bf9f3b9186e7d107631365847374aa718d547f872e
4
+ data.tar.gz: dd7cf0cd6c95151f3987e827693f3bb78811b6eba50661a6c822a54e0788b908
5
5
  SHA512:
6
- metadata.gz: 29e04ae936111350bf9673b2fd38f88e47006b72bcdd4aac5a38595623ab93558ab03e0a62677197d21f56e5370f38cc9fa5b5cc44bd8d8bdecf43e5ee66932d
7
- data.tar.gz: 0701f4569d5e37ca19d42eb1afe00bd4b99fb68a1e2951ff9ec419221d32fb559e012cbc80115013b94cb316b5b98c70f32e862ce448cc2985464a39f73d9e4a
6
+ metadata.gz: 664906f92b865e89863f5a886bacc119a1ccc1970eead36d7496a12545a8ce022553f30982bc4b8b852fee9b6cdb4580ae01e4c37d56e59ae52b026c836bbebc
7
+ data.tar.gz: b02a2dd05dae19dd283a455da70690c8086bfd6958f78986b7b8ac6442fa31bdc627362e12a59f19c9e51f1fa15a7e47fced64c09529ed3c2aa0454cd33bb84a
data/README.md CHANGED
@@ -8,7 +8,24 @@
8
8
 
9
9
  Add authentication to your Rails app without all the icky-ness of passwords. _Magic link_ authentication, if you will. We call it _passwordless_.
10
10
 
11
- ---
11
+ - [Installation](#installation)
12
+ - [Upgrading](#upgrading)
13
+ - [Usage](#usage)
14
+ - [Getting the current user, restricting access, the usual](#getting-the-current-user-restricting-access-the-usual)
15
+ - [Providing your own templates](#providing-your-own-templates)
16
+ - [Registering new users](#registering-new-users)
17
+ - [URLs and links](#urls-and-links)
18
+ - [Route constraints](#route-constraints)
19
+ - [Configuration](#configuration)
20
+ - [Delivery method](#delivery-method)
21
+ - [Token generation](#token-generation)
22
+ - [Timeout and Expiry](#timeout-and-expiry)
23
+ - [Redirection after sign-in](#redirection-after-sign-in)
24
+ - [Looking up the user](#looking-up-the-user)
25
+ - [Test helpers](#test-helpers)
26
+ - [Security considerations](#security-considerations)
27
+ - [Alternatives](#alternatives)
28
+ - [License](#license)
12
29
 
13
30
  ## Installation
14
31
 
@@ -149,6 +166,59 @@ config.action_mailer.default_url_options = {host: "www.example.com"}
149
166
  routes.default_url_options[:host] ||= "www.example.com"
150
167
  ```
151
168
 
169
+ Note as well that `passwordless_for` accepts a custom controller. One possible application of this
170
+ is to add a `before_action` that redirects authenticated users from the sign-in routes, as in this example:
171
+
172
+
173
+ ```ruby
174
+ # config/routes.rb
175
+ passwordless_for :users, controller: "sessions"
176
+ ```
177
+
178
+ ```ruby
179
+ # app/controllers/sessions_controller.rb
180
+
181
+ class SessionsController < Passwordless::SessionsController
182
+ before_action :require_unauth!, only: %i[new show]
183
+
184
+ private
185
+
186
+ def require_unauth!
187
+ return unless current_user
188
+ redirect_to("/", notice: "You are already signed in.")
189
+ end
190
+ end
191
+ ```
192
+
193
+ ### Route constraints
194
+
195
+ With [constraints](https://guides.rubyonrails.org/routing.html#request-based-constraints) you can restrict access to certain routes.
196
+ Passwordless provides `Passwordless::Constraint` and it's negative counterpart `Passwordless::ConstraintNot` for this purpose.
197
+
198
+ To limit a route to only authenticated `User`s:
199
+
200
+ ```ruby
201
+ constraints Passwordless::Constraint.new(User) do
202
+ # ...
203
+ end
204
+ ```
205
+
206
+ The constraint takes a second `if:` argument, that expects a block and is passed the `authenticatable` record, (ie. `User`):
207
+
208
+ ```ruby
209
+ constraints Passwordless::Constraint.new(User, if: -> (user) { user.email.include?("john") }) do
210
+ # ...
211
+ end
212
+ ```
213
+
214
+ The negated version has the same API but with the opposite result, ie. ensuring authenticated user **don't** have access:
215
+
216
+ ```ruby
217
+ constraints Passwordless::ConstraintNot.new(User) do
218
+ get("/no-users-allowed", to: "secrets#index")
219
+ end
220
+ ```
221
+
152
222
  ## Configuration
153
223
 
154
224
  To customize Passwordless, create a file `config/initializers/passwordless.rb`.
@@ -160,7 +230,7 @@ Passwordless.configure do |config|
160
230
  config.default_from_address = "CHANGE_ME@example.com"
161
231
  config.parent_controller = "ApplicationController"
162
232
  config.parent_mailer = "ActionMailer::Base"
163
- config.restrict_token_reuse = false # Can a token/link be used multiple times?
233
+ config.restrict_token_reuse = true # Can a token/link be used multiple times?
164
234
  config.token_generator = Passwordless::ShortTokenGenerator.new # Used to generate magic link tokens.
165
235
 
166
236
  config.expires_at = lambda { 1.year.from_now } # How long until a signed in session expires.
@@ -173,6 +243,8 @@ Passwordless.configure do |config|
173
243
  config.sign_out_redirect_path = '/' # After a user signs out
174
244
 
175
245
  config.paranoid = false # Display email sent notice even when the resource is not found.
246
+
247
+ config.after_session_confirm = ->(request, session) {} # Called after a session is confirmed.
176
248
  end
177
249
  ```
178
250
 
@@ -194,6 +266,20 @@ Passwordless.configure do |config|
194
266
  end
195
267
  ```
196
268
 
269
+ ## After Session Confirm Hook
270
+
271
+ An `after_session_confirm` hook is called after a successful session confirmation – in other words: after a user signs in successfully.
272
+
273
+ ```ruby
274
+ Passwordless.configure do |config|
275
+ config.after_session_confirm = ->(session, request) {
276
+ user = session.authenticatable
277
+ user.update!(
278
+ email_verified: true.
279
+ last_login_ip: request.remote_ip
280
+ )
281
+ }
282
+ end
197
283
  ### Token generation
198
284
 
199
285
  By default Passwordless generates short, 6-digit, alpha numeric tokens. You can change the generator using `Passwordless.config.token_generator` to something else that responds to `call(session)` eg.:
@@ -150,6 +150,7 @@ module Passwordless
150
150
  def authenticate_and_sign_in(session, token)
151
151
  if session.authenticate(token)
152
152
  sign_in(session)
153
+ call_after_session_confirm(session, request)
153
154
  redirect_to(
154
155
  passwordless_success_redirect_path(session.authenticatable),
155
156
  status: :see_other,
@@ -188,6 +189,12 @@ module Passwordless
188
189
  end
189
190
  end
190
191
 
192
+ def call_after_session_confirm(session, request)
193
+ return unless Passwordless.config.after_session_confirm.respond_to?(:call)
194
+
195
+ Passwordless.config.after_session_confirm.call(session, request)
196
+ end
197
+
191
198
  def find_authenticatable
192
199
  if authenticatable_class.respond_to?(:fetch_resource_for_passwordless)
193
200
  authenticatable_class.fetch_resource_for_passwordless(normalized_email_param)
@@ -26,7 +26,7 @@ module Passwordless
26
26
 
27
27
  mail(
28
28
  to: session.authenticatable.send(email_field),
29
- subject: I18n.t("passwordless.mailer.sign_in.subject")
29
+ subject: I18n.t("passwordless.mailer.sign_in.subject", token: @token)
30
30
  )
31
31
  end
32
32
  end
@@ -6,6 +6,7 @@ class CreatePasswordlessSessions < ActiveRecord::Migration[6.0]
6
6
  t.belongs_to(
7
7
  :authenticatable,
8
8
  polymorphic: true,
9
+ type: :int, # change to e.g. :uuid if your model doesn't use integer IDs
9
10
  index: {name: "authenticatable"}
10
11
  )
11
12
 
@@ -51,6 +51,12 @@ module Passwordless
51
51
 
52
52
  option :paranoid, default: false
53
53
 
54
+ option(
55
+ :after_session_confirm,
56
+ default: lambda do |_session, _request|
57
+ end
58
+ )
59
+
54
60
  def initialize
55
61
  set_defaults!
56
62
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "passwordless/controller_helpers"
4
+
5
+ module Passwordless
6
+ # A class the constraint routes to authenticated records
7
+ class Constraint
8
+ include ControllerHelpers
9
+
10
+ attr_reader :authenticatable_type, :predicate, :session
11
+
12
+ # @param [Class] authenticatable_type Authenticatable class
13
+ # @option options [Proc] :if A lambda that takes an authenticatable and returns a boolean
14
+ def initialize(authenticatable_type, **options)
15
+ @authenticatable_type = authenticatable_type
16
+ # `if' is a keyword but so we do this instead of keyword arguments
17
+ @predicate = options.fetch(:if) { -> (_) { true } }
18
+ end
19
+
20
+ def matches?(request)
21
+ # used in authenticate_by_session
22
+ @session = request.session
23
+ authenticatable = authenticate_by_session(authenticatable_type)
24
+ !!(authenticatable && predicate.call(authenticatable))
25
+ end
26
+ end
27
+
28
+ # A class the constraint routes to NOT authenticated records
29
+ class ConstraintNot < Constraint
30
+ # @param [Class] authenticatable_type Authenticatable class
31
+ # @option options [Proc] :if A lambda that takes an authenticatable and returns a boolean
32
+ def initialize(authenticatable_type, **options)
33
+ super
34
+ end
35
+
36
+ def matches?(request)
37
+ !super
38
+ end
39
+ end
40
+ end
@@ -54,7 +54,7 @@ module Passwordless
54
54
  end
55
55
 
56
56
  # Signs in session
57
- # @param authenticatable [Passwordless::Session] Instance of {Passwordless::Session}
57
+ # @param passwordless_session [Passwordless::Session] Instance of {Passwordless::Session}
58
58
  # to sign in
59
59
  # @return [ActiveRecord::Base] the record that is passed in.
60
60
  def sign_in(passwordless_session)
@@ -11,9 +11,5 @@ module Passwordless
11
11
  ActionDispatch::Routing::Mapper.include RouterHelpers
12
12
  ActiveRecord::Base.extend ModelHelpers
13
13
  end
14
-
15
- config.before_initialize do |app|
16
- app.config.i18n.load_path += Dir[Engine.root.join("config", "locales", "*.yml")]
17
- end
18
14
  end
19
15
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Passwordless
4
4
  # :nodoc:
5
- VERSION = "1.6.0"
5
+ VERSION = "1.8.0"
6
6
  end
data/lib/passwordless.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "active_support"
4
4
  require "passwordless/config"
5
5
  require "passwordless/context"
6
+ require "passwordless/constraint"
6
7
  require "passwordless/errors"
7
8
  require "passwordless/engine"
8
9
  require "passwordless/token_digest"
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: 1.6.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikkel Malmberg
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-25 00:00:00.000000000 Z
11
+ date: 2024-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -62,6 +62,7 @@ files:
62
62
  - lib/generators/passwordless/views_generator.rb
63
63
  - lib/passwordless.rb
64
64
  - lib/passwordless/config.rb
65
+ - lib/passwordless/constraint.rb
65
66
  - lib/passwordless/context.rb
66
67
  - lib/passwordless/controller_helpers.rb
67
68
  - lib/passwordless/engine.rb
@@ -91,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
92
  - !ruby/object:Gem::Version
92
93
  version: '0'
93
94
  requirements: []
94
- rubygems_version: 3.5.9
95
+ rubygems_version: 3.5.22
95
96
  signing_key:
96
97
  specification_version: 4
97
98
  summary: Add authentication to your app without all the ickyness of passwords.