TL;DR
OAuth 2.0 scopes are permission strings that define what an access token can do. Use format resource:action like pets:read or orders:write. Request scopes during authorization, validate them on API endpoints. Modern PetstoreAPI implements scopes for read/write access to pets, orders, and user data.
Introduction
A third-party app wants to read your pet store’s inventory. Should it have full access to create orders, delete pets, and manage users? No. It should only read inventory.
OAuth 2.0 scopes solve this. Scopes define what permissions an access token has. The app requests inventory:read scope. Your API validates the token has this scope before returning data.
Modern PetstoreAPI implements granular scopes for all resources: pets, orders, inventory, and users.
If you’re testing OAuth APIs, Apidog helps you test scope validation and authorization flows.
What Are OAuth 2.0 Scopes?
Scopes are permission strings included in OAuth access tokens.
Scope Format
pets:read - Read pet data
pets:write - Create/update pets
orders:read - Read orders
orders:write - Create orders
admin:all - Full admin access
Scope in OAuth Flow
1. Authorization Request:
GET /oauth/authorize?
client_id=app123&
scope=pets:read orders:read&
redirect_uri=https://app.com/callback
2. User Consent:
App "PetFinder" wants to:
- Read your pets
- Read your orders
[Allow] [Deny]
3. Access Token:
{
"access_token": "eyJhbGc...",
"scope": "pets:read orders:read",
"expires_in": 3600
}
4. API Request:
GET /v1/pets
Authorization: Bearer eyJhbGc...
200 OK (scope validated)
How Scopes Work
Token Contains Scopes
Access token includes granted scopes:
// Decoded JWT
{
"sub": "user-456",
"scope": "pets:read orders:read",
"exp": 1710331200
}
API Validates Scopes
app.get('/v1/pets', requireScope('pets:read'), async (req, res) => {
const pets = await getPets();
res.json(pets);
});
app.post('/v1/pets', requireScope('pets:write'), async (req, res) => {
const pet = await createPet(req.body);
res.status(201).json(pet);
});
function requireScope(requiredScope) {
return (req, res, next) => {
const token = extractToken(req);
const decoded = verifyToken(token);
if (!decoded.scope.includes(requiredScope)) {
return res.status(403).json({
error: 'insufficient_scope',
message: `Requires scope: ${requiredScope}`
});
}
next();
};
}
Designing Scopes
Scope Naming Conventions
Resource:Action Pattern:
pets:read
pets:write
orders:read
orders:write
users:read
users:write
Granular Scopes:
pets:read
pets:create
pets:update
pets:delete
Wildcard Scopes:
pets:* - All pet operations
*:read - Read all resources
admin:* - Full admin access
Scope Hierarchy
admin:all
├── pets:*
│ ├── pets:read
│ ├── pets:write
│ └── pets:delete
├── orders:*
│ ├── orders:read
│ └── orders:write
└── users:*
├── users:read
└── users:write
Implementing Scope Validation
Middleware Approach
function requireScopes(...requiredScopes) {
return (req, res, next) => {
const token = extractToken(req);
const decoded = verifyToken(token);
const tokenScopes = decoded.scope.split(' ');
const hasAllScopes = requiredScopes.every(scope =>
tokenScopes.includes(scope) || tokenScopes.includes('admin:all')
);
if (!hasAllScopes) {
return res.status(403).json({
error: 'insufficient_scope',
required: requiredScopes,
provided: tokenScopes
});
}
req.user = decoded;
next();
};
}
// Usage
app.get('/v1/pets', requireScopes('pets:read'), getPets);
app.post('/v1/pets', requireScopes('pets:write'), createPet);
app.delete('/v1/pets/:id', requireScopes('pets:delete'), deletePet);
Decorator Approach (TypeScript)
function RequireScopes(...scopes: string[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const req = args[0];
const res = args[1];
const token = extractToken(req);
const decoded = verifyToken(token);
if (!hasScopes(decoded.scope, scopes)) {
return res.status(403).json({ error: 'insufficient_scope' });
}
return originalMethod.apply(this, args);
};
};
}
// Usage
class PetsController {
@RequireScopes('pets:read')
async getPets(req, res) {
const pets = await this.petService.findAll();
res.json(pets);
}
@RequireScopes('pets:write')
async createPet(req, res) {
const pet = await this.petService.create(req.body);
res.status(201).json(pet);
}
}
How Modern PetstoreAPI Uses Scopes
Available Scopes
pets:read - Read pet data
pets:write - Create/update pets
pets:delete - Delete pets
orders:read - Read orders
orders:write - Create orders
inventory:read - Read inventory
inventory:write - Update inventory
users:read - Read user profile
users:write - Update user profile
admin:all - Full access
Scope Validation
GET /v1/pets
Authorization: Bearer token_with_pets:read
200 OK
POST /v1/pets
Authorization: Bearer token_with_pets:read
403 Forbidden
{
"error": "insufficient_scope",
"required": ["pets:write"],
"provided": ["pets:read"]
}
See Modern PetstoreAPI OAuth documentation.
Testing Scopes with Apidog
Apidog supports OAuth scope testing:
- Configure OAuth 2.0 auth
- Request specific scopes
- Test endpoints with different scopes
- Validate 403 responses for insufficient scopes
Best Practices
1. Use granular scopes - pets:read not read_all
2. Follow naming conventions - resource:action format
3. Document all scopes - List in API documentation
4. Validate on every request - Don’t trust client
5. Return clear errors - Show required vs provided scopes
6. Use least privilege - Request minimum scopes needed
Conclusion
OAuth 2.0 scopes provide granular access control. Use resource:action format, validate on every request, and document all scopes. Modern PetstoreAPI demonstrates production-ready scope implementation.
FAQ
What’s the difference between scopes and roles?
Scopes are permissions for access tokens. Roles are user groups with assigned permissions.
Can you have multiple scopes?
Yes, separate with spaces: pets:read orders:read users:write
How do you revoke scopes?
Revoke the access token or issue a new token with different scopes.
Should scopes be in the JWT?
Yes, include in the scope claim for stateless validation.
How granular should scopes be?
Balance granularity with usability. pets:read and pets:write is usually sufficient.



