fix(feature-auth): use AUTH_PATH_PREFIX to skip the /me endpoint in the 401 interceptor #135

Merged
julien merged 1 commits from fix/portal-admin-auth-me-refresh-loop into main 2026-05-14 17:23:45 +02:00
Owner

Summary

Regression introduced by PR #134 / merged minutes ago: opening portal-admin while anonymous fires the bootstrap /admin/auth/me → 401 → bffUnauthorizedInterceptor triggers AuthService.refresh() → which re-fires /admin/auth/me → 401 → tight loop that exhausts the BFF's 120/min rate limiter in seconds. The user lands on a rate_limited error instead of the "Sign in" panel.

Root cause

bffUnauthorizedInterceptor deliberately skips the /me endpoint to avoid the loop (the bootstrap /me legitimately 401s when anonymous). But the skip target was hardcoded to ${bffBaseUrl}/auth/me:

!req.url.startsWith(`${bffBaseUrl}/auth/me`)

When portal-admin overrides AUTH_PATH_PREFIX to /admin/auth (per PR #134), the bootstrap URL becomes ${bffBaseUrl}/admin/auth/me, which does not start with ${bffBaseUrl}/auth/me. The interceptor treats it as a regular protected route, calls refresh(), and we're off to the races.

The reported log shows the rate-limit counter dropping remaining=3 → 2 → 1 → 0 over four /me calls within ~25 ms, all 401.

Fix

Derive the skip URL from AUTH_PATH_PREFIX (which the interceptor already has access to via DI — same module as the AuthService):

const meUrl = `${bffBaseUrl}${pathPrefix}/me`;

!req.url.startsWith(meUrl)

Same behaviour for portal-shell (the default /auth factory keeps the skip URL at /auth/me), correct behaviour for portal-admin (/admin/auth/me), and any future surface that picks a different prefix inherits the fix automatically.

Test plan

  • pnpm nx test feature-auth30 specs pass (was 29; +1 regression spec asserting that no follow-up /me is issued after a bootstrap 401 when the prefix is /admin/auth).
  • pnpm exec nx affected -t format:check lint test build --base=origin/main — clean.
  • Manual: pnpm nx serve portal-admin, visit http://localhost:4300/, observe the BFF log — exactly one /admin/auth/me request (the bootstrap), 401, no follow-up, no rate-limit hit. The home page renders the "No admin session detected" + Sign in button.

Notes for the reviewer

  • The bug pattern is symmetric: any future host that overrides AUTH_PATH_PREFIX would have hit the same trap. Making the skip target derive from the token is the structural fix; the regression spec pins it.
  • portal-shell tests still pass without changes — the default factory returns /auth, so the existing fixtures continue exercising ${bffBaseUrl}/auth/me as the skip target.
  • No SPA-side changes needed in portal-shell or portal-admin app code. The fix is entirely inside the lib.
## Summary Regression introduced by PR #134 / merged minutes ago: opening portal-admin while anonymous fires the bootstrap `/admin/auth/me` → 401 → `bffUnauthorizedInterceptor` triggers `AuthService.refresh()` → which re-fires `/admin/auth/me` → 401 → tight loop that exhausts the BFF's 120/min rate limiter in seconds. The user lands on a `rate_limited` error instead of the "Sign in" panel. ## Root cause [`bffUnauthorizedInterceptor`](libs/feature/auth/src/lib/bff-unauthorized.interceptor.ts) deliberately skips the `/me` endpoint to avoid the loop (the bootstrap `/me` legitimately 401s when anonymous). But the skip target was **hardcoded** to `${bffBaseUrl}/auth/me`: ```ts !req.url.startsWith(`${bffBaseUrl}/auth/me`) ``` When portal-admin overrides `AUTH_PATH_PREFIX` to `/admin/auth` (per PR #134), the bootstrap URL becomes `${bffBaseUrl}/admin/auth/me`, which does **not** start with `${bffBaseUrl}/auth/me`. The interceptor treats it as a regular protected route, calls `refresh()`, and we're off to the races. The reported log shows the rate-limit counter dropping `remaining=3 → 2 → 1 → 0` over four `/me` calls within ~25 ms, all 401. ## Fix Derive the skip URL from `AUTH_PATH_PREFIX` (which the interceptor already has access to via DI — same module as the AuthService): ```ts const meUrl = `${bffBaseUrl}${pathPrefix}/me`; … !req.url.startsWith(meUrl) ``` Same behaviour for portal-shell (the default `/auth` factory keeps the skip URL at `/auth/me`), correct behaviour for portal-admin (`/admin/auth/me`), and any future surface that picks a different prefix inherits the fix automatically. ## Test plan - [x] `pnpm nx test feature-auth` — **30 specs pass** (was 29; +1 regression spec asserting that no follow-up `/me` is issued after a bootstrap 401 when the prefix is `/admin/auth`). - [x] `pnpm exec nx affected -t format:check lint test build --base=origin/main` — clean. - [ ] Manual: `pnpm nx serve portal-admin`, visit `http://localhost:4300/`, observe the BFF log — exactly **one** `/admin/auth/me` request (the bootstrap), 401, no follow-up, no rate-limit hit. The home page renders the "No admin session detected" + Sign in button. ## Notes for the reviewer - The bug pattern is symmetric: any future host that overrides `AUTH_PATH_PREFIX` would have hit the same trap. Making the skip target derive from the token is the structural fix; the regression spec pins it. - portal-shell tests still pass without changes — the default factory returns `/auth`, so the existing fixtures continue exercising `${bffBaseUrl}/auth/me` as the skip target. - No SPA-side changes needed in portal-shell or portal-admin app code. The fix is entirely inside the lib.
julien added 1 commit 2026-05-14 17:23:34 +02:00
fix(feature-auth): use AUTH_PATH_PREFIX to skip the /me endpoint in the 401 interceptor
CI / scan (pull_request) Successful in 2m46s
CI / commits (pull_request) Successful in 2m59s
CI / check (pull_request) Successful in 3m46s
CI / a11y (pull_request) Successful in 1m26s
CI / perf (pull_request) Successful in 6m4s
a1d02051de
`bffUnauthorizedInterceptor` calls `AuthService.refresh()` on every
401 from a BFF route, and explicitly skips the `/me` endpoint to
avoid an infinite loop (the bootstrap /me legitimately 401s when
anonymous). The skip target was hardcoded to `/auth/me`, which
matched portal-shell but missed portal-admin's `/admin/auth/me`.

Symptom: opening portal-admin while anonymous fires the bootstrap
/me → 401 → interceptor sees the URL is NOT `/auth/me` → calls
refresh() → fires another /me → 401 → loop. Hits the 120/min
general rate limiter in seconds; the user lands on a `rate_limited`
error instead of the "Sign in" panel.

Fix: derive the skip URL from `AUTH_PATH_PREFIX` so it matches the
surface the host is on (`/auth/me` for portal-shell, `/admin/auth/me`
for portal-admin). The token is already provided by both apps via
`app.config.ts`.

Tests: +1 regression spec exercising the admin path. The new spec
asserts that no follow-up /me is issued after the bootstrap 401
when the prefix is `/admin/auth`.
julien merged commit 71a098b154 into main 2026-05-14 17:23:45 +02:00
julien deleted branch fix/portal-admin-auth-me-refresh-loop 2026-05-14 17:23:46 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#135