From d4df0b342e938dd05d0b6c26b0acdfec6c7dbb1f Mon Sep 17 00:00:00 2001 From: Wilbur Suero Date: Mon, 7 Oct 2024 16:27:57 -0400 Subject: [PATCH] Add after_session_confirm hook (#237) --- README.md | 16 ++++++++ .../passwordless/sessions_controller.rb | 7 ++++ lib/passwordless/config.rb | 6 +++ .../passwordless/sessions_controller_test.rb | 38 +++++++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/README.md b/README.md index c33017e..bfccd99 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,8 @@ Passwordless.configure do |config| config.sign_out_redirect_path = '/' # After a user signs out config.paranoid = false # Display email sent notice even when the resource is not found. + + config.after_session_confirm = ->(request, session) {} # Called after a session is confirmed. end ``` @@ -264,6 +266,20 @@ Passwordless.configure do |config| end ``` +## After Session Confirm Hook + +An `after_session_confirm` hook is called after a successful session confirmation – in other words: after a user signs in successfully. + +```ruby +Passwordless.configure do |config| + config.after_session_confirm = ->(session, request) { + user = session.authenticatable + user.update!( + email_verified: true. + last_login_ip: request.remote_ip + ) + } +end ### Token generation 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.: diff --git a/app/controllers/passwordless/sessions_controller.rb b/app/controllers/passwordless/sessions_controller.rb index ce4face..60ce042 100644 --- a/app/controllers/passwordless/sessions_controller.rb +++ b/app/controllers/passwordless/sessions_controller.rb @@ -150,6 +150,7 @@ def artificially_slow_down_brute_force_attacks(token) def authenticate_and_sign_in(session, token) if session.authenticate(token) sign_in(session) + call_after_session_confirm(session, request) redirect_to( passwordless_success_redirect_path(session.authenticatable), status: :see_other, @@ -188,6 +189,12 @@ def call_or_return(value, *args) end end + def call_after_session_confirm(session, request) + return unless Passwordless.config.after_session_confirm.respond_to?(:call) + + Passwordless.config.after_session_confirm.call(session, request) + end + def find_authenticatable if authenticatable_class.respond_to?(:fetch_resource_for_passwordless) authenticatable_class.fetch_resource_for_passwordless(normalized_email_param) diff --git a/lib/passwordless/config.rb b/lib/passwordless/config.rb index e992e0c..7b0c472 100644 --- a/lib/passwordless/config.rb +++ b/lib/passwordless/config.rb @@ -51,6 +51,12 @@ class Configuration option :paranoid, default: false + option( + :after_session_confirm, + default: lambda do |_session, _request| + end + ) + def initialize set_defaults! end diff --git a/test/controllers/passwordless/sessions_controller_test.rb b/test/controllers/passwordless/sessions_controller_test.rb index 6042b17..4ff5109 100644 --- a/test/controllers/passwordless/sessions_controller_test.rb +++ b/test/controllers/passwordless/sessions_controller_test.rb @@ -221,6 +221,44 @@ class << User assert_nil pwless_session(User) end + test("PATCH /:passwordless_for/sign_in/:id -> after_session_confirm with request object") do + user = create_user(email: "test@example.com") + passwordless_session = create_pwless_session(authenticatable: user, token: "valid_token") + + confirm_called = false + + with_config(after_session_confirm: ->(session, request) { + confirm_called = true + assert_equal user, session.authenticatable + assert_kind_of ActionDispatch::Request, request + }) do + patch( + "/users/sign_in/#{passwordless_session.identifier}", + params: {passwordless: {token: "valid_token"}} + ) + end + + assert_equal 303, status + assert confirm_called, "after_session_confirm hook was not called" + end + + test("PATCH /:passwordless_for/sign_in/:id -> after_session_confirm not called on invalid token") do + user = create_user(email: "test@example.com") + passwordless_session = create_pwless_session(authenticatable: user, token: "valid_token") + + confirm_called = false + + with_config(after_session_confirm: ->(_) { confirm_called = true }) do + patch( + "/users/sign_in/#{passwordless_session.identifier}", + params: {passwordless: {token: "invalid_token"}} + ) + end + + assert_equal 403, status + assert_not confirm_called, "after_session_confirm hook was called with invalid token" + end + test("DELETE /:passwordless_for/sign_out") do user = User.create(email: "a@a") passwordless_session = create_pwless_session(authenticatable: user, token: "hi")