From 1af6d1c1216b4c8871b23c61628f3009186ac874 Mon Sep 17 00:00:00 2001 From: Wilbur Suero Date: Mon, 7 Oct 2024 03:13:44 -0400 Subject: [PATCH 1/7] add after_session_confirm hook --- README.md | 2 ++ app/controllers/passwordless/sessions_controller.rb | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/README.md b/README.md index c33017e..538891d 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 = ->(session) {} # Called after a session is confirmed. end ``` diff --git a/app/controllers/passwordless/sessions_controller.rb b/app/controllers/passwordless/sessions_controller.rb index ce4face..0b5d6bd 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) redirect_to( passwordless_success_redirect_path(session.authenticatable), status: :see_other, @@ -188,6 +189,16 @@ def call_or_return(value, *args) end end + def call_after_session_confirm(session) + return unless Passwordless.config.after_session_confirm.respond_to?(:call) + + if Passwordless.config.after_session_confirm.arity == 2 + Passwordless.config.after_session_confirm.call(session, request) + else + Passwordless.config.after_session_confirm.call(session) + end + end + def find_authenticatable if authenticatable_class.respond_to?(:fetch_resource_for_passwordless) authenticatable_class.fetch_resource_for_passwordless(normalized_email_param) From 3217872e30c4736f3c72b830af6068116d122a10 Mon Sep 17 00:00:00 2001 From: Wilbur Suero Date: Mon, 7 Oct 2024 03:22:31 -0400 Subject: [PATCH 2/7] add test and documentation in README --- README.md | 43 +++++++++++++ .../passwordless/sessions_controller_test.rb | 64 +++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/README.md b/README.md index 538891d..bb2c901 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,49 @@ Passwordless.configure do |config| end ``` +## After Session Confirmation Hook +Passwordless now supports an after_session_confirm hook that allows you to perform custom actions after a session has been successfully confirmed. This is particularly useful for tasks such as marking an email as verified or updating user attributes after their first successful login. + +#### Configuration +You can configure the after_session_confirm hook in your Passwordless configuration: + +```ruby +Passwordless.configure do |config| + # ... other configuration options ... + + config.after_session_confirm = ->(session) { + # Your custom logic here + user = session.authenticatable + user.update(email_verified: true) + } +end +``` + +#### Usage +The after_session_confirm hook is called automatically after a successful session confirmation. It receives the session object as an argument, which you can use to access the authenticated user (via session.authenticatable) and perform any necessary operations. + +**Example: Marking an Email as Verified** + +```ruby +Passwordless.configure do |config| + config.after_session_confirm = ->(session) { + user = session.authenticatable + user.update(email_verified: true) + } +end +``` + +**Example: Updating Last Login Timestamp** + +```ruby +Passwordless.configure do |config| + config.after_session_confirm = ->(session) { + user = session.authenticatable + user.update(last_login_at: Time.current) + } +end +``` + ### Delivery method By default, Passwordless sends emails. See [Providing your own templates](#providing-your-own-templates). If you need to customize this further, you can do so in the `after_session_save` callback. diff --git a/test/controllers/passwordless/sessions_controller_test.rb b/test/controllers/passwordless/sessions_controller_test.rb index 6042b17..7fbcc9e 100644 --- a/test/controllers/passwordless/sessions_controller_test.rb +++ b/test/controllers/passwordless/sessions_controller_test.rb @@ -221,6 +221,70 @@ class << User assert_nil pwless_session(User) end + test("PATCH /:passwordless_for/sign_in/:id -> SUCCESS with after_session_confirm") 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) { + confirm_called = true + assert_equal user, session.authenticatable + }) 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" + + follow_redirect! + assert_equal 200, status + assert_equal "/", path + + assert_equal pwless_session(User), Session.last!.id + 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") From 71237a7f312e3ae89e6b814314b0b73ede5ccfe4 Mon Sep 17 00:00:00 2001 From: Wilbur Suero Date: Mon, 7 Oct 2024 08:27:56 -0400 Subject: [PATCH 3/7] always send request, session in config --- README.md | 61 ++++++------------- .../passwordless/sessions_controller.rb | 10 +-- .../passwordless/sessions_controller_test.rb | 26 -------- 3 files changed, 21 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index bb2c901..0b1ef44 100644 --- a/README.md +++ b/README.md @@ -248,49 +248,6 @@ Passwordless.configure do |config| end ``` -## After Session Confirmation Hook -Passwordless now supports an after_session_confirm hook that allows you to perform custom actions after a session has been successfully confirmed. This is particularly useful for tasks such as marking an email as verified or updating user attributes after their first successful login. - -#### Configuration -You can configure the after_session_confirm hook in your Passwordless configuration: - -```ruby -Passwordless.configure do |config| - # ... other configuration options ... - - config.after_session_confirm = ->(session) { - # Your custom logic here - user = session.authenticatable - user.update(email_verified: true) - } -end -``` - -#### Usage -The after_session_confirm hook is called automatically after a successful session confirmation. It receives the session object as an argument, which you can use to access the authenticated user (via session.authenticatable) and perform any necessary operations. - -**Example: Marking an Email as Verified** - -```ruby -Passwordless.configure do |config| - config.after_session_confirm = ->(session) { - user = session.authenticatable - user.update(email_verified: true) - } -end -``` - -**Example: Updating Last Login Timestamp** - -```ruby -Passwordless.configure do |config| - config.after_session_confirm = ->(session) { - user = session.authenticatable - user.update(last_login_at: Time.current) - } -end -``` - ### Delivery method By default, Passwordless sends emails. See [Providing your own templates](#providing-your-own-templates). If you need to customize this further, you can do so in the `after_session_save` callback. @@ -309,6 +266,24 @@ Passwordless.configure do |config| end ``` +## After Session Confirmation Hook +The after_session_confirm hook is called automatically after a successful session confirmation. It receives the request and session objects as arguments, which you can use to access the authenticated user (via session.authenticatable) and perform any necessary operations. + +You can configure the after_session_confirm hook in your Passwordless configuration: + +```ruby +Passwordless.configure do |config| + # ... other configuration options ... + + config.after_session_confirm = ->(request, session) { + # Your custom logic here + user = session.authenticatable + user.update(email_verified: true) + user.update(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 0b5d6bd..ca51b64 100644 --- a/app/controllers/passwordless/sessions_controller.rb +++ b/app/controllers/passwordless/sessions_controller.rb @@ -150,7 +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) + call_after_session_confirm(request, session) redirect_to( passwordless_success_redirect_path(session.authenticatable), status: :see_other, @@ -189,14 +189,10 @@ def call_or_return(value, *args) end end - def call_after_session_confirm(session) + def call_after_session_confirm(request, session) return unless Passwordless.config.after_session_confirm.respond_to?(:call) - if Passwordless.config.after_session_confirm.arity == 2 - Passwordless.config.after_session_confirm.call(session, request) - else - Passwordless.config.after_session_confirm.call(session) - end + Passwordless.config.after_session_confirm.call(request, session) end def find_authenticatable diff --git a/test/controllers/passwordless/sessions_controller_test.rb b/test/controllers/passwordless/sessions_controller_test.rb index 7fbcc9e..4ff5109 100644 --- a/test/controllers/passwordless/sessions_controller_test.rb +++ b/test/controllers/passwordless/sessions_controller_test.rb @@ -221,32 +221,6 @@ class << User assert_nil pwless_session(User) end - test("PATCH /:passwordless_for/sign_in/:id -> SUCCESS with after_session_confirm") 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) { - confirm_called = true - assert_equal user, session.authenticatable - }) 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" - - follow_redirect! - assert_equal 200, status - assert_equal "/", path - - assert_equal pwless_session(User), Session.last!.id - 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") From 214d9b363012d85ba72b2eb8d39d0a2ee96d788e Mon Sep 17 00:00:00 2001 From: Wilbur Suero Date: Mon, 7 Oct 2024 08:41:37 -0400 Subject: [PATCH 4/7] add default after_session_confirm --- app/controllers/passwordless/sessions_controller.rb | 6 +++--- lib/passwordless/config.rb | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/controllers/passwordless/sessions_controller.rb b/app/controllers/passwordless/sessions_controller.rb index ca51b64..60ce042 100644 --- a/app/controllers/passwordless/sessions_controller.rb +++ b/app/controllers/passwordless/sessions_controller.rb @@ -150,7 +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(request, session) + call_after_session_confirm(session, request) redirect_to( passwordless_success_redirect_path(session.authenticatable), status: :see_other, @@ -189,10 +189,10 @@ def call_or_return(value, *args) end end - def call_after_session_confirm(request, session) + def call_after_session_confirm(session, request) return unless Passwordless.config.after_session_confirm.respond_to?(:call) - Passwordless.config.after_session_confirm.call(request, session) + Passwordless.config.after_session_confirm.call(session, request) end def find_authenticatable diff --git a/lib/passwordless/config.rb b/lib/passwordless/config.rb index e992e0c..83bd173 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 From 6ab4141e5bdb4a9c4b34eb2564b6ddaf79d55a6f Mon Sep 17 00:00:00 2001 From: Wilbur Suero Date: Mon, 7 Oct 2024 09:38:19 -0400 Subject: [PATCH 5/7] fix readme params order --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b1ef44..ac51ead 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ You can configure the after_session_confirm hook in your Passwordless configurat Passwordless.configure do |config| # ... other configuration options ... - config.after_session_confirm = ->(request, session) { + config.after_session_confirm = ->(session, request) { # Your custom logic here user = session.authenticatable user.update(email_verified: true) From d33e81a5676fea459a523d52886ae7be95f1ec2d Mon Sep 17 00:00:00 2001 From: Mikkel Malmberg Date: Mon, 7 Oct 2024 22:21:09 +0200 Subject: [PATCH 6/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac51ead..a8af0e7 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ Passwordless.configure do |config| config.paranoid = false # Display email sent notice even when the resource is not found. - config.after_session_confirm = ->(session) {} # Called after a session is confirmed. + config.after_session_confirm = ->(request, session) {} # Called after a session is confirmed. end ``` From e7f1f678df53daaa8482080119342501e99bc8f1 Mon Sep 17 00:00:00 2001 From: Mikkel Malmberg Date: Mon, 7 Oct 2024 22:26:32 +0200 Subject: [PATCH 7/7] Apply suggestions from code review --- README.md | 16 ++++++---------- lib/passwordless/config.rb | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a8af0e7..bfccd99 100644 --- a/README.md +++ b/README.md @@ -266,24 +266,20 @@ Passwordless.configure do |config| end ``` -## After Session Confirmation Hook -The after_session_confirm hook is called automatically after a successful session confirmation. It receives the request and session objects as arguments, which you can use to access the authenticated user (via session.authenticatable) and perform any necessary operations. +## After Session Confirm Hook -You can configure the after_session_confirm hook in your Passwordless configuration: +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| - # ... other configuration options ... - config.after_session_confirm = ->(session, request) { - # Your custom logic here user = session.authenticatable - user.update(email_verified: true) - user.update(last_login_ip: request.remote_ip) + 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/lib/passwordless/config.rb b/lib/passwordless/config.rb index 83bd173..7b0c472 100644 --- a/lib/passwordless/config.rb +++ b/lib/passwordless/config.rb @@ -53,7 +53,7 @@ class Configuration option( :after_session_confirm, - default: lambda do |session, _request| + default: lambda do |_session, _request| end )