bug: event.locals.accessToken always null — secureCookie mismatch in getToken() #20

Closed
opened 2026-03-15 01:54:56 +00:00 by forgejo_admin · 0 comments

Lineage

plan-2026-03-08-tryout-prep → Phase 10b (Frontend — Admin draft board + coach filtered roster)
Follow-up from Issue #18. PR #19 correctly passes the token, but the token itself is always null.

Repo

forgejo_admin/westside-app

User Story

As an admin or coach
I want my Keycloak access token available server-side
So that server-side API calls to basketball-api team endpoints succeed with authentication

Context

PR #19 fixed the team page load functions to pass event.locals.accessToken to basketball-api fetch calls. However, Playwright revalidation shows the team endpoints still return 401 "Not authenticated" — the token is null.

Root cause analysis of src/hooks.server.js:

const token = await getToken({
    req: event.request,
    secret: env.AUTH_SECRET,
    secureCookie: false  // <-- BUG: looks for non-secure cookie name
});
event.locals.accessToken = token?.accessToken || null;

The site runs on HTTPS (Tailscale funnel). Auth.js sets cookies with the __Secure- prefix on HTTPS connections. But getToken() is called with secureCookie: false, which tells it to look for the NON-secure cookie name (authjs.session-token). It never finds the cookie, so token is null, and event.locals.accessToken is always null.

This was never caught because no existing page depended on the access token:

  • /admin uses public /api/roster/{tenant} (no auth needed)
  • /admin/users gets its own Keycloak admin token via KEYCLOAK_ADMIN_PASSWORD
  • /coach calls fetchMyTeam(accessToken) but silently falls back to full roster on failure
  • /player matches by email from the public roster

