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.
05responseDaloyJS APIClient appReturn protected data after requireAuth passes
Only tokens from a Custom Authorization Server are meant to be verified by your app. The Org Authorization Server issues opaque tokens that you introspect instead, never verify locally.
1. Configure an Okta Authorization Server
In the Okta admin console, go to Security -> API -> Authorization Servers. Use the built-in default server 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.
// src/plugins/okta.tsimport 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!;// Call with the root app, not through app.register(): a decoration made// inside a registered plugin is visible only to that plugin's own routes.export function oktaPlugin(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
ts
import { z } from "zod";import { App, secureHeaders, rateLimit } from "@daloyjs/core";import { oktaPlugin } from "./plugins/okta.ts";import { requireAuth } from "./plugins/auth.ts"; // from the Overview pageconst app = new App();app.use(secureHeaders());app.use(rateLimit({ windowMs: 60_000, max: 100 }));oktaPlugin(app);app.get( "/items", { operationId: "listItems", hooks: requireAuth(process.env.OKTA_REQUIRED_SCOPE!), responses: { 200: { description: "OK", body: z.object({ user: z.string() }) }, }, }, ({ 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:
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:
@okta/jwt-verifier is a Node-only library (it imports Node modules transitively). It works on Node, Bun, and AWS Lambda. For Cloudflare Workers, 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.