@touchtech/web-platform-auth
v0.0.1
Published
OIDC / OAuth 2.0 authentication library for **React Router v7+** applications. Provides server-side session management, PKCE authorization code flow, JWT verification via JWKS, and cross-app SSO through localStorage synchronisation.
Maintainers
Keywords
Readme
@touchtech/web-platform-auth
OIDC / OAuth 2.0 authentication library for React Router v7+ applications. Provides server-side session management, PKCE authorization code flow, JWT verification via JWKS, and cross-app SSO through localStorage synchronisation.
Package entry points
| Import path | Environment | Purpose |
|---|---|---|
| @touchtech/web-platform-auth | Shared | Type definitions and constants (LS_KEY, EXPIRY_BUFFER_SEC) |
| @touchtech/web-platform-auth/server | Node.js | Auth factory, Redis session storage, token verifier, origin resolver |
| @touchtech/web-platform-auth/client | Browser | useAuthSync and useTenantSync React hooks |
Installation
npm install @touchtech/web-platform-authPeer dependencies
| Package | Version | Required |
|---|---|---|
| jose | >=5 | Yes |
| react | >=18 | Yes |
| react-router | >=7 | Yes |
| ioredis | >=5 | Only if using createRedisSessionStorage |
Server API
createAuth(config: AuthConfig)
Factory that returns all server-side auth helpers. Call once at app startup and reuse the returned object.
import { createAuth, createRedisSessionStorage } from "@touchtech/web-platform-auth/server";
const sessionStorage = createRedisSessionStorage({
cookie: { name: "__session", secrets: ["s3cret"], sameSite: "lax" },
redisUrl: process.env.REDIS_URL,
});
const auth = createAuth({
identityServiceUrl: "https://identity.example.com",
clientId: "my-client-id",
baseRoute: "/my-app",
sessionStorage,
identityProvider: "default", // or (hostname) => hostname === "a.example.com" ? "idp-a" : "idp-b"
scopes: "openid offline_access", // optional, this is the default
});AuthConfig fields
| Field | Type | Description |
|---|---|---|
| identityServiceUrl | string | OIDC issuer base URL (e.g. https://identity.example.com) |
| clientId | string | OAuth 2.0 client_id |
| baseRoute | string | Route prefix the app is served under (e.g. "/my-app"). Empty string for root. |
| sessionStorage | SessionStorage | A React Router SessionStorage instance |
| identityProvider | string \| ((hostname: string) => string) | Optional. Identity provider value sent as idp query param. |
| scopes | string | Optional. Defaults to "openid offline_access". |
Returned methods
| Method | Signature | Description |
|---|---|---|
| requireUser | (request: Request) => Promise<{ user, headers, accessToken }> | Returns the authenticated user or throws a redirect to the IdP login. Use in loaders that require authentication. |
| getUser | (request: Request) => Promise<{ user, headers, accessToken } \| null> | Like requireUser but returns null instead of redirecting. |
| getSessionState | (request: Request) => Promise<{ user, headers, accessToken, syncPayload } \| null> | Returns user info plus a SyncPayload for the client-side useAuthSync hook. Use in the root loader. |
| getTokenClaims | (request: Request) => Promise<{ accessToken, idToken }> | Decodes JWT payloads from the session without verification. |
| beginLogin | (request: Request) => Promise<Response> | Initiates PKCE authorization code flow. Generates code verifier/challenge, stores state in session, and redirects to the IdP. |
| handleAuthCallback | (request: Request) => Promise<Response> | Handles the /auth callback. Exchanges the authorization code for tokens, stores them in the session, and redirects to the original URL. |
| bootstrapSessionFromLS | (request, lsTokens) => Promise<{ ok, headers, updatedTokens? } \| { ok: false }> | Creates a new server session from localStorage tokens (cross-app SSO bootstrap). Verifies tokens via JWKS and refreshes if expired. |
| syncSessionFromLS | (request, lsTokens) => Promise<{ ok, headers, updatedTokens? } \| { ok: false }> | Updates an existing session with newer localStorage tokens. |
| destroySessionGracefully | (request: Request) => Promise<Headers> | Destroys the session and calls the IdP end-session endpoint (best-effort). |
| markLSSynced | (request: Request) => Promise<Headers> | Sets the ls_synced flag in the session. |
| handleLogout | (request: Request) => Promise<Response> | Destroys the session and redirects to the IdP logout endpoint with post_logout_redirect_uri. |
createRedisSessionStorage(options)
Creates a React Router SessionStorage backed by Redis (via ioredis).
import { createRedisSessionStorage } from "@touchtech/web-platform-auth/server";
const sessionStorage = createRedisSessionStorage({
cookie: { name: "__session", secrets: ["s3cret"], sameSite: "lax" },
redisUrl: "redis://localhost:6379", // defaults to REDIS_URL env var
});Session data is stored as JSON under Redis keys prefixed with session:. TTL is derived from the cookie expires option when set.
createTokenVerifier(idpBaseUrl, clientId)
Returns an async function that verifies both access_token and id_token against the IdP's JWKS endpoint. Used internally by createAuth but can be used standalone.
access_token: verified for signature +iss(noaud— Duende convention).id_token: verified for signature +iss+aud(must equalclientId).
Returns { valid: boolean, expired: boolean }. expired is only meaningful when valid is true.
resolvePublicOrigin(request)
Returns the public-facing origin, preferring X-Forwarded-Host / X-Forwarded-Proto headers over the internal request.url origin. Useful behind reverse proxies.
Client API
useAuthSync(syncPayload, options?)
React hook that reconciles the server session with localStorage after hydration. Must be called in a component rendered by the root route.
import { useAuthSync } from "@touchtech/web-platform-auth/client";
export default function Root() {
const { syncPayload } = useLoaderData<typeof loader>();
useAuthSync(syncPayload, {
syncActionPath: "/auth-sync", // default
loginPath: "/login", // default
});
return <Outlet />;
}Sync behaviour (case matrix)
The hook evaluates state on every idle cycle and on cross-tab storage events:
| Server session | localStorage | Condition | Action |
|---|---|---|---|
| None | None | — | Navigate to login |
| None | Has tokens | — | bootstrap (verify + create session) |
| Exists | None | ls_synced=false | Write session tokens to LS, ack |
| Exists | None | ls_synced=true | destroy session (user logged out in another tab) |
| Exists | Has tokens | Tokens match | No-op (ack if needed) |
| Exists | Has tokens | LS newer | sync-from-ls (update session) |
| Exists | Has tokens | Session newer | Write session tokens to LS |
The sync action route (default /auth-sync) must handle POST requests with JSON body { intent, tokens? }. Intent values: "bootstrap", "sync-from-ls", "write-ls-ack", "destroy".
useTenantSync(tenantId)
Keeps a tenantId cookie and localStorage entry in sync across tabs. Returns { switchTenant } to programmatically change the active tenant.
import { useTenantSync } from "@touchtech/web-platform-auth/client";
function App({ tenantId }: { tenantId: string }) {
const { switchTenant } = useTenantSync(tenantId);
// switchTenant("new-tenant-id") updates cookie + LS + triggers revalidation
}Types
All types are exported from the root entry point and re-exported from server and client where relevant.
| Type | Description |
|---|---|
| AuthConfig | Configuration for createAuth() |
| AuthUser | Decoded user from access token claims (sub, email, name, given_name, family_name) |
| TokenSet | Internal token set stored in the server session |
| LSTokenData | Token shape stored in localStorage under key AuthFlow:tokenResponse |
| SyncPayload | Data returned by getSessionState() for the client sync hook |
| SyncActionResponse | Response shape from the sync action route ({ ok: true, updatedTokens? } or { ok: false, action: "login", clearLS? }) |
| RedisSessionStorageOptions | Options for createRedisSessionStorage() |
Constants
| Constant | Value | Description |
|---|---|---|
| LS_KEY | "AuthFlow:tokenResponse" | localStorage key used for cross-app SSO |
| EXPIRY_BUFFER_SEC | 60 | Seconds before actual expiry to trigger a token refresh |
Project structure
src/
├── index.ts # Root entry — re-exports types and constants
├── types.ts # All shared type definitions and constants
├── server/
│ ├── index.ts # Server entry — re-exports all server modules
│ ├── auth.ts # createAuth() factory with PKCE, token exchange, refresh, SSO
│ ├── jwks.ts # createTokenVerifier() — JWT verification via JWKS
│ ├── session-redis.ts # createRedisSessionStorage() — Redis-backed sessions
│ └── request-origin.ts # resolvePublicOrigin() — proxy-aware origin resolution
└── client/
├── index.ts # Client entry — re-exports hooks and client types
├── use-auth-sync.ts # useAuthSync() — cross-app SSO reconciliation hook
└── use-tenant-sync.ts # useTenantSync() — multi-tenant sync hookBuild
npm run build # Production build with tsup (ESM + .d.ts)
npm run dev # Watch mode
npm run typecheck # Type-check without emittingThe build produces three bundles under dist/ matching the three exports entry points. ioredis is marked as external.
Publishing
Publishing is automated via GitHub Actions (.github/workflows/npm-publish.yml). A GitHub release triggers the workflow which runs npm test then npm publish to the @touchtech scope on the npm registry.
