Skip to content

fix(session):Added Session::flush() + Session::regenerate() at the en… - #118

Merged
smarcet merged 1 commit into
mainfrom
hotfix/session-cleanup
Mar 16, 2026
Merged

fix(session):Added Session::flush() + Session::regenerate() at the en…#118
smarcet merged 1 commit into
mainfrom
hotfix/session-cleanup

Conversation

@smarcet

@smarcet smarcet commented Mar 11, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Chores

    • Streamlined session management by centralizing logout cleanup operations for improved consistency and security.
  • Tests

    • Added comprehensive test suite validating session cleanup, cookie handling, and security context clearing during logout across different user scenarios.

…d of AuthService::logout()

after all other cleanup operations complete. This ensures:

1. invalidateSession() captures the session ID for the cache blacklist before flush
2. principal_service->clear() and security_context_service->clear() run first (now redundant but harmless)
3. Auth::logout() clears Laravel auth state before the session is destroyed
4. Session::flush() removes ALL session data in one operation
5. Session::regenerate() creates a fresh session ID to prevent fixation
@smarcet
smarcet requested review from Copilot and romanetar and removed request for Copilot March 11, 2026 16:25
@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Session management operations are being consolidated from multiple controllers into the AuthService. Session::flush() and Session::regenerate() calls are removed from HomeController and UserController, then centralized in AuthService::logout(). A comprehensive test suite validates the refactored behavior.

Changes

Cohort / File(s) Summary
Controller Session Cleanup Removal
app/Http/Controllers/HomeController.php, app/Http/Controllers/UserController.php
Removed Session facade import and explicit Session::flush()/Session::regenerate() calls from guest check and logout logic, respectively.
Auth Service Session Management
app/libs/Auth/AuthService.php
Added Session::flush() and Session::regenerate() calls to logout() method, centralizing session cleanup at the service layer after authentication logout and cookie issuance.
Logout Test Suite
tests/AuthServiceLogoutTest.php
New comprehensive test class with 8 test methods validating session invalidation, session ID capture, cookie deletion, security context clearing, principal service cleanup, and action logging order across guest and authenticated scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Hops and rejoices!
Sessions now flutter from controller to service,
Where cleanup happens cleanly and purposefully,
Tests dance in formation to prove it all works—
Order and clarity bloom in every line! 🌿✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is truncated and incomplete, ending with 'en…' without specifying what stage of the process the session flush/regenerate should occur in. Complete the pull request title to clearly indicate the specific location or context where Session::flush() and Session::regenerate() are being added (e.g., 'fix(session): Add Session::flush() and Session::regenerate() in AuthService logout').
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch hotfix/session-cleanup

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

@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.

🧹 Nitpick comments (1)
tests/AuthServiceLogoutTest.php (1)

79-81: Minor: debug_msg is not a Laravel Log facade method.

Line 81 mocks debug_msg which doesn't exist on Laravel's Log facade. This is harmless since Mockery will just ignore calls that don't happen, but it's unnecessary noise.

🧹 Suggested cleanup
         // Log calls are always allowed
         $this->log_mock->shouldReceive('debug')->zeroOrMoreTimes();
-        $this->log_mock->shouldReceive('debug_msg')->zeroOrMoreTimes();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/AuthServiceLogoutTest.php` around lines 79 - 81, Remove the unnecessary
mock for the non-existent Log method by deleting the shouldReceive('debug_msg')
line in the test setup where $this->log_mock is configured; keep the valid
shouldReceive('debug')->zeroOrMoreTimes() call so logging remains allowed, and
ensure no other tests rely on a custom debug_msg expectation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/AuthServiceLogoutTest.php`:
- Around line 79-81: Remove the unnecessary mock for the non-existent Log method
by deleting the shouldReceive('debug_msg') line in the test setup where
$this->log_mock is configured; keep the valid
shouldReceive('debug')->zeroOrMoreTimes() call so logging remains allowed, and
ensure no other tests rely on a custom debug_msg expectation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 302357c8-3e6c-4ae2-a3d8-2b1246bc4234

📥 Commits

Reviewing files that changed from the base of the PR and between fe5432a and 4992042.

📒 Files selected for processing (4)
  • app/Http/Controllers/HomeController.php
  • app/Http/Controllers/UserController.php
  • app/libs/Auth/AuthService.php
  • tests/AuthServiceLogoutTest.php
