Custom Authorization Servers in Okta
A plain-English guide to the token factory behind your APIs: what a custom authorization server is, how its issuer, audience, scopes, claims, and policies fit together, which flows to use for apps and services, how tokens are signed and checked, and how to design servers that stay secure, fast, and easy to change.
Signing in a person is one job. Letting an application call an API on that person’s behalf is another. The first job asks “who are you?” The second asks “what is this application allowed to do, for whom, and for how long?” When many applications and many APIs are involved, the second question needs its own careful machinery, with rules for what each application may ask for, what each API expects, and what goes inside the tokens that carry the answers.
In Okta, that machinery is the authorization server. Okta provides one built in for signing people in to Okta itself, and lets you create your own custom authorization servers for your APIs. Each custom server is like a private token factory with its own rules, its own keys, and its own address. This guide explains it all from zero. You will learn what tokens are, how the built-in and custom servers differ, what each part of a custom server does, which flows suit which applications, how to validate tokens in an API, how to add custom logic, and how to secure, monitor, and maintain the whole setup. The companies and numbers in the examples are invented for teaching.
1Why APIs Need Their Own Gatekeeper
Before learning any settings, it helps to see the problem a custom authorization server solves, and what goes wrong when it is skipped.
From passwords to permission slips
Long ago, if one program needed to use another service on your behalf, you handed it your username and password. That is dangerous: the program gets everything you can do, forever, and you cannot take it back without changing your password everywhere. Modern systems replaced this with tokens, small signed pieces of data that act as limited, temporary permission slips. A token says who the caller is, what they may do, who issued the permission, and when it expires. The service receiving the token checks the signature and the details, and either lets the request through or turns it away.
Who makes the permission slips?
Someone must decide which applications get which permission slips, what the slips say, and how long they last. That someone is the authorization server. It receives a request from an application, checks who is asking and whether the request is allowed under the rules, and issues a token if it is. Every API that trusts tokens from that server relies on the server being careful.
Think of a large conference with many rooms. Instead of giving every visitor a master key, the front desk issues a lanyard badge. The badge says the visitor’s name, which rooms they may enter, and the date it expires. Each room’s door checks the badge, not the visitor’s identity papers. The front desk is the authorization server, the badge is the token, and the doors are the APIs.
Why one server is not enough for everyone
- Different APIs have different rules. A payments API needs shorter token lifetimes and stricter scopes than a public product catalog.
- Different audiences. Each API should accept only tokens meant for it, not tokens meant for some other API.
- Different teams. One team may want to change claims or policies without risking another team’s API.
- Different data in the token. A mobile app and a back-end job need different claims.
- Blast radius. A mistake in one server’s configuration should not break every API at once.
Whenever you design API security, say the three-part sentence out loud: “This application may call this API with these permissions.” If you cannot fill in all three parts, the design is not finished.
2Tokens in Plain Words
Three kinds of tokens appear again and again. Knowing what each is for makes every later chapter easier.
The three tokens
| Token | Who reads it | What it says | Typical lifetime |
|---|---|---|---|
| ID token | The application that signed the person in | Who the person is and how they signed in | Short, about an hour |
| Access token | The API being called | What the caller may do, for whom, and until when | Short, from minutes to an hour |
| Refresh token | Only the authorization server | A key to get a fresh access token without asking the person again | Longer, from hours to weeks or months |
What is inside a token
Most tokens from Okta are JSON Web Tokens, often called JWTs, pronounced like the word “jot”. A JWT has three parts joined together: a header that says how it was signed, a body of facts called claims, and a signature. The claims include standard facts such as the issuer (who created the token), the subject (who it is about), the audience (who it is meant for), and the expiry time. They also include scopes, which list the permissions granted, and any custom claims you add.
The signature is made with a secret key that only the authorization server holds. Anyone can check the signature using the matching public key, but only the server can create a valid one. If anyone changes a claim, the signature stops matching, and the API rejects the token.
flowchart LR
H["Header: how it is signed"] --> J["Token"]
B["Body: issuer, subject, audience, scopes, expiry, custom claims"] --> J
S["Signature made with the server's private key"] --> J
J --> V["API checks signature with the public key"]
The figure shows the main idea: the token is self-contained. An API can often decide without calling the server, simply by checking the signature and reading the claims. That makes APIs fast, but also means a token stays valid until it expires, even if the situation has changed. Chapters on lifetimes and revocation return to this point.
A token is signed, not secret. Anyone who holds it can read the claims. Never put things in a token that you would not be happy for the holder, or for a log file, to see. Treat tokens like cash: whoever holds one can spend it.
A concert ticket has printed details and a hologram that is hard to fake. The staff at the door look at the hologram and the date, and do not need to phone the box office. A ticket is valid until the show ends, even if you later regret buying it.
3The Org Server and Custom Servers
Okta offers more than one kind of authorization server. Choosing the right one is the first design decision.
The built-in organization server
Every Okta organization has a built-in organization authorization server, often shortened to the org server. It exists mainly to sign people in to applications using OpenID Connect, and to protect Okta’s own management interfaces. Its configuration is fixed: you cannot add your own scopes, change which claims go in the tokens in the same way, or write access policies for your own APIs. Its access tokens are meant for Okta’s own interfaces, not for your APIs.
Custom authorization servers
A custom authorization server is one you create and configure yourself, usually to protect your own APIs. You choose its audience, define its scopes, add claims, write policies and rules, and set token lifetimes. Okta also provides a ready-made one called the default custom authorization server, which can be used as a starting point for simple cases. Using custom servers generally requires the API access management capability of your Okta plan, and limits on how many you can create depend on your license, so check your own organization.
| Question | Org server | Custom server |
|---|---|---|
| Main purpose | Sign people in to applications, protect Okta’s own interfaces | Protect your own APIs and issue tokens for them |
| Custom scopes | No | Yes |
| Custom claims | Limited | Yes, in ID and access tokens |
| Access policies | Not for your APIs | Yes, with rules per client and user |
| Token lifetimes | Fixed by Okta | Configurable |
| Signing key rotation | Managed by Okta | Configurable per server |
| Own issuer address | One per organization | One per server |
| Good for your APIs | No | Yes |
flowchart TD
OKTA["Okta organization"] --> ORG["Org authorization server: sign-in and Okta interfaces"]
OKTA --> DEF["Default custom server: simple starting point"]
OKTA --> C1["Custom server A: payments API"]
OKTA --> C2["Custom server B: catalog API"]
OKTA --> C3["Custom server C: partner API"]
The picture shows one organization hosting several servers side by side. They share users and applications from the same Okta organization, but each has separate scopes, claims, policies, and keys. A token from server A is useless at server B’s API, which is exactly the isolation you want.
Okta changes screen names, limits, and licensing over time. This guide describes the lasting ideas and uses cautious wording about exact screens. Always confirm details in the current Okta documentation for your organization before building.
A company’s main reception issues visitor badges for the building. But the research lab inside runs its own badge desk with stricter rules and different colors. Both desks belong to the same company, yet each decides for its own area.
4Anatomy of a Custom Authorization Server
A custom server is built from a handful of parts. Seeing them all at once makes the details in later chapters fall into place.
The six parts
Issuer
The web address that identifies the server and appears in every token it issues. APIs check it to know who created the token.
Audience
The name of the API the tokens are meant for. APIs check that the audience matches their own.
Scopes
Named permissions an application may request, such as reading orders or writing orders.
Claims
The facts placed inside tokens, such as the person’s email, department, or roles.
Access policies and rules
The conditions deciding which applications may get which tokens, for which people, and for how long.
Signing keys
The key pairs used to sign tokens and let APIs verify them. Keys can be rotated.
How a request flows through the parts
sequenceDiagram
participant APP as Client application
participant AS as Custom authorization server
participant POL as Policies and rules
participant API as Protected API
APP->>AS: Request token for scopes
AS->>POL: Find matching policy and rule
POL-->>AS: Allowed scopes and lifetimes
AS->>AS: Build claims and sign token with key
AS-->>APP: Access token with issuer, audience, scopes
APP->>API: Call API with the access token
API->>API: Check signature, issuer, audience, scopes, expiry
API-->>APP: Response if everything checks out
This sequence is the heartbeat of the whole system. The application asks, the policy decides, the server builds and signs, and the API verifies. Each later chapter zooms in on one of these steps. If a call is refused, the failure is almost always at one of these four stages: the policy said no, the claims were wrong, the signature did not verify, or the API checked something the token did not satisfy.
When you troubleshoot, walk the sequence in order and stop at the first stage that fails. Guessing at random settings wastes hours. The sequence tells you where to look.
A passport office has an official address, a country it issues for, a list of permitted travel types, a form for personal facts, a rulebook for who qualifies, and a stamp with a security feature. A custom server has the same six parts.
5Issuer and Audience
Two claims decide whether a token is trusted by the right API. Getting them wrong is a classic source of both bugs and security holes.
The issuer: who made this token?
The issuer is a web address that uniquely identifies the authorization server. Every token it creates carries that address. When an API receives a token, it compares the issuer inside the token with the issuer it trusts. If they differ, the token came from somewhere else, and the API rejects it. For custom servers in Okta, the issuer includes your Okta domain and a server identifier. If you use a custom domain for your Okta organization, the issuer can use that domain instead, which makes the address look like your own.
The audience: who is this token for?
The audience is a name, often written like a web address, that stands for the API. Each custom server has one configured audience value. When an API receives a token, it checks that the audience equals its own name. This prevents a token meant for the catalog API from being accepted by the payments API. Even if both trust the same issuer, each demands a token addressed to it.
| Check | What the API compares | What it prevents |
|---|---|---|
| Issuer | Token’s issuer against the trusted server address | Tokens made by an untrusted or fake server |
| Audience | Token’s audience against the API’s own name | Tokens meant for another API being replayed here |
| Expiry | Current time against the token’s expiry | Old tokens being used forever |
| Signature | Token’s signature against the server’s public key | Forged or altered tokens |
| Scope | Required permission against the scopes in the token | A caller doing more than it was allowed |
flowchart TD
T["Incoming token"] --> I{"Issuer is my trusted server?"}
I -- "No" --> X1["Reject"]
I -- "Yes" --> A{"Audience is my API name?"}
A -- "No" --> X2["Reject"]
A -- "Yes" --> E{"Not expired, signature valid?"}
E -- "No" --> X3["Reject"]
E -- "Yes" --> S{"Has the needed scope?"}
S -- "No" --> X4["Reject with insufficient permission"]
S -- "Yes" --> OK["Allow the request"]
The chain in the picture is a minimum set. Skipping the audience check is surprisingly common and dangerous: a token for a low-risk API can then be used against a high-risk one if both trust the same issuer. Always check both.
Never accept tokens whose issuer or audience you have not explicitly configured. Libraries that let you turn these checks off exist for testing, and have been left off in production more than once. Make the checks mandatory in your API template.
A theater checks that a ticket was printed by its own box office and is for tonight’s show in this theater. A genuine ticket for a different theater is still not valid here.
6Scopes: Naming What Can Be Done
Scopes turn vague trust into specific permissions. Well-designed scopes make both security and development easier.
What a scope is
A scope is a short name for a permission, such as “orders:read” or “orders:write”. An application asks for scopes when it requests a token. The server decides which of the requested scopes to grant, based on policy. The granted scopes appear in the access token, and the API checks that the token contains the scope needed for the operation. If the token has “orders:read” but the operation needs “orders:write”, the API refuses.
Designing good scopes
- Name by capability. A scope should describe a thing someone can do, such as reading invoices, not a particular application or team.
- Keep them stable. Applications and APIs depend on the names, so renaming is painful.
- Make them small enough. Splitting read from write lets you give the lowest-risk permission to most callers.
- Avoid giant catch-alls. A single “admin” scope that does everything undermines least privilege.
- Document each one. Provide a display name and description. These are shown to people when consent is requested.
Scope options in a custom server
| Option | Meaning | Guidance |
|---|---|---|
| Name | The identifier used in requests and tokens | Short, lowercase, consistent style |
| Display name and description | Human-friendly text for consent screens | Write for non-technical people |
| Consent requirement | Whether the person must approve this scope | Require it for personal data, skip for internal trusted apps |
| Default scope | Granted when an application asks for no specific scope | Use sparingly, safest is none |
| Metadata publishing | Whether the scope appears in the server’s public discovery document | Publish only what clients truly need to know |
flowchart LR
REQ["Application asks for: orders:read orders:write refunds:write"] --> POL["Policy rule allows: orders:read orders:write"]
POL --> GR["Token issued with: orders:read orders:write"]
POL --> DEN["refunds:write not granted"]
The figure shows scope granting as an intersection. The application can ask for more than it should have, but the policy trims the request. A scope in the token is therefore evidence of both a request and an approval.
Write down your scopes in a table with the API operations each one allows, before you create them. This table becomes both the design document and the test plan, since each row can be tested with and without the scope.
A hotel key card can be coded to open the room, the gym, and the pool, but not the staff office. The guest may ask for any door, but the front desk codes only the permitted ones.
7Claims: Putting Facts in Tokens
Claims carry facts to the API so it can make decisions without looking everything up. They are powerful, and easy to overuse.
Standard and custom claims
Some claims are standard and always present, such as issuer, audience, subject, and expiry. Others you add yourself. Custom claims might carry the person’s department, a list of roles, a customer identifier, or a tenant name. The authorization server computes each one when the token is issued, using the person’s profile, group memberships, or an expression. A claim can be configured to appear in ID tokens, access tokens, or both, and can be limited to appear only when certain scopes are requested.
Sources of claim values
Profile attributes
Values copied from the person’s Okta profile, such as department or employee number.
Group membership
A list of the person’s groups, filtered by name or pattern to include only the relevant ones.
Expressions
Small formulas that combine or transform attributes, for example building a role value or a tenant code.
Token hook
Your own web service adds or changes claims at issue time when expressions are not enough.
Claim design rules
- Include only what the API needs. Every claim makes the token bigger and exposes more data.
- Prefer identifiers to personal data. A customer identifier is safer than a full name and address.
- Keep claims stable. Changing a claim’s name or meaning breaks APIs that rely on it.
- Tie sensitive claims to scopes. Include them only when the application asks for the scope that needs them.
- Mind the size. Huge group lists make tokens too large for web headers.
flowchart LR
PROF["User profile: department, employee number"] --> BLD["Claim builder"]
GRP["Group memberships filtered by prefix"] --> BLD
EXPR["Expressions for derived values"] --> BLD
HOOK["Token hook for custom logic"] --> BLD
BLD --> TOK["Claims placed in the access token"]
The diagram shows that claims are computed at the moment of issue, not stored. That is both a strength and a trap. A change to a person’s profile appears in their next token, but tokens already issued keep the old values until they expire.
If a claim feeds an authorization decision, such as a role, it must come from an attribute that people cannot edit themselves. A role derived from a self-editable field lets people promote themselves.
A name badge can carry your name, company, and an access color. Every extra line makes the badge crowded and shows more about you to everyone who glances at it. Good badges say only what the door guards need.
8Access Policies and Rules
Policies are the brain of the server. They decide who can get what, and under which conditions.
Policy, then rule
A custom server’s rules are organized in two levels. An access policy says which client applications it applies to. Inside a policy are one or more rules, each setting conditions and outcomes. Okta evaluates policies in priority order and picks the first that matches the requesting client. Within the policy, it evaluates rules in order and uses the first rule whose conditions match. If no policy or rule matches, the request is refused. This default-deny behavior is a useful safety feature.
What a rule decides
| Condition or outcome | Meaning |
|---|---|
| Grant type | Which flow the application uses, for example authorization code or client credentials |
| User or group | Which people the rule applies to |
| Scopes | Which scopes the rule permits, all or a named list |
| Access token lifetime | How long the access token stays valid |
| Refresh token lifetime | How long a refresh token may be used |
| Refresh token window | How long it may sit unused before expiring |
| Rule priority | Which rule wins when several could match |
flowchart TD
REQ["Token request from a client"] --> P1{"Policy 1 applies to this client?"}
P1 -- "Yes" --> R1{"Rule matches grant type, user, scopes?"}
P1 -- "No" --> P2{"Policy 2 applies to this client?"}
R1 -- "Yes" --> OK["Issue token with the rule's lifetimes"]
R1 -- "No" --> NXT["Try next rule in the policy"]
P2 -- "No" --> DENY["No match: request refused"]
P2 -- "Yes" --> R2["Evaluate its rules"]
The picture shows why order matters. A broad early rule can shadow a narrow later one. Put specific rules first and general rules last, and keep a short note on why each rule exists.
A policy per kind of client
A good habit is to create one policy for each kind of client: one for public single-page applications, one for trusted server applications, one for partner integrations. Inside each, create rules for the different people or situations. This keeps the structure easy to read, and lets you give each kind of client its own lifetimes and scope limits.
Use the preview or simulation tool in the admin console to test what token a particular client and user would receive. It shows which rule matched and what the token would contain, without any real application involved.
A hotel front desk follows a rulebook: members get late checkout, groups get a special rate, everyone else gets the standard rate. The clerk reads down the list and applies the first rule that fits, which is why the order of the rules matters.
9Token Lifetimes and Refresh Tokens
How long a token lives is a trade-off between safety and convenience. The settings are simple, and the consequences are large.
Short access tokens, longer refresh tokens
An access token is like a day pass: short-lived, so that a stolen one is useful only briefly. But asking the person to sign in again every few minutes would be unbearable. The answer is the refresh token, a longer-lived key held by the application that can be exchanged at the server for a new access token without bothering the person. Together they give both safety and smoothness: the access token expires quickly, and the refresh token quietly renews it.
The lifetime settings
| Setting | What it controls | Shorter means | Longer means |
|---|---|---|---|
| Access token lifetime | How long each access token is valid | Less time for a stolen token to be misused, more refresh calls | Fewer refreshes, longer exposure after theft |
| Refresh token lifetime | Maximum total time a refresh token may be used | Users sign in again more often | Smoother experience, longer exposure after theft |
| Unused window | How long a refresh token may sit unused | Idle sessions expire sooner | Idle devices stay signed in |
| Rotation | Whether each refresh gives a new refresh token | Stolen refresh tokens are detected when reused | Without it, a stolen token works until it expires |
Refresh token rotation
With rotation, every time an application uses a refresh token, the server issues a new refresh token and invalidates the old one. If an attacker steals an old refresh token and tries to use it after the real application has already moved on, the server notices that an already-used token has come back. It can then treat this as theft and shut down the whole chain. Rotation is especially valuable for apps running in browsers or on phones, where tokens are easier to steal.
sequenceDiagram
participant APP as Application
participant AS as Authorization server
participant ATK as Attacker with a stolen refresh token
APP->>AS: Use refresh token R1
AS-->>APP: New access token and new refresh token R2
ATK->>AS: Use old refresh token R1
AS->>AS: R1 was already used, treat as theft
AS-->>ATK: Request refused
AS->>AS: Invalidate the R2 chain for safety
The sequence shows why rotation is more than a nicety. Without it, the attacker’s request would succeed, and nobody would know. With it, the reuse is detected. The cost is that real applications must store the newest refresh token carefully, and handle the rare race where two requests use the same token at once.
Long-lived access tokens are a common shortcut that causes lasting trouble. A day-long access token means a stolen token works for a day, and no revocation setting can easily claw it back. Keep access tokens short and rely on refresh tokens for convenience.
A hotel gives a guest a room key card valid for a day, and the front desk reissues it on request while the guest is staying. If a lost card is used after the guest has already received a new one, the desk notices the old code and disables everything.
10Flows: Which Grant for Which Application
Different applications get tokens in different ways. Choosing the right flow matters more than almost any other decision.
The common flows
| Flow | Used by | Involves a person? | Notes |
|---|---|---|---|
| Authorization code with PKCE | Single-page apps, mobile apps, and most web apps | Yes | The modern default. A one-time code is exchanged for tokens, protected by a proof key |
| Client credentials | Back-end services and scheduled jobs | No | The application acts as itself, with its own secret or key |
| Refresh token | Apps renewing tokens | No, uses an earlier sign-in | Extends sessions without a new prompt |
| Device authorization | Televisions, consoles, and command-line tools with no keyboard | Yes, on another device | The person approves on a phone or computer |
| Token exchange | A service acting on behalf of a caller toward another API | Sometimes | Swap one token for a different one with a narrower purpose, where supported |
| Password grant | Legacy applications | Yes | Discouraged. The app handles the password directly |
| Implicit | Old browser apps | Yes | Obsolete and discouraged. Replaced by code with PKCE |
PKCE in plain words
PKCE, pronounced “pixie”, stands for Proof Key for Code Exchange. Before starting, the application invents a random secret and sends only a fingerprint of it with the request. Later, when exchanging the one-time code for tokens, it reveals the original secret. The server checks that the secret matches the fingerprint. Someone who steals the one-time code on the way cannot use it, because they do not know the secret. PKCE protects applications that cannot keep a permanent secret, such as browser and mobile apps.
sequenceDiagram
participant U as Person
participant APP as Application
participant AS as Authorization server
participant API as API
APP->>APP: Create random secret and its fingerprint
APP->>AS: Authorization request with the fingerprint
AS->>U: Sign in and consent if needed
U-->>AS: Approves
AS-->>APP: One-time code
APP->>AS: Exchange code and the original secret
AS->>AS: Check secret matches the fingerprint
AS-->>APP: Access token, ID token, refresh token
APP->>API: Call with the access token
The sequence shows two secrets doing different jobs. The person proves who they are to the server, and the application proves it is the same application that began the request. A thief who intercepts the one-time code has only half of what is needed.
In policy rules, allow only the grant types an application actually uses. A single-page app has no reason to be allowed the client credentials grant. Narrow rules reduce what an attacker can do if they find one application’s identifier.
Collecting a parcel with a pickup code is safer if you also show the receipt you were given when you ordered. A stranger who overhears the code cannot collect your parcel without the receipt.
11Signing Keys and Rotation
Every token is signed. The keys behind those signatures are the foundation of trust, and they need looking after.
Private and public keys
The server holds a private key used to sign tokens, and publishes the matching public key so anyone can verify signatures. The public keys are published in a standard document called a JSON Web Key Set, or JWKS, available at an address listed in the server’s discovery document. Each key has an identifier, written into the header of every token it signs, so the API knows which public key to use.
Why rotate keys
Keys should not live forever. If a key is ever exposed, rotating limits the window of harm. Regular rotation also keeps the team practiced, so a forced emergency rotation is not the first time anyone tries it. Okta allows custom servers to rotate keys automatically on a schedule or manually on demand. During a rotation, the old key remains published for a while so tokens signed with it can still be verified until they expire.
New key created
The server generates a new key pair and publishes the new public key alongside the old one.
APIs fetch the updated key set
APIs that cache keys must refresh when they see an unknown key identifier.
New tokens use the new key
The server begins signing with the new key.
Old tokens still verify
The old public key remains in the set until tokens signed with it have expired.
Old key retired
After the longest token lifetime passes, the old key is removed from the set.
The caching trap
APIs usually cache the public keys for speed. If an API caches keys forever and never refreshes, then after the first rotation it will see a token signed with an unknown key and reject every request. Correct APIs refresh their key set when they meet an unknown key identifier, and also on a regular schedule, with sensible limits to avoid hammering the server.
flowchart TD
TOK["Token arrives with key id K2"] --> CACHE{"K2 in cached key set?"}
CACHE -- "Yes" --> VER["Verify signature"]
CACHE -- "No" --> FETCH["Fetch the key set from the server"]
FETCH --> FOUND{"K2 now present?"}
FOUND -- "Yes" --> VER
FOUND -- "No" --> REJ["Reject the token"]
The picture shows the safe pattern. Fetch only when needed, rate-limit the fetches, and reject when the key still cannot be found. That way, a rotation is a non-event, and a forged key identifier cannot be used to flood the server with requests.
Never hard-code a copy of the public key inside an API. The first scheduled rotation will break everything at the worst possible time. Always read keys from the published set.
A government changes its passport stamp design every few years. Border officers get the new sample before the change, and old stamps stay valid until old passports expire. An officer who knows only the first design would reject every new passport.
12Validating Tokens in Your API
The API is the last line of defense. It must check every token, every time, in the right way.
Two ways to check a token
Local validation
- The API checks signature, issuer, audience, expiry, and scopes itself, using cached public keys.
- Very fast, with no call to the server per request.
- Scales easily because the server is not involved.
- A token stays valid until it expires, even if it was revoked meanwhile.
- The API must correctly implement every check.
Introspection
- The API asks the server whether the token is currently active.
- Reflects revocation immediately.
- Works for tokens that are not self-contained.
- Adds a network call to every request, which adds delay.
- Puts load on the server, and makes it a critical dependency of every API call.
A sensible approach
Most APIs validate locally for speed and keep access tokens short so that stale permissions fade quickly. For especially sensitive operations, such as moving money or changing credentials, they may add an extra introspection call, or require a fresh token. This blends speed for ordinary requests with strictness where it counts.
The complete checklist
- Signature. Valid, using the key identified in the token header, from the published key set.
- Algorithm. The expected signing algorithm only. Never accept a token that claims it needs no signature.
- Issuer. Equals the configured server.
- Audience. Equals this API.
- Expiry and start time. Current time is within the validity window, allowing a small clock difference.
- Scopes. Include what the operation needs.
- Client and subject. Where policy requires, the caller is an expected client or user type.
flowchart LR
REQ["Request with bearer token"] --> GW["API gateway or middleware"]
GW --> LV["Local validation: signature, issuer, audience, expiry"]
LV --> SC{"Scope sufficient for this operation?"}
SC -- "Yes, ordinary operation" --> BK["Back-end service"]
SC -- "Yes, sensitive operation" --> IN["Extra introspection check"]
IN --> BK
SC -- "No" --> RJ["Reject with 403"]
Putting validation at the gateway or in shared middleware ensures every API behaves the same way. It also means developers cannot forget it in a new service. Use well-tested libraries rather than writing token checks by hand, and keep them updated.
Distinguish the two common failure codes in your API. Missing or invalid tokens should return an unauthorized response, while valid tokens lacking the needed scope should return forbidden. Clear codes make debugging much faster for client developers.
A bouncer can check an ID by looking at the hologram, which is fast, or phone the issuing office to confirm it is still valid, which is slow. Most nights the quick check is enough, and the phone call is saved for the unusual guest.
13Use Case: A Single-Page App and Its API
The most common pairing is a browser application calling a back-end API on behalf of a signed-in person.
The scenario
Imagine a fictional company, Maplestone Retail, with a web application that staff use to manage orders. The application runs entirely in the browser and calls an orders API. It needs to know who the person is, and the API needs to know what the person may do. Because browser code cannot hide secrets, the application is a public client.
The design
A custom server for the orders API
Audience set to the orders API name, with its own scopes and policies.
Authorization code with PKCE
The only grant type allowed for this application in its policy.
Read and write separately
Staff in the order-viewers group may get read only, and order-managers may get write as well.
Short access, rotating refresh
Access tokens last minutes, refresh tokens rotate and expire after a working day of inactivity.
Minimal claims
A role claim derived from groups, plus the employee identifier. No personal contact details.
Registered redirect addresses
Only the application’s exact addresses are allowed, with no wildcards.
flowchart LR
SPA["Maplestone order app in browser"] -->|"Code flow with PKCE"| AS["Custom server for the orders API"]
AS -->|"Access token: orders read, orders write"| SPA
SPA -->|"Bearer token"| GW["API gateway"]
GW --> ORD["Orders API"]
AS -.->|"Public keys"| GW
Results and lessons
The Maplestone team found three lessons early. First, storing tokens in places that scripts can read, such as long-lived browser storage, raised the risk of theft through injected scripts. They chose to keep tokens in memory and rely on refresh with rotation to survive page reloads. Second, mismatched redirect addresses between test and production caused confusing errors, so they registered each environment’s addresses separately. Third, a shared “admin” scope in the first draft was split into three narrower scopes once they realized the support team needed only to read orders.
Browser applications are exposed to script injection. Combine short-lived tokens with a strong content security policy and careful handling of third-party scripts. A token stolen from a browser is as good as the person’s session for its lifetime.
A staff member carrying a day pass on a lanyard is easy to spot and easy to replace. A pass that never expired and was kept in an open drawer would be a much bigger problem.
14Use Case: Service-to-Service Calls
Not every caller is a person. Back-end services call each other all day, and they need identities and permissions too.
The scenario
Maplestone’s order service needs to call the inventory API to check stock, and a nightly job needs to call the reporting API to export sales. No person is involved. Each service is an application with its own identity, and it uses the client credentials flow: it proves who it is with its own credential, and receives a token that represents the service itself.
Good practice for service identities
- One identity per service. Do not share a single credential among many services, because you cannot tell them apart or revoke one alone.
- Strong credentials. Prefer signed assertions with a private key over shared secrets, since secrets are easier to leak.
- Narrow scopes. Give each service only the permissions it needs, such as reading stock but not changing it.
- Short lifetimes. Service tokens can be short because services can request new ones automatically.
- Separate server or policy. Keep service access rules apart from the rules for people, so changes to one cannot weaken the other.
- Secret storage. Keep credentials in a secrets manager, never in source code or configuration files.
- Rotation. Replace credentials on a schedule and when staff with access leave.
| Service | Calls | Scopes granted | Why |
|---|---|---|---|
| Order service | Inventory API | stock:read | Needs to check stock only |
| Fulfillment service | Inventory API | stock:read, stock:reserve | Needs to reserve items for shipping |
| Nightly export job | Reporting API | reports:read | Only reads sales data |
| Admin tool | Inventory API | stock:read, stock:adjust | Supports staff corrections, with extra monitoring |
sequenceDiagram
participant SVC as Order service
participant AS as Custom server for services
participant INV as Inventory API
SVC->>AS: Authenticate as itself and request stock:read
AS->>AS: Policy checks grant type and allowed scopes
AS-->>SVC: Short-lived access token
SVC->>INV: Call with token
INV->>INV: Validate locally, check audience and scope
INV-->>SVC: Stock levels
This sequence has no person in it. That is exactly why the token must carry the service’s identity and limited scopes: nobody is watching in real time to notice something odd. Logging every token request and every call is therefore the way to know what services are doing.
Cache service tokens until shortly before they expire instead of requesting a new one for every call. This avoids unnecessary load and delay, and prevents accidental rate limit problems.
Staff in a warehouse each carry their own badge, so the log shows who opened which door. A single shared badge for the whole shift would make that log meaningless.
15Use Case: One Server per API, or One for Many
When a company has many APIs, a major design question arises: how many authorization servers should there be?
The three layouts
| Layout | Description | Strengths | Weaknesses |
|---|---|---|---|
| One server for everything | All APIs share one audience and one set of scopes and policies | Simple, easy to start | Tokens are accepted everywhere, rules tangle together, one change affects all |
| One server per API | Each API has its own audience, scopes, policies, and keys | Strong isolation, clear ownership | More servers to manage, license limits may apply |
| One server per API group | Related APIs share a server, with different scopes | Balance of isolation and effort | Needs care to keep audiences and scopes distinct |
How to choose
- Different risk levels. Separate servers for high-risk APIs such as payments and low-risk ones such as public catalogs.
- Different owners. If different teams manage the APIs, separate servers let each team change its own rules safely.
- Different token needs. Distinct lifetimes, claims, or client types point to separate servers.
- Different audiences. A single audience per server means APIs cannot be told apart from their tokens alone. Use scopes to distinguish them, or split servers.
- Practical limits. The number of servers you can create depends on your license, and each needs maintenance.
flowchart TD
subgraph LA["Layout A: one server"]
A1["Single server"] --> AP1["Payments API"]
A1 --> AP2["Catalog API"]
A1 --> AP3["Partner API"]
end
subgraph LB["Layout B: one per API group"]
B1["Server: money"] --> BP1["Payments API"]
B2["Server: public"] --> BP2["Catalog API"]
B3["Server: partners"] --> BP3["Partner API"]
end
The picture contrasts the two extremes. In layout A, a token issued for the catalog could be accepted by the payments API if the audience is shared and scopes overlap. In layout B, each server has its own audience, so tokens cannot cross over.
Many teams start with the default custom server for a quick prototype, then regret it when the prototype becomes production. Moving applications later means changing issuers, audiences, and validation code across many services. Decide the layout before the first production API depends on it.
A campus might run one pass office for the whole site, or one for each building. One office is simple but its rules must suit every building. Separate offices cost more to staff but can set rules that suit the laboratory or the library.
16Use Case: Partners and Customer-Facing APIs
When outsiders call your APIs, the stakes rise. Different trust, different rules, and a need to protect both sides.
What changes with outside callers
Partners and customers are not under your control. Their applications may be less secure, their staff turn over, and their needs differ. Treat their access as untrusted by default, and design for containment: if one partner is compromised, others and your own systems should be unaffected.
Design elements
A separate server
Partner APIs get their own server and policies, apart from internal APIs.
One client per partner
Each partner has its own application registration, so access can be tracked and removed individually.
Least-privilege scopes
Scopes tailored to the partnership’s purpose, not general internal scopes.
Tenant claim
A claim identifying the partner, so the API only returns that partner’s data.
Rate limits at the gateway
Per-partner limits protect the platform from runaway or abusive callers.
Onboarding and offboarding
A clear process for granting credentials, rotating them, and removing them at contract end.
flowchart LR
P1["Partner A app"] --> AS["Partner custom server"]
P2["Partner B app"] --> AS
AS -->|"Token with tenant claim A"| GW["Gateway with per-partner rate limits"]
AS -->|"Token with tenant claim B"| GW
GW --> API["Partner API filters data by tenant claim"]
The tenant claim in the picture is what stops one partner from reading another’s data. The API must enforce it on every request. A common mistake is to trust a tenant identifier supplied in the request body or address instead of the one in the token. Always take it from the signed token.
If a client can influence its own tenant claim, for example through a field it controls, it can read other tenants’ data. Derive tenant claims from server-side records that the partner cannot edit.
A shared office building gives each tenant company its own keycards that open its own floor only. A tenant losing a card affects that company, not the whole building.
17Token Inline Hooks: Custom Logic at Issue Time
Sometimes the built-in claim tools are not enough. A token inline hook lets your own service add or change claims just before a token is signed.
What a hook is
An inline hook is a point in Okta’s process where Okta pauses, calls a web service that you own, and uses the answer. A token inline hook sits inside the token creation process. Okta sends your service a description of the token about to be issued: who the person is, which client is asking, and the claims it would contain. Your service replies with instructions to add, replace, or remove claims. Okta applies those changes and signs the token.
When a hook is worth it
- Outside data. A claim depends on information held in another system, such as a customer record or a license database.
- Complex rules. The logic is too elaborate for the expression language, such as looping through lists or applying many conditions.
- Computed permissions. A permission set is built dynamically from several sources.
- Gradual migration. Claims need to match an older system’s format during a transition.
What a hook costs
| Concern | Why it matters |
|---|---|
| Speed | The hook runs in the middle of token issue, so every millisecond it takes is added to every sign-in or token request |
| Availability | If the hook service is down or slow, token requests may fail or wait. It becomes as critical as the server itself |
| Security | The service can mint any claim. If it is compromised or tricked, it can grant powerful access |
| Maintenance | It is custom code you must host, patch, monitor, and test |
| Limits | Okta sets time and size limits on hook calls and responses, which change over time |
sequenceDiagram
participant APP as Client application
participant AS as Authorization server
participant HK as Your hook service
participant DB as Your data store
APP->>AS: Token request
AS->>AS: Evaluate policy and build standard claims
AS->>HK: Call hook with the draft token description
HK->>DB: Look up customer tier
DB-->>HK: Tier is gold
HK-->>AS: Instruction to add claim tier = gold
AS->>AS: Apply change and sign token
AS-->>APP: Token with the extra claim
The sequence shows the hook as one more link in the chain of token issue. Every link added is one more thing that can fail, so keep the hook small, fast, and well monitored. If a claim can be built with a simple expression, use the expression, not the hook.
Protect the hook endpoint: verify that each call truly comes from Okta, use encrypted connections, and limit what the service can change. Decide in advance what should happen if the hook is unavailable, and test that behavior, since a failed hook can mean either no token or a token without the claim.
A passport office may phone the tax department to confirm one detail before stamping. That call makes the stamp more informed, but if the phone line is down, the whole queue stops. Calls in the middle of a process must be reliable and quick.
18Role-Based and Group-Based Authorization
The token tells the API who the caller is and what they may do. Turning group membership into clean roles is the usual bridge.
Scopes versus roles
Scopes and roles are often confused. Scopes describe what the application is allowed to do on the person’s behalf, such as reading orders. Roles describe what the person is allowed to do in the business, such as being an order manager. A token carries both: the API checks that the application has the scope for the operation, and that the person’s role permits it. Using only scopes treats every person using the application alike. Using only roles lets any application act with the person’s full power. Combining them gives least privilege on both sides.
| Question | Answered by | Example |
|---|---|---|
| May this application call this operation? | Scope | The report viewer app has orders:read but not orders:write |
| May this person do this in the business? | Role or group claim | Only order managers may cancel orders |
| May this person see this record? | Data-level check in the API | A manager sees only their own region’s orders |
| Is this call coming from the right tenant? | Tenant claim | Partner A cannot read Partner B’s data |
Getting roles into tokens cleanly
- Group naming convention. Name groups with a prefix for the API and a suffix for the role, so a filter picks the right ones.
- Role claim. Use a groups claim with a filter, or a computed role value, rather than sending every group the person belongs to.
- Least privilege. Give people only the groups they need, and review group membership regularly.
- Token size. Keep the role list short, to stay within header limits.
- Check in the API. The API must enforce both scope and role, since a token with the right scope but the wrong role should be refused.
flowchart TD
CALL["API receives token and operation: cancel order"] --> SC{"Token has orders:write scope?"}
SC -- "No" --> R1["Reject: application not permitted"]
SC -- "Yes" --> RL{"Roles include order-manager?"}
RL -- "No" --> R2["Reject: person not permitted"]
RL -- "Yes" --> DT{"Order belongs to the person's region?"}
DT -- "No" --> R3["Reject: not their data"]
DT -- "Yes" --> OK["Allow cancellation"]
The picture shows that authorization is layered. The token carries the first two layers. The third layer needs the API to look at the actual data. Skipping any layer leaves a gap that attackers or mistakes can use.
Test authorization with a matrix: every role against every operation, with and without the scope. Eight or ten minutes drawing the matrix often reveals a missing check before a user finds it.
A hospital badge opens the ward door (the application’s permission), but only a doctor’s role allows prescribing, and even then only for their own patients. Each layer answers a different question.
19Consent, Trusted Applications, and User Experience
When an application asks for access to a person’s data, the person sometimes needs to say yes. Not every application needs this step.
What consent is
Consent is the moment when the server shows a person a list of what an application wants, such as “view your profile” or “read your orders”, and asks them to allow or deny. It is a legal and ethical safeguard for situations where a third party gets access to personal data. A custom server can be configured per scope and per application to require consent, skip it, or apply it only the first time.
When to ask and when not to
| Situation | Consent | Reason |
|---|---|---|
| Internal application built and owned by your company | Usually skipped | The company already governs the application and the data |
| Third-party or partner application accessing personal data | Required | The person should choose what to share with outsiders |
| Sensitive scope, such as financial details | Required even for internal apps in some designs | Makes the sharing explicit |
| Service-to-service calls | Not applicable | No person is present to ask |
| Repeat sign-ins | Remembered where allowed | Avoids annoying people repeatedly |
Making consent meaningful
- Plain scope descriptions. The text shown to people comes from each scope’s display name and description. Write them as normal sentences.
- Ask for the minimum. Applications requesting many scopes at once overwhelm people into clicking accept.
- Make revocation possible. Provide a place for people to review and withdraw what they have granted.
- Be consistent. The same scope should always produce the same wording.
flowchart TD
REQ["Application requests scopes"] --> TR{"Trusted internal application?"}
TR -- "Yes" --> SKIP["Skip consent and issue token"]
TR -- "No" --> PRIOR{"Person already consented?"}
PRIOR -- "Yes" --> SKIP
PRIOR -- "No" --> ASK["Show consent screen with plain descriptions"]
ASK --> DEC{"Person decides"}
DEC -- "Allow" --> ISS["Issue token and remember the choice"]
DEC -- "Deny" --> ERR["Return an error to the application"]
The flow shows consent as a gate that only some requests need to pass. Internal applications with strong governance skip it for convenience. Anything touching outside parties should face the gate, since a person’s agreement is the basis of the sharing.
Skipping consent for an application because “it is ours” only holds if that is true. If an external team can register applications in your organization, make sure those registrations cannot automatically bypass consent. Control who can mark an application as trusted.
A family doctor can read your records without asking each time, but a researcher outside the practice must ask your permission first, and you can withdraw it later. The relationship decides whether the question is needed.
20Revocation, Logout, and the Life of a Token
Issuing tokens is easy. Taking permission back is harder, because tokens are designed to work without calling home.
The revocation problem
A signed access token is valid until it expires. If someone leaves the company at noon, tokens issued at eleven keep working until they expire, unless the API checks with the server each time. This is the price of speed. The usual answer is to keep access tokens short, so the gap is minutes, and to revoke what can be revoked.
What can be revoked
| Thing | How it is cut off | Effect |
|---|---|---|
| Refresh token | Revoke it at the server, or deactivate the person or application | No new access tokens can be obtained |
| Access token | Revocation endpoint plus introspection by APIs, or simply wait for expiry | Immediate only for APIs that check the server |
| Person’s session | Clear their Okta sessions | Stops new sign-ins, does not undo existing tokens |
| Application registration | Deactivate the client | Application can no longer request tokens |
| Whole server | Deactivate the custom server | No tokens are issued for that API |
A removal sequence for a leaver
Deactivate the person
Their account is suspended or deactivated in Okta, blocking new sign-ins.
Clear sessions
Existing Okta sessions are ended.
Revoke refresh tokens
Their refresh tokens are revoked so no new access tokens are issued.
Wait for short access tokens to expire
The gap is limited to the access token lifetime.
Extra checks for sensitive APIs
APIs that handle money or personal data may introspect or check a deny list to close the gap.
flowchart LR
T0["Access token issued"] --> T1["Person deactivated"]
T1 --> GAP["Gap: token still valid until expiry"]
GAP --> T2["Token expires"]
T2 --> BLK["No refresh possible, access ends"]
T1 --> RV["Refresh tokens revoked immediately"]
RV --> BLK
The picture makes the trade-off visible. Shorter access tokens shrink the gap. Introspection removes it but adds cost to every call. Most designs accept a small gap for ordinary APIs and close it for sensitive ones.
Test your leaver process end to end with a test account: deactivate it, then try to use an existing access token, a refresh token, and a new sign-in. Measure how long each one still works. The numbers should match what your security policy promises.
When an employee leaves, their key card is disabled, but a door that only checks the printed date on a paper pass may admit them until the date passes. Short-dated passes keep that window small.
21Discovery, Custom Domains, and Addresses
Applications and APIs need to find a server’s settings automatically. Standard documents and a tidy address make that work.
The discovery document
Each custom server publishes a standard discovery document, a machine-readable page describing itself: its issuer, where to send authorization requests, where to exchange codes for tokens, where to find its public keys, which scopes and claims it supports, and which signing methods it uses. Libraries read this document and configure themselves, so developers rarely type those addresses by hand. If the document is wrong or unavailable, applications cannot start, so treat it as part of your infrastructure.
Custom domains
By default, Okta addresses use an Okta-owned domain. A custom domain lets your own domain name appear instead, such as an address on your company website, which improves trust and branding. When you use a custom domain, the issuer in tokens uses it as well. If you add or change the domain after APIs already trust the old issuer, tokens will carry a different issuer and APIs will reject them, so plan the switch carefully.
| Item | Where it comes from | Why it matters |
|---|---|---|
| Issuer | Server address, which may use a custom domain | APIs compare it against their trusted value |
| Authorization address | Discovery document | Where applications send people to sign in |
| Token address | Discovery document | Where applications exchange codes for tokens |
| Key set address | Discovery document | Where APIs find public keys |
| Supported scopes and claims | Discovery document | Tells clients what they can ask for, if published |
sequenceDiagram
participant LIB as API or app library
participant DISC as Discovery document
participant AS as Authorization server
LIB->>DISC: Fetch settings at startup
DISC-->>LIB: Issuer, token address, key set address
LIB->>AS: Fetch public keys from the key set address
AS-->>LIB: Current signing keys
LIB->>LIB: Configure validation and refresh keys on a schedule
The picture shows why consistency matters. Everything an application or API needs begins from the discovery document. Mistakes in one place, such as an issuer mismatch caused by a custom domain change, ripple outward through every library that trusts it.
An issuer address that differs by even a trailing character will fail the comparison. Copy the issuer value from the discovery document rather than typing it, and keep one authoritative copy in shared configuration.
A town’s information board lists the address of the town hall, the hours, and which stamps it uses. Visitors read the board instead of guessing. If the board is out of date, visitors end up at a locked door.
22Security Considerations
An authorization server is a high-value target. Whoever controls it controls access to every API that trusts it.
Common threats
- Stolen tokens. Tokens taken from browsers, logs, or compromised devices and replayed by attackers.
- Redirect abuse. An attacker tricks the server into sending codes or tokens to an address they control.
- Leaked client secrets. Credentials left in source code, mobile apps, or public repositories.
- Overbroad scopes and policies. A rule that allows every scope to every client.
- Missing audience checks. Tokens for one API accepted by another.
- Weak validation in APIs. Accepting unsigned tokens, ignoring expiry, or trusting any issuer.
- Administrative takeover. An attacker who can edit policies or claims can give themselves anything.
- Unprotected hooks. A weak hook service can add powerful claims.
Use PKCE and exact redirects
Register exact redirect addresses without wildcards, and require PKCE for public clients.
Short tokens, rotating refresh
Keep access tokens short and rotate refresh tokens so theft is limited and detectable.
Strong client credentials
Prefer private-key signing over shared secrets, and keep secrets in a secrets manager.
Least-privilege policies
Each policy allows only the grant types and scopes the client needs.
Mandatory API checks
Gateway or middleware enforces signature, issuer, audience, expiry, and scope for every call.
Protect administration
Few administrators, strong sign-in, change review, and alerts on edits to servers, policies, and keys.
flowchart TD
ATT["Attacker goals"] --> A1["Steal or forge a token"]
ATT --> A2["Trick a redirect"]
ATT --> A3["Change server configuration"]
A1 --> D1["Short lifetimes, rotation, signature and audience checks"]
A2 --> D2["Exact redirect addresses, PKCE, state checks"]
A3 --> D3["Restricted admin roles, change review, alerts"]
The picture reminds us that no single control suffices. Tokens are protected by lifetimes, redirects by exact matching, and configuration by strict administration. A gap in any one is where attackers go.
Do not log full tokens. Logs are widely readable, and a token in a log is a live credential for as long as it is valid. Log a short fingerprint or the token identifier instead, enough to trace a request without being usable.
A passport office protects its stamps, its staff, and its records at once. Locking only the stamp drawer while leaving the records open to anyone would not make the office secure.
23Monitoring, Logging, and Troubleshooting
When a call fails, the cause is almost always in one of a few places. Good logs and a calm routine find it fast.
What to watch
Okta’s System Log records token issuance and denial events, policy evaluations, and administrative changes. Send these to your central monitoring system. In your APIs, log the outcome of every token validation with a reason, such as expired, wrong audience, or missing scope, but never the token itself.
Common symptoms and causes
| Symptom | Likely cause | First check |
|---|---|---|
| No token issued, request denied | No policy or rule matches the client, user, grant type, or scope | System Log reason and the policy preview |
| API says invalid audience | Server audience differs from the API’s expected name | Decode a sample token and compare |
| API says invalid issuer | Wrong server address, or a custom domain change | Compare issuer in token with configuration |
| Suddenly all tokens rejected | Key rotation with an API caching old keys | Key identifier in token against cached set |
| Insufficient scope | Application did not request it, or the rule did not allow it | Requested scopes and the matched rule |
| Missing claim | Claim limited to a scope not requested, or the expression returned empty | Claim settings and the user’s attributes |
| Token too large errors | Too many group values in claims | Group filter and claim contents |
| Intermittent slow sign-ins | Slow hook service | Hook response times |
| Expired too soon or too late | Wrong rule matched, giving unexpected lifetimes | Matched rule and its settings |
flowchart TD
FAIL["API call fails"] --> Q1{"Did the app get a token?"}
Q1 -- "No" --> L1["Check System Log and policy match"]
Q1 -- "Yes" --> Q2{"Does the token decode with expected issuer and audience?"}
Q2 -- "No" --> L2["Fix server configuration or API settings"]
Q2 -- "Yes" --> Q3{"Signature and expiry valid?"}
Q3 -- "No" --> L3["Check keys, clock, and caching"]
Q3 -- "Yes" --> L4["Check scopes, roles, and API authorization logic"]
Use the token preview feature in the admin console for first-line debugging. It shows exactly what a token would contain for a given client, user, and scope, which settles most arguments between the API team and the identity team in a minute.
When a parcel does not arrive, the courier traces it from the depot to each stop. You do not repack and resend before you know where it stopped. Tracing a token works the same way.
24Performance, Scale, and Limits
Token systems sit in the path of everything. Small inefficiencies multiply into large delays and outages.
Where load comes from
- Token requests. Every sign-in, refresh, and service token fetch. Short lifetimes mean more requests.
- Key set fetches. APIs refreshing keys too often, especially during outages or badly written retry loops.
- Introspection calls. One per API request if used everywhere.
- Hook calls. One per token issue, run in the middle of the request.
- Bursts. Morning sign-in peaks, batch jobs starting at the same minute, and restarts of many services at once.
Practical measures
| Measure | Benefit |
|---|---|
| Cache service tokens until near expiry | Cuts token requests from services by a large factor |
| Validate locally instead of introspecting everywhere | Removes a call per request and a dependency on the server |
| Cache and refresh public keys sensibly | Avoids repeated fetches and survives rotations |
| Add random delay to batch job starts | Spreads bursts so rate limits are not triggered |
| Retry with backoff | Avoids making a rate limit problem worse |
| Keep hooks fast and local | Prevents slow token issue |
| Keep tokens small | Faster requests and fewer header size errors |
Rate limits and product limits
Okta applies rate limits to its token and related interfaces to protect the service, and the limits differ by endpoint and plan. If a limit is exceeded, Okta returns a rate-limit response, and well-behaved clients must wait and retry rather than hammering. There are also limits on the number of custom servers, scopes, claims, and policies, which depend on your license and change over time. Check your own organization’s limits before a large design.
flowchart LR
SVC["Many service instances"] --> CACHE["Token cache per instance"]
CACHE --> HIT{"Cached token still valid?"}
HIT -- "Yes" --> USE["Use cached token"]
HIT -- "No" --> REQ["Request a new token with backoff"]
REQ --> AS["Authorization server"]
AS --> CACHE
The picture shows the simplest scaling trick. A service that calls an API a thousand times a minute needs only one token request per lifetime, not a thousand. That single habit often reduces load on the authorization server by orders of magnitude.
Failing services that retry immediately in a tight loop can turn a short outage into a self-inflicted rate-limit lockout. Use exponential backoff with random jitter, and cap the number of retries.
A delivery driver who makes one stop for fuel and one for supplies each morning does far better than a driver who returns to the depot after every parcel. Reuse saves trips for everyone.
25Environments and Change Management
A server’s configuration is code in disguise. A careless change can break every API at once, so treat it with the same care as software.
Separate environments
Keep separate Okta organizations, or at least clearly separate servers, for development, testing, and production. Each environment has its own issuer, audience, keys, clients, and users, so that mistakes and experiments cannot leak into production. Name audiences and scopes consistently across environments so applications need only change the base address, not the logic.
Configuration as a managed asset
- Write it down. Keep a short document for each server describing the audience, scopes, claims, policies, and owners.
- Use automation. Okta’s management interfaces and infrastructure tools let you describe servers, scopes, claims, and policies in files kept under version control, then apply them the same way to every environment.
- Review changes. Another person reads every change to policies, scopes, and claims before it is applied.
- Avoid console drift. Manual edits in the console that are not reflected in the files make environments diverge.
- Plan breaking changes. Renaming scopes, changing audiences, or switching issuers needs a migration plan with overlap.
Propose
Describe the change, its reason, and the effect on APIs and applications.
Apply in development
Try the change and test with real client flows and the token preview.
Test across APIs
Run authorization tests for each role and scope combination.
Review and approve
A second person reviews, and the owner of each affected API signs off.
Release with a rollback plan
Apply to production at a quiet time, record the previous settings, and watch the logs.
Handling breaking changes
Some changes break existing callers, for example removing a scope or changing the audience. The safe path is to add the new version alongside the old, move callers across, and only then remove the old. If the audience must change, consider creating a new server and migrating one API at a time, because tokens already issued will carry the old audience until they expire.
flowchart LR
DEV["Development organization"] --> TST["Test organization"]
TST --> PRD["Production organization"]
GIT["Version-controlled configuration"] --> DEV
GIT --> TST
GIT --> PRD
PRD --> MON["Monitoring and change alerts"]
The picture shows a single source of truth applied outward. Developers can see exactly how production differs from test, and a rollback is simply reapplying the previous version of the files.
Put alerts on changes to production servers, policies, scopes, claims, and keys. A change nobody expected is either an emergency fix that needs recording or an intrusion that needs stopping. Either way, someone should look the same day.
A theater rehearses in an empty hall before opening night. Lighting changes are tried out on a stage with no audience. Nobody changes the lights for the first time with a full house watching.
26Failure Scenarios and Recovery
A handful of failures cause most authorization outages. Know them and practice the fixes.
| Failure | Symptom | Recovery | Prevention |
|---|---|---|---|
| Key rotation breaks APIs | Sudden wave of invalid signature errors | Force key set refresh, restart the API if needed | APIs refresh on unknown key, test rotation in advance |
| Policy change denies a client | Token requests refused for one application | Restore previous rule, check the policy preview | Review and test rules, keep a record of the previous version |
| Hook service down | Token requests slow or failing | Restore the hook or disable it temporarily if safe | High availability, timeouts, tested fallback behavior |
| Issuer changed | All APIs reject tokens | Revert domain change or update API configuration | Plan domain moves with overlap |
| Client secret leaked | Unexpected token requests from unknown places | Rotate credential, review logs for abuse | Secrets manager, private-key authentication, scanning for leaks |
| Overlong tokens | Header size errors after users join groups | Tighten group filter, send roles instead of groups | Test with heavy-membership users |
| Clock drift on APIs | Tokens seem expired or not yet valid | Fix time synchronization | Time sync monitoring and small tolerance |
| Rate limits reached | Rate-limit responses from the server | Backoff, cache tokens, spread jobs | Caching and jittered schedules |
| Server deactivated by mistake | No tokens issued for an API | Reactivate | Restrict who can deactivate and alert on it |
A recovery routine
Check recent changes
The System Log shows who changed what and when. Most outages follow a change.
Narrow the stage
Use the token journey: request, policy, issue, validation, authorization.
Revert if in doubt
If a recent change is the suspect, undo it first and investigate afterward.
Communicate
Tell application and API teams what is happening and what to expect.
Learn
Write down the cause and add a check, test, or alert that would have caught it.
flowchart TD
OUT["Outage reported"] --> C1{"Recent configuration change?"}
C1 -- "Yes" --> RV["Revert it and retest"]
C1 -- "No" --> C2{"Many APIs failing at once?"}
C2 -- "Yes" --> KY["Check keys, issuer, and server status"]
C2 -- "No" --> C3{"Only one client or API affected?"}
C3 -- "Yes" --> PL["Check that client's policy, scopes, and credentials"]
C3 -- "No" --> HK["Check hooks, rate limits, and dependencies"]
Keep at least one administrator account that does not depend on any of the custom servers you manage, and test that it works. If you lock yourself out of the tool that fixes the problem, a small incident becomes a long one.
When the power goes out in a building, the caretaker first asks what changed, then checks the main switch, then the floor, then the room. Starting with the wiring in one room wastes time if the main switch has tripped.
27Design Patterns That Work
These patterns have proved themselves in many organizations. Adapt them to your size and risk.
One server per API group
Group APIs by risk and ownership, and give each group its own server, audience, scopes, and keys, so a mistake or a breach in one does not spread to the others.
Policy per kind of client
Create one policy for browser apps, one for trusted server apps, and one for partner integrations. Each allows only the grants, scopes, and lifetimes that suit it.
Short access, rotating refresh
Keep access tokens short, use rotating refresh tokens for convenience, and rely on refresh revocation for leavers.
Gateway validation
Validate tokens in one shared place, such as a gateway or common middleware, so every API gets the same strict checks.
Scopes for applications, roles for people
Use scopes to limit what an application can do, roles to limit what a person can do, and data-level checks inside the API for the rest.
Configuration as code
Describe servers, scopes, claims, and policies in version-controlled files, apply them to every environment, and alert on out-of-band changes.
flowchart LR
CL1["Browser app"] --> POL1["Browser policy: code with PKCE, short lifetimes"]
CL2["Service"] --> POL2["Service policy: client credentials, narrow scopes"]
CL3["Partner"] --> POL3["Partner policy: tenant claim, rate limits"]
POL1 --> AS["Custom server per API group"]
POL2 --> AS
POL3 --> AS
AS --> GW["Gateway validates every token"]
GW --> API["APIs enforce scope, role, and data checks"]
A well-run venue has separate entrances for staff, visitors, and delivery drivers, each with its own checks, and one security desk at the inner doors that looks at every badge the same way.
28Anti-Patterns to Avoid
These habits seem convenient, and cause long-lasting trouble. Recognize them early.
Anti-patterns
- Using the default server for everything. One audience, shared scopes, and no isolation.
- Long-lived access tokens. Days or weeks of exposure after theft.
- Skipping the audience check. Tokens meant for one API accepted by another.
- Catch-all scopes. A single admin scope that does everything.
- Wildcard redirect addresses. Lets attackers steal codes and tokens.
- Shared client credentials. Many services use the same identity, so nothing can be traced or revoked individually.
- Everything in the token. Large group lists and personal data in every token.
- Hard-coded signing keys. The next rotation causes an outage.
- Logging full tokens. Logs become a store of live credentials.
- Console-only changes. No history, no review, no repeatability.
Better habits
- Servers by risk and owner. Isolation by design.
- Short tokens, rotating refresh. Small windows, detected theft.
- Mandatory audience and issuer checks. In shared middleware.
- Small, named scopes. Read and write separated.
- Exact redirect addresses and PKCE. Codes are useless if stolen.
- One identity per service. Traceable and individually revocable.
- Lean claims. Roles, not every group, and identifiers not personal data.
- Keys read from the published set. Rotations are non-events.
- Log fingerprints. Trace without exposing.
- Configuration as code. Reviewed, repeatable, and alertable.
| Anti-pattern | What it causes | Quick fix |
|---|---|---|
| Default server for everything | Tangled rules, wide blast radius | Plan separate servers by risk and ownership |
| Long access tokens | Stolen tokens work for days | Shorten and add rotating refresh tokens |
| No audience check | Token replay across APIs | Enforce in gateway or shared library |
| Wildcard redirects | Stolen authorization codes | Register exact addresses only |
| Shared credentials | No traceability | One client per service |
| Hard-coded keys | Outage on rotation | Read the key set and refresh on unknown key |
The most dangerous anti-pattern is the one that works perfectly in testing: an API that accepts any validly signed token from your server, with no audience or scope check. It passes every happy-path test and fails the first time someone presents a token meant for something else.
A building where one badge opens every door is convenient until the badge is lost. The tidy-looking shortcut moves the cost from today to the worst day.
29Trade-Offs, Best Practices, and Common Mistakes
Every choice gives something up. Naming the trade-offs makes decisions deliberate, and a short checklist keeps good habits alive.
The main trade-offs
| Choice | Gain | Cost | Lean toward it when |
|---|---|---|---|
| Many custom servers | Isolation, clear ownership | More to manage, license limits | APIs differ in risk or owner |
| One shared server | Simplicity | Tangled rules, wide blast radius | Small environment with uniform needs |
| Short access tokens | Small theft window | More refresh traffic | Data is sensitive |
| Longer access tokens | Less load | Longer exposure | Low-risk data and constrained devices |
| Local validation | Fast, scalable | Revocation lag | Most APIs, with short lifetimes |
| Introspection | Immediate revocation | Extra call and dependency | High-value operations |
| Rich claims | Fewer lookups in the API | Bigger tokens, more data exposed, staleness | Few, stable, non-sensitive facts |
| Token hook | Custom logic and outside data | Latency, availability, security burden | No simpler tool can do the job |
| Consent screens | Clear user choice | Extra clicks | Third parties and sensitive data |
Best practices
- Plan the server layout before the first production API depends on it.
- Give each server one clear audience and a small, documented set of scopes.
- Use authorization code with PKCE for apps with people, and client credentials with strong credentials for services.
- Allow only the grants each client needs, with exact redirect addresses.
- Keep access tokens short, rotate refresh tokens, and revoke on departure.
- Validate signature, algorithm, issuer, audience, expiry, and scope on every call.
- Keep claims small, and derive authorization claims only from data users cannot edit.
- Rotate keys on a schedule and make sure APIs handle it.
- Treat configuration as code, with review, environments, and alerts.
- Monitor issuance, denials, changes, and latency.
Common mistakes
- Starting on the default server and never moving off it.
- Forgetting that tokens already issued keep old claims until they expire.
- Using one scope for many different operations.
- Accepting any token from the right issuer without checking the audience.
- Switching the issuer by adding a custom domain without informing the APIs.
- Testing only with users who belong to few groups.
- Leaving test clients and old policies active in production.
quadrantChart
title Choosing token strictness
x-axis Low impact if misused --> High impact if misused
y-axis Few callers --> Many callers
quadrant-1 Short tokens plus gateway checks
quadrant-2 Standard lifetimes with local validation
quadrant-3 Relaxed lifetimes, basic checks
quadrant-4 Short tokens plus introspection
Public catalog API: [0.1, 0.85]
Internal reporting API: [0.4, 0.4]
Customer profile API: [0.7, 0.7]
Payments API: [0.9, 0.5]
Admin API: [0.95, 0.1]
The picture is a way of thinking, not a measurement. Low-impact APIs can use relaxed lifetimes and basic checks. High-impact APIs justify short tokens, strict scopes, and sometimes an extra live check. Match effort to consequence.
A shop locks the stockroom more carefully than the shop window display. The effort follows the value of what is behind the door.
30Worked Example: Harborline Freight
To pull everything together, here is an invented company and a complete authorization design. The company, numbers, and names are made up for teaching.
The company
Harborline Freight is a fictional logistics company with a customer web portal, a driver mobile app, internal dispatch tools, a set of back-end services, and an API offered to forty partner companies. Until recently, everything used the default custom server with one audience and a few broad scopes. A security review found that a token from the driver app could be accepted by the billing API, because both trusted the same issuer and audience. Leadership asked for a redesign within a quarter.
The design
| Server | Audience | Clients | Key settings |
|---|---|---|---|
| Operations | Dispatch and driver APIs | Driver mobile app, dispatch web tool | Code with PKCE, rotating refresh, access tokens of 10 minutes, scopes for shipments and routes |
| Finance | Billing and payments APIs | Customer portal billing section, internal finance tools, finance services | Access tokens of 5 minutes, consent for personal data scopes, introspection for payment actions, strict roles |
| Partners | Partner API | One client per partner | Client credentials with private-key signing, tenant claim, per-partner rate limits at the gateway, reviewed yearly |
flowchart LR
DRV["Driver app"] --> OPS["Operations server"]
DSP["Dispatch tool"] --> OPS
PRT["Customer portal"] --> FIN["Finance server"]
FSV["Finance services"] --> FIN
PAR["40 partner clients"] --> PSV["Partners server"]
OPS --> GW["Gateway validates issuer, audience, scope"]
FIN --> GW
PSV --> GW
GW --> OAPI["Dispatch and driver APIs"]
GW --> FAPI["Billing and payments APIs"]
GW --> PAPI["Partner API with tenant filtering"]
How the first six months went
The team began by writing a table of scopes and the API operations each allowed, which exposed several operations with no clear owner. They created the three servers through configuration files kept under version control and applied them first to a test organization. The gateway was updated to require issuer and audience matches for each route, and a scheduled test confirmed that a driver app token was refused by the billing API.
The first rotation of signing keys exposed one older internal service that had cached keys indefinitely. It failed for ten minutes before the team restarted it, and then fixed the library settings. The partner migration took longest: each partner needed new credentials and a test window. The team moved partners in small batches, with the old and new servers running side by side for a month. Partner credentials moved to private-key signing, which removed the shared secrets that had previously been emailed.
A security exercise later showed that an attacker with a stolen driver app token could no longer call the finance APIs, and that a stolen refresh token was detected on reuse. Token request volume fell after service tokens were cached, and the median sign-in time improved because the unnecessary hook that had enriched every token was replaced with a simple group-based role claim.
Result
Harborline now has isolated servers for operations, finance, and partners, short-lived tokens, strict validation at the gateway, and configuration kept under change control. The most valuable work was not clever technology. It was deciding who owns each API, what each scope means, and making sure every token is checked for who it is meant for.
A port moves from one gate with one pass for everything to separate gates for crews, finance staff, and visiting contractors, each pass opening only the right areas and expiring quickly. The cargo moves just as fast, and nobody wanders into the wrong warehouse.
31Frequently Asked Questions
Short answers to questions that come up often. Names, limits, and licensing change over time, so confirm exact details in the current Okta documentation.
What is a custom authorization server?
A token-issuing service you create and configure in Okta to protect your own APIs, with its own audience, scopes, claims, policies, and signing keys.
How is it different from the org authorization server?
The org server is built in, mainly for signing people in and protecting Okta’s own interfaces, and its configuration is fixed. A custom server is configurable and meant for your APIs.
Should I use the default custom server?
It is fine for a quick prototype. For production APIs with different risk levels or owners, plan separate servers before many applications depend on it.
What is the difference between audience and issuer?
The issuer says who created the token. The audience says which API it is meant for. APIs must check both.
Which flow should a single-page app use?
Authorization code with PKCE. The older implicit flow is obsolete and should not be used for new applications.
Which flow should a back-end service use?
Client credentials, with strong credentials such as private-key signing, and narrow scopes.
How long should access tokens last?
As short as practical, often minutes to an hour, shorter for sensitive APIs. Use refresh tokens for convenience.
What does refresh token rotation do?
It issues a new refresh token at each use and invalidates the old one, so reuse of a stolen token is detected.
How should an API validate tokens?
Check signature, algorithm, issuer, audience, expiry, and scope, usually locally with cached public keys, and add introspection for sensitive operations.
Why did all my tokens suddenly fail?
Common causes are a signing key rotation with APIs caching old keys, a changed issuer after adding a custom domain, or a deactivated server. Check recent changes first.
Can I revoke an access token immediately?
Only if APIs check with the server. Otherwise it stays valid until expiry. Short lifetimes and refresh token revocation limit the gap.
What are scopes versus roles?
Scopes limit what an application may do on someone’s behalf. Roles limit what the person may do. APIs should check both.
When do I need a token inline hook?
When claims depend on outside data or logic too complex for expressions. Keep it fast, secure, and highly available, because it runs during token issue.
Why is my token too large?
Usually too many group values in claims. Filter to the groups the API needs, or send a short role value.
Where should I start?
Pick one API, write down its audience and scopes, create a custom server with one policy per client type, validate strictly in a gateway, test in a non-production organization, and expand from there.
32Summary and Key Takeaways
A custom authorization server is a private token factory for your APIs. Used well, it gives each API strong isolation, clear permissions, and tokens that are safe to trust.
Key Takeaways
- Use custom servers for your own APIs. The org server serves sign-in and Okta’s own interfaces. Custom servers give you scopes, claims, policies, and keys.
- Isolate by risk and owner. Separate servers and audiences stop tokens crossing between APIs and limit the damage of mistakes.
- Design scopes and claims carefully. Small, named scopes, lean claims, and authorization data from sources users cannot edit.
- Pick the right flow. Authorization code with PKCE for people, client credentials for services, exact redirect addresses, and strong credentials.
- Keep tokens short and renewable. Short access tokens, rotating refresh tokens, and a revocation plan for leavers.
- Validate everything in the API. Signature, issuer, audience, expiry, and scope on every call, with keys read from the published set.
- Manage it like software. Separate environments, configuration as code, change review, monitoring, and a practiced recovery routine.
Okta changes feature names, screens, limits, and licensing over time, and custom authorization servers depend on your plan. Use this post to understand the ideas, and confirm exact settings in your own admin console and the current Okta documentation before building.