Files
stack/docs/scratchpads/362-auth-session-chain-debug.md

4.5 KiB

362 - Auth Session Chain Debug (Authentik -> BetterAuth -> API Guard)

Context

  • Date (UTC): 2026-02-19
  • Environment under test: production domains
    • Web: https://app.mosaicstack.dev/login
    • API: https://api.mosaicstack.dev
    • IdP: https://auth.diversecanvas.com
  • Tooling: Playwright MCP + Chromium

Problem Statement

Users can complete Authentik login and consent, but Mosaic web app returns to login and remains unauthenticated.

Timeline and Evidence

  1. Initial reproduction from web login:

    • POST /auth/sign-in/oauth2 returned 200 with Authentik authorize URL.
    • Authentik login flow and consent screen loaded correctly.
  2. First callback failure mode (before jarvis email fix):

    • Callback ended at API error redirect with error=email_is_missing.
    • Result URL: https://api.mosaicstack.dev/?error=email_is_missing.
  3. User updated Authentik account:

    • jarvis account email set to jarvis@mosaic.local.
    • email_is_missing failure no longer occurs.
  4. Current callback behavior (after email fix):

    • GET /auth/oauth2/callback/authentik?code=...&state=... returns 302 to https://app.mosaicstack.dev/.
    • Callback sets BetterAuth cookies:
      • __Secure-better-auth.state=...; Max-Age=0; ...
      • __Secure-better-auth.session_token=...; Max-Age=604800; Path=/; HttpOnly; Secure; SameSite=Lax
    • Browser cookie jar confirms session cookie present for api.mosaicstack.dev.
  5. Session validation mismatch (critical):

    • BetterAuth direct session endpoint succeeds:
      • GET /auth/get-session -> 200 with session payload.
    • Guarded API session endpoint fails:
      • GET /auth/session -> 401 with {"message":"Invalid or expired session", ...}
    • Reproduced repeatedly in same browser context immediately after callback.

Config Sync Notes

User synced local files with deployed Portainer stack:

  • .env updated with deployed values.
  • docker-compose.swarm.portainer.yml changed:
    • Removed BETTER_AUTH_URL env mapping from API service.

Observed auth behavior after sync:

  • Improvement: removed email_is_missing callback error.
  • Remaining failure: /auth/session still returns 401 despite valid BetterAuth cookie and successful /auth/get-session.

Root Cause Hypothesis (Strong)

AuthGuard extracts BetterAuth session cookie token correctly, but AuthService.verifySession() validates it using Authorization: Bearer <token> instead of a BetterAuth cookie/header context.

Relevant code paths:

  • apps/api/src/auth/guards/auth.guard.ts
    • extracts __Secure-better-auth.session_token / better-auth.session_token
  • apps/api/src/auth/auth.service.ts
    • verifySession() calls auth.api.getSession({ headers: { authorization: "Bearer ..." } })

Why this matches evidence:

  • /auth/get-session (native BetterAuth endpoint reading request cookie) succeeds.
  • /auth/session (custom guard + verify path) fails for same browser session.

Next Actions

  1. Fix verifySession() to validate using BetterAuth-compatible cookie header candidates first, with bearer fallback for API clients.
  2. Add/update unit tests in auth.service.spec.ts to cover cookie-first validation and bearer fallback.
  3. Re-run targeted API auth tests.
  4. Re-run Playwright auth chain to confirm:
    • callback sets cookie
    • /auth/session returns 200
    • web app transitions out of /login.

Implementation Update (2026-02-19)

Completed items:

  1. Updated backend session verification logic:

    • File: apps/api/src/auth/auth.service.ts
    • verifySession() now tries session resolution in this order:
      • cookie: __Secure-better-auth.session_token=<token>
      • cookie: better-auth.session_token=<token>
      • cookie: __Host-better-auth.session_token=<token>
      • authorization: Bearer <token> (fallback)
    • Added helper methods:
      • buildSessionHeaderCandidates()
      • isExpectedAuthError()
  2. Added/updated tests:

    • File: apps/api/src/auth/auth.service.spec.ts
    • Added RED->GREEN test:
      • should validate session token using secure BetterAuth cookie header
    • Updated fallback coverage test:
      • should fall back to Authorization header when cookie-based lookups miss
  3. Verification:

    • Command: pnpm --filter @mosaic/api test -- src/auth/auth.service.spec.ts
    • Result: pass (all tests green).
    • Command: pnpm --filter @mosaic/api lint
    • Result: pass.

Remaining step (requires deploy):

  • Redeploy API with this patch and rerun live Playwright flow on app.mosaicstack.dev to confirm /auth/session returns 200 after callback.