Search docs

Jump between documentation pages.

Browse docs

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.

Okta access-token verification
Client appOkta Custom Auth ServerDaloyJS APIOkta JWKS
  1. 01asyncClient appOkta Custom Auth ServerUser signs in; Okta issues a scoped access token (RS256)iss = https://{domain}/oauth2/{asId}
  2. 02requestClient appDaloyJS APICall API with Authorization: Bearer <access token>
  3. 03asyncDaloyJS APIOkta JWKSOktaJwtVerifier fetches signing keys (cached 1h)jwksRequestsPerMinute throttles fetches
  4. 04noteDaloyJS APIDaloyJS APIverifyAccessToken checks issuer, audience, scp & assertClaims
  5. 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

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