Skip to content

fix: resolve session overlap when following email profile links#140

Open
romanetar wants to merge 1 commit into
mainfrom
fix/email-profile-link-session-overlap
Open

fix: resolve session overlap when following email profile links#140
romanetar wants to merge 1 commit into
mainfrom
fix/email-profile-link-session-overlap

Conversation

@romanetar

@romanetar romanetar commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

ref https://app.clickup.com/t/86b9tx32u

Summary by CodeRabbit

  • New Features

    • Added a new secure profile link feature allowing users to access and manage their profiles through signed URLs.
    • Updated email notifications to direct users to the new profile link instead of the previous login page.
  • Bug Fixes

    • Fixed session overlap issue that could redirect logged-in users incorrectly when accessing another user's profile link.
  • Tests

    • Added comprehensive test coverage for profile link signature validation, session handling, and authentication flows.

Signed-off-by: romanetar <roman_ag@hotmail.com>
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a signed profile link feature that allows users to access their profile through cryptographically verified URLs sent in emails. A new getProfileLink endpoint validates signatures, logs out mismatched session users, and redirects to profile or login with email hints. Mail classes generate signed URLs, templates render them, and comprehensive tests validate session-overlap handling.

Changes

Signed Profile Link Implementation

Layer / File(s) Summary
Profile link endpoint with signature validation
app/Http/Controllers/UserController.php, routes/web.php, start_local_server.sh
New getProfileLink(LaravelRequest $request) endpoint validates request signatures and enforces conditional logout when the authenticated user does not match the requested user_id. GET route auth/profile-link is added to the auth route group. Route caching is cleared during startup.
Mail classes updated to generate signed profile links
app/Mail/UserEmailVerificationRequest.php, app/Mail/UserEmailVerificationSuccess.php, app/Mail/WelcomeNewUserEmail.php
UserEmailVerificationRequest, UserEmailVerificationSuccess, and WelcomeNewUserEmail now expose a $profile_link property populated via URL::signedRoute('auth.profile-link', ['user_id' => $user->getId()]), replacing prior controller-action-based links.
Email templates render signed profile links
resources/views/emails/auth/email_verification_request.blade.php, resources/views/emails/auth/email_verification_request_success.blade.php, resources/views/emails/welcome_new_user_email.blade.php, resources/views/emails/welcome_new_user_email_fn.blade.php
Four email Blade templates now use the $profile_link variable in profile-related CTAs instead of inline URL generation.
Test coverage for profile link feature and session handling
tests/ProfileLinkSessionTest.php
New ProfileLinkSessionTest class validates tampered-signature rejection (HTTP 403), guest redirect flow with email hints, authenticated-owner redirect to profile, and regression coverage for session-overlap when a different user follows another's link and is logged out.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A signed link hops through the mail,
Cryptographic seals that never fail.
Users click and verify their way,
Sessions logout when they go astray.
Safe profiles on a trusted path! 🔐

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: resolve session overlap when following email profile links' directly addresses the main issue the PR solves: preventing session overlap bugs when users follow profile links from emails.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/email-profile-link-session-overlap

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-140/

This page is automatically updated on each push to this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/Http/Controllers/UserController.php (1)

250-266: ⚡ Quick win

Consider adding method documentation.

The implementation correctly handles signature validation, session mismatch logout, and redirect logic as confirmed by the test suite. Adding a PHPDoc block would help future maintainers understand the intended behavior for each scenario (guest, owner, different user).

📝 Suggested documentation
+    /**
+     * Handle signed profile link requests with session-overlap protection.
+     * 
+     * Validates the request signature and manages session transitions:
+     * - If a different user is logged in, logs them out
+     * - Redirects authenticated matching users to their profile
+     * - Redirects guests to login with email pre-filled
+     * 
+     * `@param` LaravelRequest $request Must contain signed user_id parameter
+     * `@return` \Illuminate\Http\RedirectResponse
+     */
     public function getProfileLink(LaravelRequest $request)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Controllers/UserController.php` around lines 250 - 266, Add a PHPDoc
block above the getProfileLink(LaravelRequest $request) method describing its
purpose, parameters, return value, and side effects: state that it validates the
signed request (abort 403 on invalid), logs out the current session if the
signed user_id differs from the current user, redirects authenticated owners to
getProfile, and redirects guests to getLogin with an optional login_hint derived
from the target user's email; include `@param` LaravelRequest $request, `@return`
\Illuminate\Http\RedirectResponse (or appropriate return type), and note
possible abort/side-effect of auth_service->logout() and that it uses
auth_service->getUserById().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Mail/WelcomeNewUserEmail.php`:
- Line 93: The signed profile link in WelcomeNewUserEmail is generated without
an expiration at the $this->profile_link assignment using
URL::signedRoute('auth.profile-link', ['user_id' => $user->getId()]); add an
expiration argument (e.g., a Carbon timestamp like Carbon::now()->addDays(14))
as the third parameter to URL::signedRoute to limit validity (choose 7–30 days
per policy) and add the Carbon import if missing; ensure the route verification
logic still accepts signed URLs with an expiration.

In `@tests/ProfileLinkSessionTest.php`:
- Around line 37-38: The test assigns results of
EntityManager::getRepository(User::class)->findOneBy(...) directly to the typed
properties $this->user_a and $this->user_b which can be null; add explicit
assertions (e.g. assertNotNull or similar) immediately after each findOneBy call
to verify the fixture exists before assigning to the User-typed properties, and
keep the subsequent assignment only after the assertion so failures are clear
and actionable.