The team pages (PR #17, Issue #16) are the first routes that REQUIRE event.locals.accessToken to be non-null.

Secondary concern: Even if the cookie lookup is fixed, the Keycloak access token stored at sign-in time will expire (default 5 minutes). Auth.js does not auto-refresh tokens. A token rotation callback may be needed for long-lived sessions.

File Targets

Files the agent should modify:

  • src/hooks.server.js — fix getToken() call. Either remove secureCookie: false (let it auto-detect), or set secureCookie: true to match HTTPS. Also verify the cookie name matches what Auth.js actually sets. Consider adding console logging temporarily to debug the token value.

Files the agent should investigate:

  • src/auth.js — check if jwt callback correctly persists accessToken on subsequent calls (not just initial sign-in). May need token rotation logic for expired Keycloak tokens.
  • Check the actual cookie name set by Auth.js in the browser (via Playwright browser_evaluate or cookie inspection)

Files the agent should NOT touch:

  • src/lib/server/api.js — the token passing from PR #19 is correct
  • src/routes/admin/teams/+page.server.js — the token wiring from PR #19 is correct
  • Any basketball-api files

Acceptance Criteria

  • event.locals.accessToken contains a valid Keycloak JWT (not null) after sign-in
  • /admin/teams loads team data (no more "Basketball API not reachable")
  • /teams loads team data for authenticated users
  • Token works for at least one full browser session (address expiration if feasible)
  • No access tokens leaked to browser

Test Expectations

  • Manual verification: sign in → navigate to /admin/teams → team data loads
  • Manual verification: check browser cookies — verify cookie name matches what getToken() expects
  • Run command: N/A (server-side auth debugging)

Constraints

  • Access tokens MUST stay server-side only
  • The fix must work on HTTPS (Tailscale funnel with AUTH_TRUST_HOST=true)
  • If token rotation is complex, defer it — just fix the cookie lookup first and document the expiration limitation
  • Check the @auth/core version to confirm the correct getToken() API and cookie name conventions

Checklist

  • PR opened
  • No unrelated changes
  • Access token stays server-side only
  • project-westside-basketball — blocks team management UI (Phase 10b)
  • Issue #18 / PR #19 — correctly passes the token but token is null
### Lineage `plan-2026-03-08-tryout-prep` → Phase 10b (Frontend — Admin draft board + coach filtered roster) Follow-up from Issue #18. PR #19 correctly passes the token, but the token itself is always null. ### Repo `forgejo_admin/westside-app` ### User Story As an admin or coach I want my Keycloak access token available server-side So that server-side API calls to basketball-api team endpoints succeed with authentication ### Context PR #19 fixed the team page load functions to pass `event.locals.accessToken` to basketball-api fetch calls. However, Playwright revalidation shows the team endpoints still return 401 "Not authenticated" — the token is null. Root cause analysis of `src/hooks.server.js`: ```js const token = await getToken({ req: event.request, secret: env.AUTH_SECRET, secureCookie: false // <-- BUG: looks for non-secure cookie name }); event.locals.accessToken = token?.accessToken || null; ``` The site runs on HTTPS (Tailscale funnel). Auth.js sets cookies with the `__Secure-` prefix on HTTPS connections. But `getToken()` is called with `secureCookie: false`, which tells it to look for the NON-secure cookie name (`authjs.session-token`). It never finds the cookie, so `token` is null, and `event.locals.accessToken` is always null. This was never caught because no existing page depended on the access token: - `/admin` uses public `/api/roster/{tenant}` (no auth needed) - `/admin/users` gets its own Keycloak admin token via `KEYCLOAK_ADMIN_PASSWORD` - `/coach` calls `fetchMyTeam(accessToken)` but silently falls back to full roster on failure - `/player` matches by email from the public roster The team pages (PR #17, Issue #16) are the first routes that REQUIRE `event.locals.accessToken` to be non-null. **Secondary concern**: Even if the cookie lookup is fixed, the Keycloak access token stored at sign-in time will expire (default 5 minutes). Auth.js does not auto-refresh tokens. A token rotation callback may be needed for long-lived sessions. ### File Targets Files the agent should modify: - `src/hooks.server.js` — fix `getToken()` call. Either remove `secureCookie: false` (let it auto-detect), or set `secureCookie: true` to match HTTPS. Also verify the cookie name matches what Auth.js actually sets. Consider adding console logging temporarily to debug the token value. Files the agent should investigate: - `src/auth.js` — check if `jwt` callback correctly persists `accessToken` on subsequent calls (not just initial sign-in). May need token rotation logic for expired Keycloak tokens. - Check the actual cookie name set by Auth.js in the browser (via Playwright `browser_evaluate` or cookie inspection) Files the agent should NOT touch: - `src/lib/server/api.js` — the token passing from PR #19 is correct - `src/routes/admin/teams/+page.server.js` — the token wiring from PR #19 is correct - Any basketball-api files ### Acceptance Criteria - [ ] `event.locals.accessToken` contains a valid Keycloak JWT (not null) after sign-in - [ ] `/admin/teams` loads team data (no more "Basketball API not reachable") - [ ] `/teams` loads team data for authenticated users - [ ] Token works for at least one full browser session (address expiration if feasible) - [ ] No access tokens leaked to browser ### Test Expectations - [ ] Manual verification: sign in → navigate to `/admin/teams` → team data loads - [ ] Manual verification: check browser cookies — verify cookie name matches what `getToken()` expects - Run command: N/A (server-side auth debugging) ### Constraints - Access tokens MUST stay server-side only - The fix must work on HTTPS (Tailscale funnel with `AUTH_TRUST_HOST=true`) - If token rotation is complex, defer it — just fix the cookie lookup first and document the expiration limitation - Check the @auth/core version to confirm the correct `getToken()` API and cookie name conventions ### Checklist - [ ] PR opened - [ ] No unrelated changes - [ ] Access token stays server-side only ### Related - `project-westside-basketball` — blocks team management UI (Phase 10b) - Issue #18 / PR #19 — correctly passes the token but token is null
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/westside-app#20
No description provided.