Skip to content

typed API client ships no JWT auth wiring: every generated route that requires a valid JWT token returns 401 #433

Description

@kilodesodiq-arch

Problem

The frontend's typed API client is built for a header the backend no longer uses. api-client.ts attaches no auth header and still documents API keys:

// app/frontend/src/lib/api-client.ts
/**
 * - Auth: the backend uses x-api-key headers. If a default key is needed,
 *   add `headers: { 'x-api-key': '...' }` to the createClient options.
 */
export const apiClient = createClient<paths>({
  baseUrl: apiUrl,
  fetch: withTimeoutFetch(fetchClient as typeof fetch) as typeof fetch,
});

But the generated spec this client is typed against declares JWT auth on its routes:

// app/frontend/src/lib/generated/api.ts  (multiple endpoints)
/** @description Unauthorized - valid JWT token required. */

The backend issued OIDC JWTs (tracked as issue #218), so the live API now expects a bearer token, while the generated client neither acquires, attaches, nor refreshes one.

Consequence: every request through apiClient to a JWT-required endpoint is sent without an Authorization header and fails with 401 unless each caller hand-rolls token attachment. Auth is fragmented: there is no single place that acquires a token, attaches it to openapi-fetch requests, and refreshes it on expiry, so the typed client's compile-time guarantees mask a runtime auth failure. The x-api-key comment actively misleads a contributor into wiring the wrong credential.

Root cause

The OIDC/JWT backend work (#218) changed the auth model after api-client.ts was written, but the client was never updated to a token-aware fetch, and no CI/test exercises an authenticated request end-to-end.

Why this is architecturally hard

  1. Token lifecycle is a cross-cutting concern. The fix is not a one-line header: it needs token acquisition, storage, attach-on-request, and refresh-on-401 (with single-flight refresh so concurrent requests share one refresh). A naive headers: { Authorization: token } baked at client construction breaks the moment the token rotates.
  2. Mock and SSR paths must stay coherent. api-client.ts routes through fetchClient (mock interception via NEXT_PUBLIC_USE_MOCKS) and withTimeoutFetch; token attachment must compose with both, and with Next.js server/client boundaries (next.config.ts, src/proxy.ts, src/i18n.ts) rather than bypassing them.
  3. The generated types are the source of truth. The client types come from openapi.json via pnpm generate:api; the auth wiring must not fight the generated paths types, and adding auth headers must keep the typed request/response contract intact.
  4. Refresh failure must be observable. A 401 caused by an expired token must be distinguishable from a 401 caused by an unauthorized role so the client refreshes the former and surfaces the latter.

Proposed design

Introduce a token-aware fetch wrapper (or openapi-fetch middleware) that reads the token from a store, attaches Authorization: Bearer <token>, and performs single-flight refresh on 401. Keep createClient typed against paths, and update the x-api-key comment to reflect the JWT model.

Downstream impact

apiClient is the entry point for all frontend API calls (including verification-api.ts, verification-inbox-api.ts, and dashboard hooks under src/hooks). The auth model must stay consistent with app/backend/src/auth-oidc (JWT issuance/revocation). Regenerating types after any schema change is cd app/frontend && pnpm generate:api.

Acceptance criteria

Client

  • apiClient attaches a valid bearer token to authenticated requests without per-caller manual headers.
  • On 401 with an expired token, the client refreshes once (single-flight) and retries; a second 401 is surfaced as an auth error.
  • The x-api-key comment is removed/replaced with the correct JWT guidance.

Tests

  • A test proves authenticated requests carry the bearer header and that a 401 triggers a single refresh+retry.

Out of scope

The OpenAPI spec-drift CI gate and backend JWT issuance/revocation are separate issues.

Getting started

Files: app/frontend/src/lib/api-client.ts, app/frontend/src/lib/generated/api.ts, app/frontend/src/lib/env.ts, app/frontend/src/lib/retry.ts.

cd app/frontend
pnpm type-check
pnpm test

Good first files to read: lib/api-client.ts (the client + stale comment) and a few 401 ... valid JWT token required entries in lib/generated/api.ts.

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third Campaignarea:frontendFrontend (Next.js) areabugSomething isn't workinghighHigh severity issues

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions