Server quickstart
Your game API decides who a request belongs to by verifying the auth4.dev access token. The server SDK checks the signature, issuer, project, audience and expiry, and fetches signing keys only from your configured issuer.
Before you start
You need three values from the console:
| Option | Console value | Example |
|---|---|---|
tenantId | Project ID | ten_abcdefghijklmnop |
issuer | Issuer | https://auth.auth4.dev/t/ten_abcdefghijklmnop |
audience | Audience (the client ID of the application calling your API) | cli_… |
Configure these values on the server. Never read the issuer or key URL from the incoming token: the SDK always fetches keys from {issuer}/.well-known/jwks.json.
1. Add the SDK
The SDK is the @auth4/sdk-server workspace package in packages/sdk-server, built on jose. It isn't published to a package registry yet, so add it as a workspace dependency ("@auth4/sdk-server": "workspace:*"). The package is TypeScript source, so use Node.js 22.19 or later with type stripping, or a bundler such as Wrangler.
2. Protect an endpoint
Both examples share one handler. authenticateRequest returns the verified claims, or null for a missing, malformed, expired or otherwise invalid token. unauthorizedResponse() returns 401 withWWW-Authenticate: Bearer.
examples/server/shared/protected-endpoint.tsimport {
authenticateRequest,
unauthorizedResponse,
type Auth4ServerClient,
} from "../../../packages/sdk-server/src/index.ts";
export async function handleProtectedRequest(
request: Request,
client: Auth4ServerClient,
): Promise<Response> {
const claims = await authenticateRequest(request, client);
if (!claims) return unauthorizedResponse();
return Response.json({ subject: claims.sub, tenant: claims.tenant_id });
}Node.js
examples/server/node/src/index.tsconst client = new Auth4ServerClient({
tenantId: requiredEnvironment("AUTH4_TENANT_ID"),
issuer: requiredEnvironment("AUTH4_ISSUER"),
audience: requiredEnvironment("AUTH4_AUDIENCE"),
});
const handle = createNodeHandler(client);pnpm exec tsc --noEmit -p examples/server/node/tsconfig.json
AUTH4_TENANT_ID=ten_… \
AUTH4_ISSUER=https://auth.auth4.dev/t/ten_… \
AUTH4_AUDIENCE=cli_… \
node --experimental-strip-types examples/server/node/src/index.tsThe endpoint listens on http://127.0.0.1:3000. Try it with a token from your app:
curl -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:3000/Cloudflare Workers
Keep one verifier per Worker instance so its signing-key cache is reused across requests. The example rebuilds it only when its configuration changes:
examples/server/worker/src/index.tslet cachedConfig = "";
let cachedClient: Auth4ServerClient | undefined;
function clientFor(env: Env): Auth4ServerClient {
const config = JSON.stringify([env.AUTH4_TENANT_ID, env.AUTH4_ISSUER, env.AUTH4_AUDIENCE]);
if (cachedClient && cachedConfig === config) return cachedClient;
const client = new Auth4ServerClient({
tenantId: env.AUTH4_TENANT_ID,
issuer: env.AUTH4_ISSUER,
audience: env.AUTH4_AUDIENCE,
});
cachedConfig = config;
cachedClient = client;
return client;
}Set AUTH4_TENANT_ID, AUTH4_ISSUER and AUTH4_AUDIENCE in examples/server/worker/wrangler.jsonc, then check and bundle it:
pnpm exec tsc --noEmit -p examples/server/worker/tsconfig.json
pnpm exec wrangler deploy --dry-run --config examples/server/worker/wrangler.jsoncHono
honoBearerAuth stores the verified claims in the auth4Authvariable and returns 401 otherwise. Hono stays your application's dependency.
import { Hono } from "hono";
import {
Auth4ServerClient,
honoBearerAuth,
type VerifiedAccessTokenClaims,
} from "@auth4/sdk-server";
const auth4 = new Auth4ServerClient({ tenantId, issuer, audience });
const app = new Hono<{ Variables: { auth4Auth: VerifiedAccessTokenClaims } }>();
app.use("/api/*", honoBearerAuth(auth4));
app.get("/api/profile", (c) => c.json({ player: c.get("auth4Auth").sub }));3. Use the claims
| Claim | Meaning |
|---|---|
sub | The player's user ID. It stays the same when a guest upgrades. Key your game data on it. |
tenant_id | Your project ID. Always equal to the configured tenantId. |
client_id, aud | The application the token was issued to. |
scope | Space-separated granted scopes, for example openid email. |
sid | The session ID, when the token belongs to a session. |
exp, iat, jti | Expiry (at most five minutes after issue), issue time and token ID. |
Access tokens don't contain the player's email address. Call the project's UserInfo endpoint with the token if you need it and the email scope was granted. The optionalguest field is reserved and not issued yet, so don't rely on it to detect guests.
Accepting tokens from several applications
Each verifier accepts one audience. If both a browser application and a game application call the same API, create one verifier for each:
// One verifier per application whose tokens this API accepts.
const verifiers = [WEB_CLIENT_ID, GAME_CLIENT_ID].map(
(audience) => new Auth4ServerClient({ tenantId, issuer, audience }),
);
async function verify(request: Request) {
for (const verifier of verifiers) {
const claims = await authenticateRequest(request, verifier);
if (claims) return claims;
}
return null;
}Failures and key rotation
- Signing keys are cached. A token signed with an unknown key ID triggers one rate-limited key refresh, so key rotation needs no restart.
- If keys can't be fetched, verification fails closed.
authenticateRequestthen returnsnulllike any other failure. To tell an outage apart from a bad token, callvalidateAuthorizationHeaderand check theTokenVerificationErrorcode:
try {
const claims = await auth4.validateAuthorizationHeader(request.headers.get("authorization"));
// ...handle the request
} catch (error) {
if (error instanceof TokenVerificationError && error.code !== "invalid_token") {
// Signing keys could not be fetched: report an outage instead of signing the player out.
return new Response("Service unavailable", { status: 503, headers: { "Retry-After": "5" } });
}
return unauthorizedResponse();
}ID tokens, tokens from other projects, and console management tokens are always rejected. The default clock tolerance is zero. If your server's clock drifts, allow up to 60 seconds withclockToleranceSeconds.
Example notes: examples/server/README.md. Integration tests: tests/integration/auth4d-25.server-sdk.test.ts.