6.2 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.
Playwright Re-Check (2026-02-19, later run)
Live flow evidence after previous deploy attempt:
-
OAuth callback succeeds:
GET https://api.mosaicstack.dev/auth/oauth2/callback/authentik?code=...&state=...->302- Redirect target observed:
https://app.mosaicstack.dev/ - Browser cookie jar includes:
__Secure-better-auth.session_tokenonapi.mosaicstack.dev(HttpOnly, Secure, SameSite=Lax)
-
Session bootstrap still fails immediately:
GET https://api.mosaicstack.dev/auth/session->500- Response body shape:
{"success":false,"message":"An unexpected error occurred","errorId":"...","path":"/auth/session","statusCode":500}
- Web app returns to login because session fetch fails.
-
Frontend version mismatch observed:
- Live
POST /auth/sign-in/oauth2response from login flow still shows callback URL pointing to/dashboard. - Current repository login page uses callback URL
/. - This indicates deployed web image is older than current
developcode (or stale image tag in runtime).
- Live
Additional Code Fix Applied Locally (pending push/deploy)
Refined cookie candidate construction in API session verification:
- File:
apps/api/src/auth/auth.service.ts- Removed URL-encoding of session token when constructing cookie headers.
- Cookie candidates now pass raw token value exactly as extracted from incoming cookie.
Why:
- BetterAuth cookie tokens can contain characters like
/,+, and=. - Re-encoding these values can mutate token bytes and cause lookup/parse failures.
Regression test added:
- File:
apps/api/src/auth/auth.service.spec.tsshould preserve raw cookie token value without URL re-encoding