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:

OptionConsole valueExample
tenantIdProject IDten_abcdefghijklmnop
issuerIssuerhttps://auth.auth4.dev/t/ten_abcdefghijklmnop
audienceAudience (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.ts
import {
  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

Excerpt from examples/server/node/src/index.ts
const client = new Auth4ServerClient({
  tenantId: requiredEnvironment("AUTH4_TENANT_ID"),
  issuer: requiredEnvironment("AUTH4_ISSUER"),
  audience: requiredEnvironment("AUTH4_AUDIENCE"),
});
const handle = createNodeHandler(client);
Run the Node example from the repository root
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.ts

The 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:

Excerpt from examples/server/worker/src/index.ts
let 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.jsonc

Hono

honoBearerAuth stores the verified claims in the auth4Authvariable and returns 401 otherwise. Hono stays your application's dependency.

Hono middleware
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

ClaimMeaning
subThe player's user ID. It stays the same when a guest upgrades. Key your game data on it.
tenant_idYour project ID. Always equal to the configured tenantId.
client_id, audThe application the token was issued to.
scopeSpace-separated granted scopes, for example openid email.
sidThe session ID, when the token belongs to a session.
exp, iat, jtiExpiry (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 returns null like any other failure. To tell an outage apart from a bad token, call validateAuthorizationHeader and check the TokenVerificationError code:
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.