fix(auth): clear cache + force re-auth on 401 #429

Merged
pm-bot merged 1 commit from fix/408-auth-cache-invalidation into main 2026-05-29 13:35:22 +02:00
Collaborator

Summary

Fixes #408.

Behebt die Stale-Cache-Schwäche bei abgelaufenem ID-Token: API gibt korrekt 401 zurück (jose validiert exp standardmäßig), aber der Client behielt die gecachten Listen, weil weder der HTTP-Layer noch der QueryClient differenziert zwischen Auth-Errors und generischen 4xx/5xx unterschieden.

Änderungen

  • AuthError extends ApiError im @mrrmlab/api-client: HttpClient wirft typed AuthError bei 401 (statt generisch ApiError). instanceof-Checks auf ApiError bleiben kompatibel.
  • installAuthErrorHandler / <AuthErrorHandler> in @mrrmlab/ui: subscribed an QueryCache + MutationCache; bei matching Error (predicate-supplied vom App-Shell) → queryClient.clear() + onAuthError(). Decoupled per Predicate, sodass @mrrmlab/ui nicht auf @mrrmlab/api-client hängt.
  • Wiring in apps/web/src/app.tsx: redirect zu logoutUrl bzw. /login. In apps/mobile/app/_layout.tsx: router.replace('/login').
  • Test-Infra ergänzt für packages/api-client (vitest) und packages/ui (vitest), beide ohne Tests bisher.

Curl-Test (server-side expired token → 401)

Statt manueller curl-Doku wurde der Test als Integration-Test eingebaut, um Regressionen zu fangen:

// apps/api/test/integration/auth.int-spec.ts (#408)
it('lehnt Anfragen mit abgelaufenem Token ab (401)', async () => {
  const expired = await jwks.signToken({ sub: 'test-user' }, { expiresIn: '-1m' });
  await request(app.getHttpServer())
    .get('/lists')
    .set('Authorization', `Bearer ${expired}`)
    .expect(401);
});

signToken akzeptiert jetzt eine optionale expiresIn-Option ('-1m' = bereits abgelaufen).

Akzeptanz (#408)

  • API gibt 401 bei abgelaufenem Token zurück — verifiziert via neuem Integration-Test
  • Client invalidiert bei 401 den Cache und triggert Re-Auth — installAuthErrorHandler + Unit-Tests
  • Lists werden nicht mit Stale-Auth-State gerendert — queryClient.clear() läuft beim Auth-Fehler
  • Curl-Test in PR-Body dokumentiert (siehe oben, als Integration-Test reproducible)

Test plan

  • pnpm --filter @mrrmlab/api-client test (4 passed)
  • pnpm --filter @mrrmlab/ui test (4 passed)
  • pnpm --filter @mrrmlab/api test:integration (84 passed, +1 neu)
  • pnpm --filter @mrrmlab/web test (70 passed)
  • pnpm --filter @mrrmlab/mobile test (61 passed)
  • Typecheck web, mobile, ui, api-client grün
  • Manuell: web + mobile mit echtem Keycloak — Token-Ablauf simuliert (Refresh-Token explizit revoked)
## Summary Fixes #408. Behebt die Stale-Cache-Schwäche bei abgelaufenem ID-Token: API gibt korrekt 401 zurück (jose validiert `exp` standardmäßig), aber der Client behielt die gecachten Listen, weil weder der HTTP-Layer noch der QueryClient differenziert zwischen Auth-Errors und generischen 4xx/5xx unterschieden. ## Änderungen - **`AuthError extends ApiError`** im `@mrrmlab/api-client`: HttpClient wirft typed AuthError bei 401 (statt generisch ApiError). instanceof-Checks auf ApiError bleiben kompatibel. - **`installAuthErrorHandler` / `<AuthErrorHandler>`** in `@mrrmlab/ui`: subscribed an QueryCache + MutationCache; bei matching Error (predicate-supplied vom App-Shell) → `queryClient.clear()` + `onAuthError()`. Decoupled per Predicate, sodass `@mrrmlab/ui` nicht auf `@mrrmlab/api-client` hängt. - **Wiring** in `apps/web/src/app.tsx`: redirect zu logoutUrl bzw. `/login`. In `apps/mobile/app/_layout.tsx`: `router.replace('/login')`. - **Test-Infra ergänzt** für `packages/api-client` (vitest) und `packages/ui` (vitest), beide ohne Tests bisher. ## Curl-Test (server-side expired token → 401) Statt manueller curl-Doku wurde der Test als Integration-Test eingebaut, um Regressionen zu fangen: ```ts // apps/api/test/integration/auth.int-spec.ts (#408) it('lehnt Anfragen mit abgelaufenem Token ab (401)', async () => { const expired = await jwks.signToken({ sub: 'test-user' }, { expiresIn: '-1m' }); await request(app.getHttpServer()) .get('/lists') .set('Authorization', `Bearer ${expired}`) .expect(401); }); ``` `signToken` akzeptiert jetzt eine optionale `expiresIn`-Option (`'-1m'` = bereits abgelaufen). ## Akzeptanz (#408) - [x] API gibt 401 bei abgelaufenem Token zurück — verifiziert via neuem Integration-Test - [x] Client invalidiert bei 401 den Cache und triggert Re-Auth — `installAuthErrorHandler` + Unit-Tests - [x] Lists werden nicht mit Stale-Auth-State gerendert — `queryClient.clear()` läuft beim Auth-Fehler - [x] Curl-Test in PR-Body dokumentiert (siehe oben, als Integration-Test reproducible) ## Test plan - [x] `pnpm --filter @mrrmlab/api-client test` (4 passed) - [x] `pnpm --filter @mrrmlab/ui test` (4 passed) - [x] `pnpm --filter @mrrmlab/api test:integration` (84 passed, +1 neu) - [x] `pnpm --filter @mrrmlab/web test` (70 passed) - [x] `pnpm --filter @mrrmlab/mobile test` (61 passed) - [x] Typecheck web, mobile, ui, api-client grün - [ ] Manuell: web + mobile mit echtem Keycloak — Token-Ablauf simuliert (Refresh-Token explizit revoked)
fix(auth): clear cache + force re-auth on 401
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
af3e74b7fa
Fixes #408.

Stale-cache bug: bei abgelaufenem ID-Token wirft die API korrekt 401
(integration-test verifiziert), aber TanStack Query behielt die Cached
Lists. User sah weiterhin Daten obwohl Token weg war.

- `AuthError extends ApiError`: typed 401-Variante im api-client, sodass
  Handler-Code 401 von 4xx/5xx unterscheiden kann.
- `installAuthErrorHandler` / `<AuthErrorHandler>` in @mrrmlab/ui:
  subscribed an QueryCache + MutationCache; bei AuthError → qc.clear() +
  app-provided onAuthError (logout + redirect).
- Wired in apps/web (window.location.assign auf logoutUrl/login) und
  apps/mobile (router.replace('/login')).
- Vitest-Infra ergänzt für packages/api-client und packages/ui (waren
  bisher ohne Tests).
- Neuer Integration-Test in apps/api: abgelaufener JWT → 401 (jose
  validiert exp standardmäßig, war nur ungetestet).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pm-bot merged commit 821037476a into main 2026-05-29 13:35:22 +02:00
Sign in to join this conversation.
No reviewers
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
admin-mrrm/mrrmlabapp!429
No description provided.