---

Nitpick comments:
In `@app/Http/Controllers/UserController.php`:
- Around line 250-266: Add a PHPDoc block above the
getProfileLink(LaravelRequest $request) method describing its purpose,
parameters, return value, and side effects: state that it validates the signed
request (abort 403 on invalid), logs out the current session if the signed
user_id differs from the current user, redirects authenticated owners to
getProfile, and redirects guests to getLogin with an optional login_hint derived
from the target user's email; include `@param` LaravelRequest $request, `@return`
\Illuminate\Http\RedirectResponse (or appropriate return type), and note
possible abort/side-effect of auth_service->logout() and that it uses
auth_service->getUserById().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 281b405b-a410-4d79-9bba-5aa6c7dd29b9

📥 Commits

Reviewing files that changed from the base of the PR and between 08712ec and 332b2c8.

📒 Files selected for processing (11)
  • app/Http/Controllers/UserController.php
  • app/Mail/UserEmailVerificationRequest.php
  • app/Mail/UserEmailVerificationSuccess.php
  • app/Mail/WelcomeNewUserEmail.php
  • resources/views/emails/auth/email_verification_request.blade.php
  • resources/views/emails/auth/email_verification_request_success.blade.php
  • resources/views/emails/welcome_new_user_email.blade.php
  • resources/views/emails/welcome_new_user_email_fn.blade.php
  • routes/web.php
  • start_local_server.sh
  • tests/ProfileLinkSessionTest.php

$this->user_email = $user->getEmail();
$this->user_fullname = $user->getFullName();
$this->bio_link = URL::action("UserController@getLogin");
$this->profile_link = URL::signedRoute('auth.profile-link', ['user_id' => $user->getId()]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add expiration to the signed profile link for security.

The signed URL is generated without an expiration time, which means it remains valid indefinitely. If the email is forwarded, leaked, or the user's mailbox is compromised at any point in the future, this link could be used for unauthorized profile access.

Laravel's URL::signedRoute() accepts an optional third parameter for expiration. Consider adding a reasonable lifetime (e.g., 7-30 days) to limit the window of exposure.

🔒 Proposed fix to add expiration
-        $this->profile_link = URL::signedRoute('auth.profile-link', ['user_id' => $user->getId()]);
+        $this->profile_link = URL::signedRoute('auth.profile-link', ['user_id' => $user->getId()], now()->addDays(30));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$this->profile_link = URL::signedRoute('auth.profile-link', ['user_id' => $user->getId()]);
$this->profile_link = URL::signedRoute('auth.profile-link', ['user_id' => $user->getId()], now()->addDays(30));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Mail/WelcomeNewUserEmail.php` at line 93, The signed profile link in
WelcomeNewUserEmail is generated without an expiration at the
$this->profile_link assignment using URL::signedRoute('auth.profile-link',
['user_id' => $user->getId()]); add an expiration argument (e.g., a Carbon
timestamp like Carbon::now()->addDays(14)) as the third parameter to
URL::signedRoute to limit validity (choose 7–30 days per policy) and add the
Carbon import if missing; ensure the route verification logic still accepts
signed URLs with an expiration.

Comment on lines +37 to +38
$this->user_a = EntityManager::getRepository(User::class)->findOneBy(['identifier' => 'sebastian.marcet']);
$this->user_b = EntityManager::getRepository(User::class)->findOneBy(['identifier' => '2']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add explicit fixture existence assertions before typed-property assignment (Line 37, Line 38).

findOneBy(...) may return null; assigning that to User-typed properties can fail with a low-signal TypeError. Add explicit assertions so failures are actionable.

Suggested patch
-        $this->user_a = EntityManager::getRepository(User::class)->findOneBy(['identifier' => 'sebastian.marcet']);
-        $this->user_b = EntityManager::getRepository(User::class)->findOneBy(['identifier' => '2']);
+        $userA = EntityManager::getRepository(User::class)->findOneBy(['identifier' => 'sebastian.marcet']);
+        $userB = EntityManager::getRepository(User::class)->findOneBy(['identifier' => '2']);
+
+        $this->assertNotNull($userA, 'Expected fixture user "sebastian.marcet" to exist');
+        $this->assertNotNull($userB, 'Expected fixture user with identifier "2" to exist');
+
+        $this->user_a = $userA;
+        $this->user_b = $userB;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$this->user_a = EntityManager::getRepository(User::class)->findOneBy(['identifier' => 'sebastian.marcet']);
$this->user_b = EntityManager::getRepository(User::class)->findOneBy(['identifier' => '2']);
$userA = EntityManager::getRepository(User::class)->findOneBy(['identifier' => 'sebastian.marcet']);
$userB = EntityManager::getRepository(User::class)->findOneBy(['identifier' => '2']);
$this->assertNotNull($userA, 'Expected fixture user "sebastian.marcet" to exist');
$this->assertNotNull($userB, 'Expected fixture user with identifier "2" to exist');
$this->user_a = $userA;
$this->user_b = $userB;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ProfileLinkSessionTest.php` around lines 37 - 38, The test assigns
results of EntityManager::getRepository(User::class)->findOneBy(...) directly to
the typed properties $this->user_a and $this->user_b which can be null; add
explicit assertions (e.g. assertNotNull or similar) immediately after each
findOneBy call to verify the fixture exists before assigning to the User-typed
properties, and keep the subsequent assignment only after the assertion so
failures are clear and actionable.

@romanetar romanetar requested a review from smarcet June 8, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant