Protect a DaloyJS API with Okta
Okta provides workforce and customer identity with granular policies and the API Access Management add-on for issuing custom-scoped access tokens. This guide uses the official @okta/jwt-verifier package (4.x, stable) to validate tokens from a Custom Authorization Server.
1. Configure an Okta Authorization Server
- In the Okta admin console, go to Security → API → Authorization Servers. Use the built-in
defaultserver or create a new Custom Authorization Server (requires the API Access Management license). - Add scopes (e.g.
items:read,items:write) and an access policy that allows your client app to request them. - Note the Issuer URI (e.g.
https://dev-12345.okta.com/oauth2/default) and the client's Client ID.
2. Install
pnpm add @okta/jwt-verifier3. Environment variables
# .env
OKTA_ISSUER=https://dev-12345.okta.com/oauth2/default
OKTA_CLIENT_ID=0oa1example2345
OKTA_AUDIENCE=api://default
OKTA_REQUIRED_SCOPE=items:read4. Plugin
// src/plugins/okta.ts
import OktaJwtVerifier from "@okta/jwt-verifier";
import type { App } from "@daloyjs/core";
const verifier = new OktaJwtVerifier({
issuer: process.env.OKTA_ISSUER!,
clientId: process.env.OKTA_CLIENT_ID,
// Defaults shown for transparency:
cacheMaxAge: 60 * 60 * 1000, // 1 hour
jwksRequestsPerMinute: 10,
});
export interface Principal {
sub: string;
scopes: string[];
groups: string[];
claims: Record<string, unknown>;
}
const expectedAudience = process.env.OKTA_AUDIENCE!;
export const oktaPlugin = {
name: "okta",
register(app: App) {
app.decorate("verifier", {
async verify(token: string): Promise<Principal> {
const { claims } = await verifier.verifyAccessToken(token, expectedAudience);
const scopes = Array.isArray(claims.scp)
? (claims.scp as string[])
: typeof claims.scp === "string"
? (claims.scp as string).split(" ")
: [];
const groups = Array.isArray(claims.groups) ? (claims.groups as string[]) : [];
return { sub: String(claims.sub), scopes, groups, claims };
},
});
},
};
declare module "@daloyjs/core" {
interface AppState {
verifier: { verify(token: string): Promise<Principal> };
principal?: Principal;
}
}5. Guard a route
import { z } from "zod";
import { App, secureHeaders, rateLimit } from "@daloyjs/core";
import { oktaPlugin } from "./plugins/okta";
import { requireAuth } from "./plugins/auth"; // from the Overview page
const app = new App();
app.use(secureHeaders());
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
app.register(oktaPlugin);
app.route({
method: "GET",
path: "/items",
operationId: "listItems",
middleware: [requireAuth(process.env.OKTA_REQUIRED_SCOPE!)],
responses: {
200: { description: "OK", body: z.object({ user: z.string() }) },
},
handler: ({ state }) => ({ status: 200, body: { user: state.principal!.sub } }),
});Custom claim assertions
The verifier can enforce extra claims at construction time. For example, to require that the token includes both items:read and items:write in the space-separated scp claim:
const verifier = new OktaJwtVerifier({
issuer: process.env.OKTA_ISSUER!,
clientId: process.env.OKTA_CLIENT_ID,
assertClaims: {
"scp.includes": ["items:read", "items:write"],
"groups.includes": ["Engineering"],
},
});Verifying ID tokens
Use verifyIdToken(token, expectedClientId, expectedNonce?) if your client also sends ID tokens (for example, to populate a user profile). Pass the nonce only when the original auth request included one.
Custom JWKS URI
When the JWKS isn't under the issuer (e.g. you front Okta with a proxy), pass jwksUri explicitly:
const verifier = new OktaJwtVerifier({
issuer: process.env.OKTA_ISSUER!,
clientId: process.env.OKTA_CLIENT_ID,
jwksUri: "https://dev-12345.okta.com/oauth2/v1/keys",
});Runtimes
@okta/jwt-verifier is a Node-only library (it imports Node modules transitively). For Node, Bun, and AWS Lambda it works out of the box; for Cloudflare Workers or Vercel Edge, use jose's createRemoteJWKSet + jwtVerify against the same issuer (the Auth0 page shows that exact pattern — only the issuer URL changes).
Org server vs Custom Authorization Server
Only tokens from a Custom Authorization Server are meant to be verified by your app — those issuers look like https://{domain}/oauth2/{asId}. The Org Authorization Server (https://{domain}) issues opaque tokens that only Okta should consume; validate those via the /introspect endpoint instead.
See also Auth0, Clerk, and the auth integrations overview.