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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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