Production Auth Best Practices
Put your identity provider in front — WorkOS AuthKit setup, trusted JWKS for your own issuer, per-user identity, and key rotation
In production, browser users authenticate against your identity provider, and uRun verifies their short-lived JWTs directly. The onramp token route gets you a first session in minutes; this page is what you switch to when real end users arrive.
Just getting started?
If you are prototyping or building an internal tool, the onramp — a token-vending route in your own app, minting scoped client tokens from your server-side API key — is the faster path. Come back here when your app has real users.
Why leave the onramp
Onramp tokens are org credentials: your route mints them from your org API key for anyone your server answers. That is exactly right for a demo or an internal tool, and exactly wrong once you have end users, because:
- Identity — an onramp token carries whatever
subjectyour route derives (or a random one). An IdP-issued JWT carries a verified user identity (sub,email), so sessions, logs, and usage attribute to real users. - Access control — with an IdP, "who may open a session" is your login + your rules, not "anyone who can reach my token route".
- Blast radius — the onramp path has one root secret (the org API key) on your server. The IdP path has none: uRun trusts your issuer's public JWKS; there is no uRun secret in your stack at all.
Two production modes follow. Both end the same way: the browser passes orgId plus a short-lived user JWT (via an auth bridge or getAccessToken), and uRun validates it server-side against a JWKS it trusts for your org.
Mode 1 — WorkOS AuthKit
If you use WorkOS as your own identity provider, register your WorkOS client ID's JWKS with uRun (the same registration path as any customer-JWT provider — see Mode 2 below), and uRun validates the access tokens AuthKit issues. This is the mode uRun's own example apps run.
1. Provisioning — register your org's WorkOS client ID ↔ JWKS mapping via
urun auth trust-jwk or Settings → Frontend Auth in the
console.
2. Wrap the app in the bridge — UrunWorkOSProvider reads the WorkOS AuthKit context and forwards refreshed access tokens to UrunProvider; you never wire token refresh yourself:
import { UrunProvider } from '@urun-sh/react'
import { UrunWorkOSProvider } from '@urun-sh/react/workos' // Next.js: '@urun-sh/react/next-workos'
export default function Root({ children }: { children: React.ReactNode }) {
return (
<UrunWorkOSProvider clientId="client_...">
<UrunProvider baseUrl="https://urun.sh" orgId="your-org-id" authProvider="workos" appId="my-app">
{children}
</UrunProvider>
</UrunWorkOSProvider>
)
}Pure React apps import the bridge from @urun-sh/react/workos; Next.js apps from @urun-sh/react/next-workos (it pairs with @workos-inc/authkit-nextjs). Both are optional peers — install the WorkOS package that matches your bridge.
3. Done — every session request now carries a short-lived WorkOS access token for the signed-in user; uRun validates it against the provisioned JWKS. Signed-out users can't open sessions.
Mode 2 — Customer JWT (your own issuer, trusted JWKS)
If you already have auth (Auth0, Clerk, Supabase, Cognito, your own issuer), register issuer-scoped trust once; uRun then validates your JWTs directly — it never mints, fetches, or stores them.
1. Register the trust relationship — point uRun at your issuer's JWKS, with the expected iss/aud:
urun auth trust-jwk https://auth.example.com/.well-known/jwks.json \
--issuer https://auth.example.com \
--audience urun-sessionsThe console twin is Settings → Frontend Auth. The JWKS URL must be HTTPS. Identity is always read from the token's sub and email claims — custom claim-name mapping is not yet supported. Full reference: urun auth trust-jwk.
2. Point the frontend at your tokens — enable JWT mode and hand the SDK your session's JWT:
import { UrunProvider, UrunAuthProvider } from '@urun-sh/react'
// NEXT_PUBLIC_AUTH_MODE=jwt
export default function Root({ children }: { children: React.ReactNode }) {
return (
<UrunAuthProvider getAccessToken={async () => myAuth.getSessionToken()}>
<UrunProvider baseUrl="https://urun.sh" orgId="your-org-id" appId="my-app">
{children}
</UrunProvider>
</UrunAuthProvider>
)
}Vanilla (@urun-sh/core) apps pass the same thing directly: App('my-app', { baseUrl, orgId, getAccessToken }).
3. Verification — at session admission, uRun resolves your registered JWKS by issuer, verifies the JWT's signature, iss, aud, and expiry, and reads the user identity from the token's sub and email claims. Bad or expired tokens are rejected at the door with a typed error the SDK surfaces on phase.error.
Per-user identity vs org tokens
| Onramp (scoped client token) | IdP JWT (WorkOS / customer) | |
|---|---|---|
| Credential | Org-scoped, minted by your server | User-scoped, issued by your IdP |
| Identity | subject your route derives (or random) | Verified sub/email from the token |
| Who can get one | Anyone your token route answers | Only users who pass your login |
| uRun secret in your stack | Org API key (server-side) | None — trust is via public JWKS |
| Session coalescing | Stable subject = one user's tabs share a session | The user's sub does this naturally |
| Right for | Prototypes, demos, internal tools | Products with real end users |
Key rotation
- IdP signing keys — rotate at your IdP as usual. uRun fetches keys from the registered JWKS URL, so publishing the new key in the JWKS (standard
kid-based rollover: publish new, issue with new, retire old) needs no uRun-side change. Never register a fixed key by value where a JWKS URL exists. - Access tokens — already short-lived by design (minutes). The bridges/
getAccessTokenrefresh them automatically; nothing to rotate. - Org API keys (CLI, deploys, an onramp route if you still run one) — rotate from the console (API keys); they are server-side-only credentials and must never appear in browser bundles, client env vars, or logs.
- Scoped client tokens — expire on their own (server max 3600s); expiry gates starting sessions only, so rotation is free.
Checklist
- No
urun_…API key or long-lived secret in any browser bundle orNEXT_PUBLIC_*var. - Browser auth = short-lived user JWTs via an auth bridge or
getAccessToken. - Customer-JWT: issuer trust registered against an HTTPS JWKS URL (not a pasted key),
audpinned. - The onramp route (if kept for internal tools) scoped with
allowedFunctions+allowedOrigins+maxSessionS, and a stablesubject. - Key rollover tested: new IdP key published in JWKS → sessions still admit.
Related:
- Getting started → the onramp — the prototype-speed path this page graduates from
- Frontend Auth (console) /
urun auth trust-jwk(CLI) — managing trusted issuers - React Providers — the full bridge/provider reference