Validate an access token (Node.js)
Verify Olympus access tokens in a Node.js backend
This recipe shows how to validate an access token issued by Olympus Hydra in a Node.js backend service.
There are two paths depending on whether you've configured Hydra to issue opaque tokens (the Olympus default) or JWT tokens.
Path 1, Opaque tokens (default)
Opaque tokens are random strings that must be introspected at Hydra's /oauth2/introspect endpoint.
Install
bun add ofetchImplement
// src/lib/auth.ts
import { ofetch } from "ofetch";
const HYDRA_INTROSPECT = process.env.HYDRA_INTROSPECT_URL!;
// e.g. https://ciam.your-domain/.ory/hydra/admin/oauth2/introspect
// (NOTE: introspect is on the admin API, which must be firewalled, see Operate, Network Topology)
const HYDRA_ADMIN_AUTH = `Basic ${Buffer.from(
`${process.env.HYDRA_ADMIN_USER}:${process.env.HYDRA_ADMIN_PASS}`,
).toString("base64")}`;
export interface TokenInfo {
active: boolean;
sub?: string;
scope?: string;
client_id?: string;
exp?: number;
iat?: number;
iss?: string;
aud?: string[];
// any custom claims you've configured
ext?: Record<string, unknown>;
}
export async function validateToken(token: string): Promise<TokenInfo> {
const res = await ofetch<TokenInfo>(HYDRA_INTROSPECT, {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
authorization: HYDRA_ADMIN_AUTH,
},
body: new URLSearchParams({ token }).toString(),
});
if (!res.active) {
throw new Error("Token inactive (expired, revoked, or unknown)");
}
return res;
}Wire into your handler
// src/app/api/widgets/route.ts (Next.js)
import { validateToken } from "@/lib/auth";
export async function GET(request: Request) {
const auth = request.headers.get("authorization");
if (!auth?.startsWith("Bearer ")) {
return Response.json({ error: "missing_token" }, { status: 401 });
}
try {
const info = await validateToken(auth.slice(7));
if (!info.scope?.split(" ").includes("read:widgets")) {
return Response.json({ error: "insufficient_scope" }, { status: 403 });
}
return Response.json({ widgets: [], user: info.sub });
} catch (e) {
return Response.json({ error: "invalid_token" }, { status: 401 });
}
}Note: introspection cost
Each introspection is one HTTP round-trip to Hydra. If your service handles >1000 RPS, cache introspection results for short windows (30s) keyed by token hash. Be careful with revocation latency: cached "active" tokens stay accepted until the cache window expires.
Path 2, JWT tokens (configured per client)
If you've registered the OAuth2 client with access_token_strategy=jwt, the access token is a JWT signed by Hydra. Validate locally with the JWKS.
Install
bun add joseImplement
// src/lib/auth-jwt.ts
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
const ISSUER = process.env.OLYMPUS_ISSUER!;
// e.g. https://ciam.your-domain
const JWKS = createRemoteJWKSet(
new URL(`${ISSUER}/.well-known/jwks.json`),
{ cacheMaxAge: 10 * 60 * 1000 }, // 10 min
);
export interface TokenClaims extends JWTPayload {
scope?: string;
client_id?: string;
}
export async function validateJwt(token: string): Promise<TokenClaims> {
const { payload } = await jwtVerify(token, JWKS, {
issuer: ISSUER,
// audience: optionally restrict to your client_id
});
return payload as TokenClaims;
}Wire it in
Same shape as the opaque example, but call validateJwt instead of validateToken. No network round-trip on the hot path (after the initial JWKS fetch is cached).
Note: JWT revocation
JWT access tokens are stateless. They cannot be revoked before their expiry. If you need revocation on demand (logout-and-forget), use opaque tokens or accept the revocation latency = access-token lifetime (default 1 hour).
Choosing between paths
| Use case | Recommendation |
|---|---|
| Backend can tolerate ~30s latency to revoke a token | Opaque (default) |
| Service handles >1000 RPS and revocation latency of 1h is acceptable | JWT |
| Multi-region service where introspecting cross-region is too slow | JWT |
| Per-user logout must take effect immediately | Opaque (must) |
Related
- Integrate, OAuth2 overview, picking the right grant.
- Integrate, OAuth2 Refresh Tokens, renewing without re-login.
- Reference, Hydra introspect endpoint.
- Cookbook, Validate access token (Go)
- Cookbook, Validate access token (Python)