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
- Web:
- 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
-
Initial reproduction from web login:
POST /auth/sign-in/oauth2returned200with Authentik authorize URL.- Authentik login flow and consent screen loaded correctly.
-
First callback failure mode (before
jarvisemail fix):- Callback ended at API error redirect with
error=email_is_missing. - Result URL:
https://api.mosaicstack.dev/?error=email_is_missing.
- Callback ended at API error redirect with
-
User updated Authentik account:
jarvisaccount email set tojarvis@mosaic.local.email_is_missingfailure no longer occurs.
-
Current callback behavior (after email fix):
GET /auth/oauth2/callback/authentik?code=...&state=...returns302tohttps://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.
-
Session validation mismatch (critical):
- BetterAuth direct session endpoint succeeds:
GET /auth/get-session->200with session payload.
- Guarded API session endpoint fails:
GET /auth/session->401with{"message":"Invalid or expired session", ...}
- Reproduced repeatedly in same browser context immediately after callback.
- BetterAuth direct session endpoint succeeds:
Config Sync Notes
User synced local files with deployed Portainer stack:
.envupdated with deployed values.docker-compose.swarm.portainer.ymlchanged:- Removed
BETTER_AUTH_URLenv mapping from API service.
- Removed
Observed auth behavior after sync:
- Improvement: removed
email_is_missingcallback error. - Remaining failure:
/auth/sessionstill 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
- extracts
apps/api/src/auth/auth.service.tsverifySession()callsauth.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
- Fix
verifySession()to validate using BetterAuth-compatible cookie header candidates first, with bearer fallback for API clients. - Add/update unit tests in
auth.service.spec.tsto cover cookie-first validation and bearer fallback. - Re-run targeted API auth tests.
- Re-run Playwright auth chain to confirm:
- callback sets cookie
/auth/sessionreturns200- web app transitions out of
/login.
Implementation Update (2026-02-19)
Completed items:
-
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()
- File:
-
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
- File:
-
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.
- Command:
Remaining step (requires deploy):
- Redeploy API with this patch and rerun live Playwright flow on
app.mosaicstack.devto confirm/auth/sessionreturns200after callback.