docs

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 subject your 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 bridgeUrunWorkOSProvider 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-sessions

The 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)
CredentialOrg-scoped, minted by your serverUser-scoped, issued by your IdP
Identitysubject your route derives (or random)Verified sub/email from the token
Who can get oneAnyone your token route answersOnly users who pass your login
uRun secret in your stackOrg API key (server-side)None — trust is via public JWKS
Session coalescingStable subject = one user's tabs share a sessionThe user's sub does this naturally
Right forPrototypes, demos, internal toolsProducts 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/getAccessToken refresh 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 or NEXT_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), aud pinned.
  • The onramp route (if kept for internal tools) scoped with allowedFunctions + allowedOrigins + maxSessionS, and a stable subject.
  • Key rollover tested: new IdP key published in JWKS → sessions still admit.

Related:

On this page