Search docs

Jump between documentation pages.

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

  1. 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).
  2. Add scopes (e.g. items:read, items:write) and an access policy that allows your client app to request them.
  3. Note the Issuer URI (e.g. https://dev-12345.okta.com/oauth2/default) and the client's Client ID.

2. Install

ts
pnpm add @okta/jwt-verifier

3. Environment variables

ts
# .env
OKTA_ISSUER=https://dev-12345.okta.com/oauth2/default
OKTA_CLIENT_ID=0oa1example2345
OKTA_AUDIENCE=api://default
OKTA_REQUIRED_SCOPE=items:read

4. Plugin

ts
// 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

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

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

ts
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.