💤 Files with no reviewable changes (2)
  • app/Http/Controllers/UserController.php
  • app/Http/Controllers/HomeController.php

@smarcet
smarcet merged commit 4864f50 into main Mar 16, 2026
4 checks passed
smarcet added a commit that referenced this pull request Aug 12, 2026
…OIDC circuit tests (#152)

* fix(2fa): validate pending OAuth2 client before redeeming a recovery code

verify2FARecovery() skipped the resolveClientFromMemento() guard that
verify2FA() applies, so with a pending OAuth2 authorization request whose
client no longer exists the single-use recovery code was burned and an IDP
session established for an authorization request that could only fail at
the /oauth2/auth hop. Apply the same guard before redemption; recovery-code
checking itself stays client-agnostic.

* refactor(2fa): extract MFA error_code literals into MFAConstants

The error_code values emitted by UserController's MFA endpoints were
hardcoded strings, duplicated in TwoFactorRateLimitMiddleware::FAILURE_CODES
where a silent drift would break the rate-limit failure counting. Tests keep
asserting the literal wire values on purpose, pinning the contract.

* refactor(2fa): return a typed DTO from getPendingState() instead of an array

MFAPendingState (getUserId / getPendingAt / shouldRemember) replaces the
string-keyed array, so callers stop scattering 'user_id'/'remember' literals
and casts, and the shape is enforced by the type system instead of by
convention.

* refactor(2fa): serialize recovery-codes standing via a RecoveryCodesStatus DTO

verify2FARecovery() and getProfile() each hand-built the
recovery_codes_remaining/total/low_threshold payload with their own config()
reads and magic defaults. IRecoveryCodeService::getStatus() now returns a
RecoveryCodesStatus DTO whose toArray() owns the wire keys, so both call
sites merge the same serialized shape. Side effect: the recovery XHR
response now also carries recovery_codes_total (additive, ignored by the
SPA).

* test(2fa): prove the full OIDC consent circuit for verify2FA and verify2FARecovery

authorize -> login -> MFA challenge -> verify (OTP / recovery code) ->
redirect_url back to the authorization endpoint (rebuilt from the session
memento) -> consent screen -> AllowOnce -> authorization code delivered to
the client redirect_uri. Locks in that the XHR verify contract composes with
the interactive grant's memento round-trip.

Note: OIDCProtocolTestCase's password-login circuits (e.g. testAuthCode)
predate the MFA gate and post a wrong seed password - broken independently
of this change.

* test(oidc): repair OIDCProtocolTestCase login circuits (stale password + MFA gate)

Two stacked breakages, both predating and unrelated to each individual test:

- 021bee3 (jul 2024) changed the TestSeeder passwords from '1qaz2wsx' to
  '1Qaz2wsx!' without updating this class, so every password login leg has
  silently failed since - errorLogin() also answers 302, so the post-login
  assertion kept passing and tests died downstream instead.
- The MFA gate now challenges the seeded login user (SuperAdminGroup is in
  two_factor.enforced_groups), so even a correct password stops at the 2FA
  challenge. This class exercises the OIDC protocol, not the gate - enforced
  groups are cleared in prepareForTests(); the gate plus the full
  authorize -> MFA -> consent -> code circuit live in TwoFactorLoginFlowTest.

Result: 29 broken -> 3 (32/35 green). The 3 residuals have distinct
pre-existing causes: testConsentLogin and
testGetRefreshTokenWithPromptSetToConsentLogin lose the login hint because
AuthService::logout()'s Session::flush() (4864f50 / #118) wipes the
session-backed security context even when called with clear_security_ctx =
false (prompt=login path); testTokenResponseModePost uses max_age=1 and the
multi-request dance now takes longer than 1s, forcing a re-login.

* fix(auth): honor clear_security_ctx=false across logout()'s session flush

The Session::flush() hardening added in #118 wipes the whole session at the
end of logout(), including the session-backed security context - even when
the caller passed clear_security_ctx = false (the prompt=login
re-authentication path in InteractiveGrantType::mustAuthenticateUser()),
which broke the login-hint prefill on the login screen for prompt=login
OIDC requests. Capture the context before the flush and re-save it after
the session ID regenerate; everything else is still flushed, so the #118
hardening stands.

* test(oidc): raise testTokenResponseModePost max_age from 1 to 3200

The test exercises response_mode=form_post, not max_age expiry
(testMaxAge1AndWait2 owns that) - with max_age=1 the multi-request
login+consent dance takes longer than 1s and the final authorize hop forced
a re-login instead of delivering the form post. 3200 matches the sibling
circuits. OIDCProtocolTestCase is now fully green: 35/35.

* test(2fa): negative-path OIDC circuits for verify2FA and verify2FARecovery

Six tests inside a pending OIDC authorization-code flow, three per endpoint:
- wrong code then correct code: the rejection keeps the pending challenge
  and the OAuth2 memento alive, and the retry completes the full circuit
  (consent -> authorization code).
- consecutive wrong codes up to the rate-limit threshold: every attempt is
  401 without a session, and once the window closes even the CORRECT code
  answers 429 - brute-forcing inside a pending flow buys no extra attempts.
- burned single-use code (used recovery code / redeemed OTP): rejected like
  any invalid code, and the flow still completes afterwards with a fresh
  code (new recovery code / resent OTP).

* test(2fa): cover the error branches of verify2FA and verify2FARecovery

- validator 412s (malformed request, no otp_value / recovery_code)
- vanished pending user -> mfa_session_expired + pending state cleared
- recovery without a pending challenge -> mfa_session_expired
- stale OAuth2 client guard on verify2FA (parity with the recovery test):
  412 before the OTP is redeemed
- audit failure on the FAILED-verify path stays a clean 401 with the
  error_code the rate-limit middleware keys on, for both endpoints

verify2FA line coverage 82.3% -> 95.2%, verify2FARecovery 82.7% -> 94.2%;
the only uncovered lines left are the generic Exception -> 500 catches.

* test(ci): actually run the protocol TestCase suites, stop hiding failure breadth

Two changes to phpunit.xml:
- The Application suite's <directory> scan only picks up *Test.php (PHPUnit's
  default suffix), so the four concrete *TestCase.php protocol suites
  (OAuth2Protocol, OIDCProtocol, OIDCPasswordless, OpenIdProtocol - 93 tests)
  were NEVER executed by CI. That is how OIDCProtocolTestCase stayed broken
  for two years with green builds. They are now listed explicitly.
