Software Development, AI
OAuth scope design: a six-step checklist for engineers
TL;DR: Good OAuth scope design uses resource:action names, audience-restricted tokens, short access token lifetimes and refresh token rotation. Add authorization_details (RAR) when a scope is too blunt. Write consent text in plain English and retire old scopes on a set date.
Most OAuth problems start with one lazy scope. Someone ships `admin` or `full_access` to save a design meeting. Then one leaked token can do everything.
Good OAuth scope design is mostly a few small habits. Here is the pattern that holds up in production:
- Name scopes as `resource:action`.
- Lock each token to one API with an audience check.
- Keep access tokens short-lived.
- Rotate refresh tokens on every use.
- Use `authorization_details` when a scope is too blunt.
- Write consent text a real person can read.
Each habit lines up with a published RFC. So you aren't guessing.
What a scope is (and isn't)
RFC 6749 defines scope as a space-delimited list of case-sensitive strings. The meaning of each string is up to your authorisation server. There's no universal list. You write the vocabulary. Clients ask from it.
A few things catch teams out early:
- The server can grant less than the client asked for. When it does, it must say so in the token response.
- Clients need to cope with a smaller grant. Don't assume you got everything.
- Scope says what a token can do. The `aud` claim says where it can be used. They are two different jobs.
Keep those three ideas apart from day one. It saves a lot of rework.
Start with least privilege
A single `admin` scope turns every stolen token into a full account takeover. Scopes are a way to hand out limited power. They aren't a whole security plan. RFC 9700, the current OAuth security guidance, says to pair them with other controls.
Two controls are worth building in from the start:
- Audience restriction. A token issued for one API can't be replayed against another.
- Resource indicators. The server knows which API the client means before it issues anything.
Granularity is a trade-off. Split too finely and your consent screen becomes a wall of checkboxes. Keep scopes too broad and you're back to handing out admin by default. Aim for scopes that match a real user decision, not every database column.
Here's a quick test. Write the scope's description as the consent text a user would see. If you can't say it in one plain sentence, the scope is too broad or too narrow.
Naming scopes you can maintain
`resource:action` is a common pattern and a good one. `calendar:read` and `orders:write` tell a reviewer what's being granted. No lookup table needed.
A few rules keep it tidy as your API grows:
- Pick one separator and one set of actions (read, write, delete). Enforce it in code review, not just in docs.
- Version a scope when its behaviour changes. Don't quietly redefine an old string.
- Skip wildcard and "default" scopes that give more than the client asked for.
- For tenant-specific access, use the resource parameter. Don't invent a new scope per customer.
That last one matters most for multi-tenant products. Scopes describe the capability. The resource parameter describes where it applies. Your scope list stays short as you add customers.
Lock tokens to an audience
A token needs to be tied to its API as well as its permissions. RFC 9700 recommends audience-restricted access tokens. Resource servers should check the `aud` claim and reject anything that doesn't match. Skip that check and a token leaked from one service works on any other service that trusts the same issuer.
RFC 8707 adds the `resource` parameter. The client tells the server which protected resource it plans to call. The server then issues a token for that one resource, not a broad one that works everywhere.
- Send the most specific resource URI you have. Not a shared base domain.
- Check `aud` on every request. Reject a mismatch.
- Where you can, ask for one resource per token. A leak then does less damage.
When scopes aren't enough
Plain string scopes get blunt fast. Think "transfer $500 to this merchant" versus "can start payments." RFC 9396 adds `authorization_details`. It's a JSON parameter that carries structured, fine-grained requests next to scopes, or in place of them. The standard calls this Rich Authorization Requests, or RAR.
RAR earns its extra work in cases like these:
- Payment flows where the amount and recipient shape the decision.
- Picking specific accounts or files instead of a whole category.
- Any consent screen that needs to show a transaction, not a permission.
A sensible path is coarse scopes for general access. Layer `authorization_details` on top for the one transaction being approved. The server should pass the granted details to resource servers, in the token or through introspection. That way every API enforces the same thing.
As a rule of thumb, if your scope list keeps growing and every new feature needs another scope, try RAR before splitting again. That's our judgement, not a number from the RFC.
Token lifetimes, rotation and reuse
Scope design only helps if the tokens carrying those scopes are short-lived and hard to replay. For more on this, see these secure token management practices. The pattern we use and recommend:
- Give access tokens a short life. Five to fifteen minutes is a common range.
- Rotate the refresh token on every use. Tag each new one with a family ID.
- If someone reuses an old refresh token, revoke the whole family. Force a fresh login.
- Refresh once at a time on the client. Allow a short grace window on the server for honest parallel requests.
- Sign tokens with an asymmetric algorithm such as RS256 once more than one service verifies them. That way verifiers never hold the signing secret.
A tip: store refresh tokens hashed. Never keep them in plain text or in `localStorage`. Log reuse events with enough detail to investigate.
Consent screens and developer tooling
What makes sense to an engineer rarely makes sense to the person clicking "allow." Map each scope to one short, honest sentence. Group low-risk scopes so the screen doesn't bury people in near-identical lines.
- Publish a scope registry, so client developers can see what exists without reading your source.
- Expose supported scopes and descriptions through discovery metadata or clear docs.
- Plan retirement. Keep old scopes working through a transition window, log their use and set a sunset date.
- Track which scopes clients request before you remove any.
A checklist before you ship scope changes
Run through this before any scope change goes live:
- Write down your naming rules, your default scopes and what each scope grants.
- Confirm every resource server checks `aud` and rejects mismatches.
- Turn on refresh token rotation with family tracking and reuse detection. Expiry alone isn't enough.
- Test the consent text on someone outside engineering.
- Check that audit logs capture scope grants, token issuance and reuse events.
- Book a recurring review of granted scopes against what each client really uses.
Most scope incidents come from one of these steps being skipped. The spec itself is rarely the problem.
Where this fits in a real build
Scopes, resource boundaries and tenant isolation come up in almost every platform we build. Two good examples are the white-label AI platform case study, a multi-tenant SaaS, and the CARED allied health platform, which ties into healthcare and government systems.
If you're adding permissions to an API, these posts cover the next layer:
- What is role-based access control shows how roles sit on top of the scopes a token carries.
- Chatbot security for CTOs covers threats and tests for AI front ends.
- The role of APIs in AI software explains where auth fits in an AI stack.
For AI agents that call your APIs, the same least-privilege thinking applies. The AI Orchestrators team wrote about layered AI agent security and agent guardrails. AI-Led covers the practical side of AI API integration.
Need a second pair of eyes on your scope model? Our team designs and audits authorisation as part of AI program and platform work and web app development. Worth a chat? Talk to James.
Frequently asked questions
What are scopes in OAuth?
Scopes are space-delimited, case-sensitive strings that say what an access token can do. Your authorisation server decides what each one means. A client asks for a set of scopes, and the server may grant all of them or fewer.
Is OAuth risky?
OAuth is a delegation protocol, not a full security plan. How safe it is depends on how you set up scopes, token lifetimes and audience checks. RFC 9700 sets out the current guidance, including audience-restricted tokens and strict aud checks.
What is the difference between scopes and roles in OAuth?
A scope says what one token may do in one delegated session. A role usually describes a user's wider place in your app. OAuth doesn't define roles. Many apps add role logic on top of the scopes a token carries.
Is OAuth 2.0 obsolete?
No. OAuth 2.0 is still the framework most systems use. RFC 9700 updates its security advice and doesn't replace it. OAuth 2.1 work pulls that advice into one document.
How should I handle scope revocation or escalation?
Revoke the whole refresh token family for a session, not just the token in use. Then a stolen credential can't be reused. If a client needs more scope than it was given, ask the user for fresh consent. Never widen an existing token quietly.
About James Killick
10+ years building digital products · 200+ apps shipped since 2015
James is a co-founder of Devwiz and an AI product specialist. Since 2015 he has helped ship 200+ apps for founders, businesses and government, including work for NSW Government, Briometrix and Huskee. He builds AI-first platforms and writes about turning a proven program into software. He also hosts the Up in the AI podcast.
More articles by James · James's personal site · LinkedIn · AI Orchestrators
Tags: OAuth, API Security, Software Development, Authentication


