passwordless 1.6.0 → 1.8.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: 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.