- stopOnFailure=false so a run reports every failure instead of dying on the
  first one.

Also fixes the one test the newly-wired suites surfaced:
testResourceServerIntrospectionNotValidIP expected an unconditional 400, but
the resource-server IP check became opt-in in #98
(oauth2.validate_resource_server_ip, default off) - the test now enables the
flag before asserting the rejection.

Full-suite evidence (523 tests): green except 8 pre-existing
environment-dependent Turnstile tests that need TEST_USER_EMAIL /
TEST_USER_PASSWORD and the Turnstile secrets CI injects (they pass in CI;
locally their markTestSkipped guard is defeated by a typed-property
TypeError when the env vars are absent).

* refactor(2fa): single home for every MFA string constant

MFAConstants now owns all of them:
- error codes: the existing three plus mfa_rate_limit and mfa_required.
  ITwoFactorRateLimitService::RATE_LIMIT_ERROR_CODE and
  ILoginStrategy::MFA_REQUIRED alias it, so consumers keep their names while
  the value is defined once.
- 2fa_* session keys: previously defined TWICE in production
  (AbstractMFAChallengeStrategy's private consts and
  ITwoFactorRateLimitService::PENDING_USER_SESSION_KEY) - both now alias
  MFAConstants.

Also promotes the rate-limit cache-key prefix ('2fa_rate:', previously a
sprintf literal in TwoFactorRateLimitService duplicated by the test flush
helper) to ITwoFactorRateLimitService::RATE_LIMIT_CACHE_KEY_PREFIX.

All ~50 hardcoded literals across TwoFactorLoginFlowTest,
AbstractMFAChallengeStrategyTest and EmailOTPMFAChallengeStrategyTest now
reference the constants.

* test(oidc): extract the seeded password into a SEED_PASSWORD constant

The literal appeared at 26 call sites; a seed password change is now a
one-line edit, matching TwoFactorLoginFlowTest. The trailing-space login
test keeps its spacing explicit around the constant, since that spacing
is the subject under test. Suite re-run in idp-app: 35/35, 506 assertions.
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