DEV Community

Preecha
Preecha

Posted on

What Are OAuth 2.0 Scopes and How Do They Work?

TL;DR

OAuth 2.0 scopes are permission strings that define what an access token can do. Use a format such as resource:action, for example pets:read or orders:write. Request scopes during authorization and validate them on every protected API endpoint. Modern PetstoreAPI uses scopes for read and write access to pets, orders, inventory, and user data.

Try Apidog today

Introduction

A third-party app wants to read your pet store’s inventory. Should it also be allowed to create orders, delete pets, or manage users? No. It should receive only the permissions it needs.

OAuth 2.0 scopes provide this access control. The app requests an inventory:read scope, and your API checks that the access token includes the scope before returning inventory data.

Modern PetstoreAPI implements granular scopes for 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 associated with OAuth access tokens. They define which operations the token can perform.

Scope format

A resource:action format makes scopes easy to understand and validate:

pets:read       - Read pet data
pets:write      - Create or update pets
orders:read     - Read orders
orders:write    - Create orders
admin:all       - Full admin access
Enter fullscreen mode Exit fullscreen mode

How Scopes Work in the OAuth Flow

1. Request scopes during authorization

The client includes the requested scopes in the authorization request:

GET /oauth/authorize?
  client_id=app123&
  scope=pets:read orders:read&
  redirect_uri=https://app.com/callback
Enter fullscreen mode Exit fullscreen mode

2. Display the requested permissions

The authorization server shows the user what the application is requesting:

App "PetFinder" wants to:

- Read your pets
- Read your orders

[Allow] [Deny]
Enter fullscreen mode Exit fullscreen mode

3. Return granted scopes in the access token

The token response includes the scopes granted to the client:

{
  "access_token": "eyJhbGc...",
  "scope": "pets:read orders:read",
  "expires_in": 3600
}
Enter fullscreen mode Exit fullscreen mode

The granted scopes can be narrower than the scopes originally requested if the user or authorization server denies some permissions.

4. Send the access token to the API

The client includes the token in the Authorization header:

GET /v1/pets
Authorization: Bearer eyJhbGc...
Enter fullscreen mode Exit fullscreen mode

The API verifies the token and checks whether it contains pets:read before returning the response.

Store Scopes in the Access Token

For a JWT access token, scopes are commonly stored in the scope claim:

{
  "sub": "user-456",
  "scope": "pets:read orders:read",
  "exp": 1710331200
}
Enter fullscreen mode Exit fullscreen mode

Before checking permissions, your authentication middleware should:

  1. Extract the bearer token.
  2. Verify the token signature and expiration.
  3. Read the scope claim.
  4. Reject the request if the required scope is missing.

Validate Scopes on API Endpoints

Each protected endpoint should declare the scope it requires:

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);
  }
);
Enter fullscreen mode Exit fullscreen mode

A simple requireScope middleware can perform the check:

function requireScope(requiredScope) {
  return (req, res, next) => {
    const token = extractToken(req);
    const decoded = verifyToken(token);
    const tokenScopes = decoded.scope.split(' ');

    if (!tokenScopes.includes(requiredScope)) {
      return res.status(403).json({
        error: 'insufficient_scope',
        message: `Requires scope: ${requiredScope}`
      });
    }

    next();
  };
}
Enter fullscreen mode Exit fullscreen mode

A request with a valid token but insufficient permissions should return 403 Forbidden:

{
  "error": "insufficient_scope",
  "message": "Requires scope: pets:write"
}
Enter fullscreen mode Exit fullscreen mode

Authentication and authorization are separate checks:

  • Return 401 Unauthorized when the token is missing, invalid, or expired.
  • Return 403 Forbidden when the token is valid but lacks the required scope.

Design a Scope Naming Convention

Use the resource:action pattern

Start with a consistent naming convention:

pets:read
pets:write
orders:read
orders:write
users:read
users:write
Enter fullscreen mode Exit fullscreen mode

Choose between broad and granular actions

Broad scopes are easier to manage:

pets:read
pets:write
Enter fullscreen mode Exit fullscreen mode

More granular scopes provide tighter control:

pets:read
pets:create
pets:update
pets:delete
Enter fullscreen mode Exit fullscreen mode

Choose the level of granularity based on the operations your clients need to perform. Avoid creating separate scopes when the additional distinction does not provide a meaningful security benefit.

Define wildcard or administrator scopes carefully

Some systems define broader scopes:

pets:*        - All pet operations
*:read        - Read all resources
admin:*       - Full admin access
Enter fullscreen mode Exit fullscreen mode

