feat(auth): cross-platform OIDC client package #16

Merged
admin-mrrm merged 1 commit from feat/auth-package into main 2026-04-15 09:53:41 +02:00
Owner

Summary

  • New workspace package @mrrmlab/auth — OAuth2 + authorization-code-with-PKCE flow against any OIDC issuer (target: Keycloak)
  • Single code path runs in Vite, Expo, Node SSR, and tests — only fetch, globalThis.crypto, and jose (already used in the API guard). No oidc-client-ts, no expo-auth-session.
  • Bridges directly into @mrrmlab/api-client via getAccessToken()

Modules

  • pkce.ts — RFC 7636 verifier + S256 challenge
  • discovery.ts/.well-known/openid-configuration fetch + Zod validation
  • oidc.ts — auth/logout URL builders, token endpoint client (exchange + refresh)
  • id-token.tsverifyIdToken via jose with cached JWKS, claims projection
  • token-store.tsTokenStore interface, InMemoryTokenStore, WebStorageTokenStore
  • auth-client.tsAuthClient orchestrator: startLogin, handleCallback, getAccessToken (auto-refresh + skew), getUser, logout, onChange

Design notes worth surfacing

  • Logout is fail-safe: local token clearing always succeeds even if discovery is unreachable. Caught by the smoke test before commit.
  • Single in-flight refresh: simultaneous getAccessToken() callers share one network round-trip via a memoized promise.
  • Refresh token rotation: prefers the new refresh_token from the response, falls back to the previous one (Keycloak rotates, the spec doesn't require it).
  • Replay protection: id_token.nonce is verified against the value persisted in the pending login.
  • Mobile story: package is platform-agnostic — apps/mobile will provide a SecureTokenStore (Expo SecureStore) and drive the redirect with expo-web-browser. README has the recipe.

Test plan

  • pnpm --filter @mrrmlab/auth build succeeds
  • 27 smoke checks pass: PKCE primitives, URL builders, store round-trip, expired-token clears state, listener fires on logout, logout survives unreachable issuer
  • Network integration (real Keycloak round-trip) deferred until #11 / #12 wire up the apps

Closes #8

## Summary - New workspace package `@mrrmlab/auth` — OAuth2 + authorization-code-with-PKCE flow against any OIDC issuer (target: Keycloak) - Single code path runs in **Vite, Expo, Node SSR, and tests** — only `fetch`, `globalThis.crypto`, and `jose` (already used in the API guard). No `oidc-client-ts`, no `expo-auth-session`. - Bridges directly into `@mrrmlab/api-client` via `getAccessToken()` ## Modules - `pkce.ts` — RFC 7636 verifier + S256 challenge - `discovery.ts` — `/.well-known/openid-configuration` fetch + Zod validation - `oidc.ts` — auth/logout URL builders, token endpoint client (exchange + refresh) - `id-token.ts` — `verifyIdToken` via `jose` with cached JWKS, claims projection - `token-store.ts` — `TokenStore` interface, `InMemoryTokenStore`, `WebStorageTokenStore` - `auth-client.ts` — `AuthClient` orchestrator: `startLogin`, `handleCallback`, `getAccessToken` (auto-refresh + skew), `getUser`, `logout`, `onChange` ## Design notes worth surfacing - **Logout is fail-safe**: local token clearing always succeeds even if discovery is unreachable. Caught by the smoke test before commit. - **Single in-flight refresh**: simultaneous `getAccessToken()` callers share one network round-trip via a memoized promise. - **Refresh token rotation**: prefers the new `refresh_token` from the response, falls back to the previous one (Keycloak rotates, the spec doesn't require it). - **Replay protection**: `id_token.nonce` is verified against the value persisted in the pending login. - **Mobile story**: package is platform-agnostic — apps/mobile will provide a `SecureTokenStore` (Expo SecureStore) and drive the redirect with `expo-web-browser`. README has the recipe. ## Test plan - [x] `pnpm --filter @mrrmlab/auth build` succeeds - [x] 27 smoke checks pass: PKCE primitives, URL builders, store round-trip, expired-token clears state, listener fires on logout, logout survives unreachable issuer - [ ] Network integration (real Keycloak round-trip) deferred until #11 / #12 wire up the apps Closes #8
New workspace package @mrrmlab/auth implementing the OAuth2 +
authorization-code-with-PKCE flow against any OIDC issuer (target:
Keycloak). Same code path runs in Vite, Expo, and Node SSR — only
platform primitives are used (fetch, globalThis.crypto, jose for ID
token verification). No oidc-client-ts, no expo-auth-session.

Modules:
- pkce.ts             — RFC 7636 verifier + S256 challenge
- discovery.ts        — fetches and validates /.well-known/openid-configuration
- oidc.ts             — auth/logout URL builders, token endpoint client
                        (exchange + refresh), TokenEndpointError
- id-token.ts         — verifyIdToken via jose with cached JWKS,
                        toAuthUser claims projection
- token-store.ts      — TokenStore interface, InMemoryTokenStore,
                        WebStorageTokenStore (sessionStorage/localStorage)
- auth-client.ts      — AuthClient orchestrator: startLogin, handleCallback,
                        getAccessToken (with auto-refresh + skew), isAuthenticated,
                        getUser, logout, onChange listeners
- types.ts            — Zod schemas + inferred types

Integration story: AuthClient.getAccessToken() bridges directly into
@mrrmlab/api-client.createApiClient({ getToken }), so the rest of the
codebase never touches the token store.

A couple of intentional design choices:
- Logout always succeeds locally even if the issuer is unreachable —
  we don't want a flaky network to leave the user "still signed in"
  on the device. RP-Initiated Logout URL is best-effort.
- Single in-flight refresh promise so simultaneous getAccessToken()
  callers share one network round-trip.
- Refresh-token rotation: new refresh_token from the response is
  preferred, but we fall back to the previous one if the issuer didn't
  rotate (Keycloak does, but the spec doesn't require it).
- ID-token nonce verified against the value persisted in the pending
  login — closes the OIDC replay window.

Build setup matches shared-types and api-client (CJS dist,
tsBuildInfoFile inside dist/).

Smoke-tested locally (27 checks): PKCE primitives, URL builders for
auth/logout, store round-trip, expired-token clears state, listener
fires on logout. Full network integration deferred until a Keycloak
client is configured for the web/mobile apps in #11/#12.

Closes #8
admin-mrrm deleted branch feat/auth-package 2026-04-15 09:53:41 +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!16
No description provided.