Every access token IntelliAuth issues carries two pieces of authorisation information: the audience (the API the token is for) and the scope (the permissions the token grants). Backends should verify both before serving a request.
Audience — who the token is for
Section titled “Audience — who the token is for”The audience claim (aud) names the API the token was minted for. When your backend validates a token, it checks that the audience matches what this API expects. If a token issued for api.banking.cymmetri.com lands on a request to api.health.cymmetri.com, the validation rejects it — even though the token is otherwise perfectly valid.
// JWT payload (decoded){ "iss": "https://banking-cymmetri.intelliauth.local", "sub": "usr_01HZX...", "aud": "api.banking.cymmetri.com", // ← this token is for this API only "scope": "openid profile read:transactions", "exp": 1750000000, "iat": 1749996400}The audience is set when the client requests a token. The IntelliAuth React SDK's getAccessToken({ audience: 'api.banking.cymmetri.com' }) mints a token for that audience. If your app talks to multiple APIs (your own + a third-party), you request a separate access token per audience — the SDK caches each one independently.
Scope — what the token permits
Section titled “Scope — what the token permits”The scope claim names the permissions the token grants. It's a space-separated string of scope identifiers. Standard OIDC scopes:
openid— required for OIDC flows; grants identity claims.profile— name, picture, locale, etc.email— email + email_verified.
Plus any custom scopes your tenant defines for your API:
read:transactions— read-only access to transaction data.write:settings— write access to user settings.manage:applications— register and rotate applications.
When your backend gets a request, it checks two things:
- Audience matches.
- The scope claim contains every scope the requested action needs.
function requireScope(scope: string) { return (req: Request, res: Response, next: NextFunction) => { const tokenScopes = (req.user.scope as string).split(' ') if (!tokenScopes.includes(scope)) { return res.status(403).json({ error: 'insufficient_scope', required: scope }) } next() }}
app.get('/transactions', requireScope('read:transactions'), handler)app.post('/settings', requireScope('write:settings'), handler)Why per-app scope allowlists matter
Section titled “Why per-app scope allowlists matter”Each application can declare which scopes it's allowed to request. This is a hard cap — even if a client asks for manage:applications, if its application registration doesn't include that scope in its allowlist, the platform refuses to mint a token with it. Set the allowlist when you register the application in the tenant admin console.
The right default: applications get the minimum scopes their feature set needs. Adding a scope to an application is a deliberate action by the tenant admin; the platform's audit log records who added it and when. This is how principle of least privilege gets enforced.
What end-users see — consent
Section titled “What end-users see — consent”When a user signs in to an application that's requesting scopes for the first time (or scopes that haven't been previously consented to), they see a consent screen describing what permissions the application wants. They click Allow; the scopes are added to their consent record for that application; tokens then start including those scopes.
You don't have to write the consent screen — IntelliAuth renders it from the scope metadata in your tenant config. You do have to author the scope descriptions: each scope on the tenant has a human-readable name + description shown to end-users at consent time.
Scope strings vs. enforcement points
Section titled “Scope strings vs. enforcement points”A common source of confusion: scope strings are claims in the token. Enforcing them is the backend's job. The platform mints the token with whatever scopes are validly requested; whether your /transactions endpoint actually checks for read:transactions is up to you.
The SDK helpers (requireScope above, equivalents in other SDKs) make this easy. The discipline is to actually use them everywhere — a token validly verified against the wrong scope is still authorised, which defeats the point.