If you support wildcard scopes, document how they expand and ensure the validation logic handles them consistently.

Model Scope Hierarchies

A scope hierarchy can describe broader permissions and their narrower capabilities:

admin:all
  ├── pets:*
  │   ├── pets:read
  │   ├── pets:write
  │   └── pets:delete
  ├── orders:*
  │   ├── orders:read
  │   └── orders:write
  └── users:*
      ├── users:read
      └── users:write
Enter fullscreen mode Exit fullscreen mode

For example, an admin:all token can be treated as satisfying resource-level checks. Make this behavior explicit in your authorization rules rather than assuming that every scope hierarchy will be understood automatically.

Implement Multiple-Scope Validation

Some endpoints require more than one scope. A reusable middleware can accept multiple required scopes:

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();
  };
}
Enter fullscreen mode Exit fullscreen mode

Use the middleware when registering routes:

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
);
Enter fullscreen mode Exit fullscreen mode

The example uses an all-scopes rule: the token must contain every required scope unless it contains admin:all. If your API needs an any-scopes rule, implement that separately and document the difference.

Validate Scopes with a TypeScript Decorator

In a TypeScript application, authorization can also be applied with a decorator:

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);
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

Apply the decorator to controller methods:

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Whichever implementation style you choose, keep token verification and scope checks centralized so that routes do not implement authorization inconsistently.

How Modern PetstoreAPI Uses Scopes

Available scopes

Modern PetstoreAPI defines the following scopes:

pets:read          - Read pet data
pets:write         - Create or 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
Enter fullscreen mode Exit fullscreen mode

Scope validation example

A token with pets:read can access the read endpoint:

GET /v1/pets
Authorization: Bearer token_with_pets:read
Enter fullscreen mode Exit fullscreen mode
200 OK
Enter fullscreen mode Exit fullscreen mode

The same token cannot create a pet because it does not include pets:write:

POST /v1/pets
Authorization: Bearer token_with_pets:read
Enter fullscreen mode Exit fullscreen mode
403 Forbidden
Enter fullscreen mode Exit fullscreen mode
{
  "error": "insufficient_scope",
  "required": ["pets:write"],
  "provided": ["pets:read"]
}
Enter fullscreen mode Exit fullscreen mode

See Modern PetstoreAPI OAuth documentation.

Test OAuth Scopes with Apidog

Use an API client such as Apidog to test both successful and failed authorization cases.

A practical scope test plan includes:

  1. Configure OAuth 2.0 authentication.
  2. Request a token with pets:read.
  3. Call GET /v1/pets and verify that it succeeds.
  4. Call POST /v1/pets with the same token.
  5. Verify that the API returns 403 Forbidden.
  6. Request a token with pets:write.
  7. Call POST /v1/pets again and verify the expected success response.
  8. Test expired, malformed, and missing tokens separately.

This tests both the OAuth flow and the endpoint-level scope enforcement.

Best Practices

  1. Use granular scopes. Prefer pets:read over a broad permission such as read_all.

  2. Follow a naming convention. Use a consistent resource:action format.

  3. Document every scope. List available scopes and the endpoints that require them in your API documentation.

  4. Validate every request. Do not trust the client to enforce permissions.

  5. Return clear errors. Include the required and provided scopes where appropriate.

  6. Apply least privilege. Request and grant only the minimum scopes a client needs.

  7. Test both positive and negative cases. Confirm that allowed operations succeed and insufficient scopes produce 403 responses.

Conclusion

OAuth 2.0 scopes provide granular access control for APIs. Use a consistent resource:action naming scheme, include granted scopes in access tokens, validate them on every protected request, and document the permissions available to clients.

Modern PetstoreAPI demonstrates this approach with separate scopes for pets, orders, inventory, and user data.

FAQ

What’s the difference between scopes and roles?

Scopes are permissions associated with access tokens. Roles are user groups with assigned permissions. A role can be used to determine which scopes a user or client may receive, while the API uses the token’s scopes to authorize individual requests.

Can you have multiple scopes?

Yes. Separate multiple scopes with spaces:

pets:read orders:read users:write
Enter fullscreen mode Exit fullscreen mode

How do you revoke scopes?

Revoke the access token or issue a new token with a different set of scopes.

Should scopes be in the JWT?

Scopes can be included in the JWT scope claim for stateless validation. Your API must still verify the token before trusting the claim.

How granular should scopes be?

Balance granularity with usability. pets:read and pets:write are often sufficient, but separate scopes such as pets:create, pets:update, and pets:delete may be useful when clients need more narrowly defined permissions.

Top comments (0)