Choosing Between Okta Org and Custom Authorization Servers for Your API
Learn why Okta’s Org Authorization Server can’t issue custom scopes and how a Custom Authorization Server gives you least‑privilege tokens with verifiable issuer and audience claims.
11 Mar 2026, 13:30 UTC

The problem: tokens that work everywhere but not for your API
You have built a REST API that expects OAuth 2.0 access tokens with scopes like orders:read or billing:write. When you test with a token obtained from Okta’s default Org Authorization Server, the API returns 403 Forbidden because the token’s aud claim is api://okta and it lacks the custom scopes you defined. The token is still validly signed, so a naïve validation that only checks the signature would accept it, opening a replay risk across environments.
Thesis: use a Custom Authorization Server to get least‑privilege tokens that match your API’s audience and scopes
Okta provides two kinds of authorization servers:
- Org Authorization Server – the default server, audience fixed to
api://okta, scopes limited to Okta’s built‑in set (e.g.,openid,profile,offline_access). It cannot mint tokens for custom audiences or custom scopes. - Custom Authorization Server – user‑defined server with its own issuer URL, audience, scopes, claims, and access policies. It lets you issue tokens that carry exactly the permissions your API expects.
If your API is public, versioned, or needs fine‑grained consent, a Custom Authorization Server is the supported way to avoid work‑arounds and to enforce least‑privilege token issuance.
Designing a Custom Authorization Server for least privilege
Before you create the server, decide on three items:
- Audience – the value your API will check in the
audclaim (e.g.,https://api.example.com/orders). - Scopes – the granular permissions you want to expose (e.g.,
orders:read,orders:write). - Access policy – rules that restrict grant types, token lifetimes, and which clients can request which scopes.
These settings are immutable after creation; changing the server ID or issuer requires recreating the server and updating all clients.
Worked example: create a server via the Okta API and validate a token
Where to run: a terminal with curl and jq installed. Permissions needed: an Okta API token with scopes okta.authorizationServers.manage, okta.clients.manage, and okta.users.read (to test).
# Variables – replace with your own values
OKTA_DOMAIN="your-dev.okta.com"
API_TOKEN="{{your_okta_api_token}}"
CLIENT_ID="{{your_client_id}}"
CLIENT_SECRET="{{your_client_secret}}"
# 1. Create a Custom Authorization Server
curl -X POST "https://${OKTA_DOMAIN}/api/v1/authorizationServers" \
-H "Authorization: SSWS ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Orders API Server",
"description": "Server for the orders microservice",
"audience": "https://api.example.com/orders",
"scope": [
{"value": "orders:read", "description": "Read access to orders"},
{"value": "orders:write", "description": "Write access to orders"}
]
}'
# Response includes "id" (the authServerId) and "issuer"
# 2. Create a client that can request client‑credentials tokens
curl -X POST "https://${OKTA_DOMAIN}/api/v1/clients" \
-H "Authorization: SSWS ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Orders API Client",
"client_uri": "https://example.com",
"logo_uri": "https://example.com/logo.png",
"grant_types": ["client_credentials"],
"response_types": ["token"],
"application_type": "service",
"secret": "{{your_client_secret}}"
}'
# 3. Request a token using client credentials
TOKEN_RESPONSE=$(curl -s -X POST "https://${OKTA_DOMAIN}/oauth2/${authServerId}/v1/token" \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-d "grant_type=client_credentials&scope=orders:read")
ACCESS_TOKEN=$(echo "${TOKEN_RESPONSE}" | jq -r .access_token)
# 4. Validate the token locally (example using jose library)
# Fetch JWKS for the specific server
JWKS_URL="https://${OKTA_DOMAIN}/oauth2/${authServerId}/v1/keys"
# In code: verify signature with JWKS, then check:
# iss == https://${OKTA_DOMAIN}/oauth2/${authServerId}/
# aud == https://api.example.com/orders
# cid == ${CLIENT_ID}
# exp > now
# scope contains orders:read
Expected checks: The token’s iss must match the issuer URL of the server you created, the aud must equal the audience you set, and the scope (or scp claim) must contain orders:read. If any of these fail, reject the token.
Risks: If you only verify the signature and ignore iss or aud, a token issued from the Org Authorization Server (or from another custom server) could be accepted, allowing privilege escalation or replay across environments.
Trade‑offs and limitations
- Immutability: The authorization server ID and issuer URL cannot be changed after creation. To modify them you must create a new server and update every client configuration.
- Configuration surface area: Each custom server adds its own set of policies, scopes, and keys that must be version‑controlled (e.g., via Terraform or the Okta CLI) to avoid drift.
- Claims mapping: You can map user profile attributes to claims using Okta’s expression language, but complex computed claims may require enrichment in your API.
- Rate limits: Custom servers share the org‑level rate limits; high‑volume token issuance can still trigger throttling.
Actionable closing
- Audit existing tokens: Use
jwt.ioor the Okta introspection endpoint to confirm that tokens issued to your API contain the expectediss,aud, and scopes. - Start small: If you only need a single custom scope for an internal service, create a modest Custom Authorization Server with a tight access policy (client‑credentials only, short token lifetime).
- Automate: Define the server, scopes, and policies in Terraform (
okta_authorization_serverandokta_authorization_server_policyresources) so changes are reviewable and reproducible. - Monitor: Enable Okta System Log alerts for token validation failures that mention
issoraudmismatches – these often indicate a client pointing to the wrong server.
By aligning your API’s validation logic with the issuer and audience of a Custom Authorization Server, you eliminate the class of token‑replay bugs that arise from trusting signatures alone, and you gain the ability to version scopes and enforce least‑privilege access across environments.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.