Migrating from Okta's Default Org Authorization Server to Custom AS: A Practical Walkthrough
Moving from Okta's default Org Authorization Server to a Custom AS lets you issue JWTs with your own scopes and audiences, but adds licensing, rate limits, and migration complexity. A practical walkthrough with validation and trade-offs.
15 Apr 2026, 02:57 UTC

The Problem: One Authorization Server Doesn't Fit All
Most Okta tenants start with the default Org Authorization Server (the one at https://{your-domain}.okta.com/oauth2/default). It works fine for Okta API access and basic OIDC flows. But as soon as you need to issue access tokens for your own APIs—with custom scopes, audiences, or claims tied to your business logic—the default AS becomes a constraint. You can't change its issuer URI, you can't define custom scopes that map to your microservice permissions, and you can't enforce per-client access policies without affecting Okta's own management APIs.
The takeaway: Custom Authorization Servers are the supported way to separate your API authorization from Okta's internal authorization. They're a paid feature (API Access Management SKU), so verify licensing before designing around them.
What Changes When You Create a Custom AS
A Custom AS gets its own issuer URI (e.g., https://{your-domain}.okta.com/oauth2/ausabc123), its own signing keys (rotated automatically by Okta), and its own JWKS endpoint. You define scopes like orders:read or billing:write that map to your resource server permissions. Access policies let you restrict which clients can request which scopes—so your frontend SPA gets profile:read while your backend service gets admin:all.
Tokens issued by the Custom AS are JWTs (RS256 by default). Your resource servers validate them by fetching the JWKS from https://{your-domain}.okta.com/oauth2/{authServerId}/v1/keys and verifying the signature, iss, aud, and expiration. Okta also supports opaque tokens with introspection if you need immediate revocation checks, but that adds a network call per request.
Worked Example: Validating a Custom AS Token in a Node Resource Server
Below is a minimal Express middleware that validates a JWT from a Custom AS. It caches the JWKS for the Cache-Control max-age (typically 1 hour) to avoid hammering Okta's keys endpoint—a critical practice given the 1,000 requests/minute token endpoint limit and 100/minute introspection limit.
// Run this in your Node.js resource server (Express/Fastify)
// Requires: npm install jose
import { createRemoteJWKSet, jwtVerify } from 'jose';
const ISSUER = 'https://your-org.okta.com/oauth2/ausabc123';
const JWKS_URI = `${ISSUER}/v1/keys`;
const EXPECTED_AUD = 'api://your-resource-server';
// Cache JWKS; jose respects Cache-Control max-age automatically
const jwks = createRemoteJWKSet(new URL(JWKS_URI), {
cacheMaxAge: 3600000, // 1 hour fallback
cooldownDuration: 60000, // 1 min cooldown on fetch errors
});
export async function validateAccessToken(req, res, next) {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'missing_token' });
}
const token = auth.slice(7);
try {
const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
audience: EXPECTED_AUD,
// Optional: require a specific scope
// custom: { requiredScope: 'orders:read' },
});
// Attach claims for downstream handlers
req.auth = payload;
next();
} catch (err) {
// Distinguish expired vs invalid signature vs wrong audience
const status = err.code === 'ERR_JWT_EXPIRED' ? 401 : 403;
return res.status(status).json({ error: 'invalid_token', reason: err.code });
}
}
Where to run: Your API gateway or resource server process. Permissions: Network egress to your Okta domain on HTTPS. Placeholders: Replace your-org, ausabc123, and api://your-resource-server with your actual values. Expected check: A valid token returns 200 and populates req.auth with sub, scp (scopes array), cid (client ID), and any custom claims. Risk: If JWKS fetch fails and cache is stale, all requests return 403. Monitor JWKS fetch latency and errors.
Trade-offs and Limitations
- Licensing: Custom AS requires the API Access Management SKU. The default Org AS is included in base Okta licensing.
- Rate limits: 1,000 token requests/minute per org, 100 introspection requests/minute. Implement exponential backoff with
Retry-Afterheader handling in your clients. Test by loopingcurlagainst/oauth2/{authServerId}/v1/tokenuntil you hit 429. - Inline Hooks latency: Token inline hooks have a 3-second hard timeout. Design external enrichment endpoints for <500ms p99. A slow hook breaks every token request for that AS.
- Migration is disruptive: Moving clients from the default AS to a Custom AS means updating every client's issuer, audience, JWKS URL, and token validation logic simultaneously. Plan a deprecation window with overlapping key rotation; deleting a Custom AS invalidates all its issued tokens immediately.
- Expression language limits: Custom claims via Okta Expression Language run in a sandbox. Complex logic may hit execution limits. Test thoroughly in a preview org before production.
Verification Checklist Before Going Live
- Create a free Okta Developer org and enable the API Access Management trial.
- Create a Custom AS, define scopes (
orders:read,orders:write), and an access policy granting them to a test client. - Use
curl -X POST -u \"client_id:client_secret\" -d \"grant_type=client_credentials&scope=orders:read\" https://your-org.okta.com/oauth2/ausabc123/v1/tokento mint a token. Inspect the JWT: verifyiss,aud,scp, and custom claims. - Point your resource server's validation logic at the Custom AS JWKS. Confirm successful validation and scope enforcement.
- Simulate rate limiting: loop token requests until 429; verify your client respects
Retry-After. - Deploy a test inline hook (e.g., a Cloudflare Worker adding a
departmentclaim) and measure end-to-end issuance latency. Target <500ms p99. - Review Okta's current rate limit documentation and pricing page to confirm limits and feature availability for your tier.
Closing Action
If you're still on the default Org AS for your own APIs, spin up a Custom AS in a preview org this week. Define one scope, one policy, and one test client. Mint a token and validate it in your staging resource server using the JWKS caching pattern above. The migration effort is real, but the separation of concerns—your API authorization independent of Okta's management API authorization—pays off every time you add a new microservice or change a permission model.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.