Add a new OAuth2 scope
Define a custom OAuth2 scope and check for it in your backend
OAuth2 scopes are bearer-token permissions. A token with scope read:widgets lets the app call APIs that require that scope.
Step 1: Decide on the scope name
Convention: <verb>:<resource> or <resource>:<verb>.
Examples:
read:profile, read user profile.write:account, modify account settings.admin:users, admin operations on user accounts.widgets:read,widgets:write, alternative ordering.
Pick one convention and stick with it across your APIs.
Step 2: Register clients with the scope
When creating a client in Athena or via Hydra CLI, include the scope in the allowed list:
hydra create client \
--endpoint http://localhost:3103 \
--name my-app \
--grant-type authorization_code,refresh_token \
--scope "openid profile email read:widgets write:widgets"Or update an existing client to add the scope:
hydra update client <id> --endpoint http://localhost:3103 \
--scope "openid profile email read:widgets"Step 3: Request the scope from your app
In the authorization URL:
GET /oauth2/auth?
client_id=<your-client>
&response_type=code
&scope=openid+profile+email+read:widgets # space-separated, URL-encoded as +
&redirect_uri=...The user sees the requested scope on the consent screen (if consent is prompted).
Step 4: Check for the scope in your backend
When validating the access token (introspect or JWT decode), check the scope claim:
async function requireScope(token: string, required: string) {
const info = await introspect(token);
const granted = (info.scope || "").split(" ");
if (!granted.includes(required)) {
throw new Error(`missing scope: ${required}`);
}
}
// Wire into your handler:
app.get('/api/widgets', async (req) => {
await requireScope(extractBearer(req), 'read:widgets');
return Response.json({ widgets: [...] });
});For Go/Python/Rust, see the parallel cookbook recipes.
What scope is NOT
- Not a role. Roles describe the user; scopes describe what the token (the app on behalf of the user) is permitted to do.
- Not fine-grained per-resource permissions. If you need "user X can edit widget Y but not widget Z," that's authorization logic in your application, not a scope.
A scope tells you "this token can in principle do something." Your app then decides "can this user, with this token, do this specific thing."
Reserved scopes
These are reserved by OIDC and Olympus's Hydra:
openid, required to get an ID token.profile, request profile claims (name, etc.) in the userinfo endpoint and ID token.email, request email claim.offline_access, request a refresh token.
Don't reuse these for your custom permissions.