DEV Community

Cover image for What Are OAuth 2.0 Scopes and How Do They Work?
Wanda
Wanda

Posted on • Originally published at apidog.com

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 the format resource:action, such as pets:read or orders:write. Request scopes during authorization, and validate them on your API endpoints. Modern PetstoreAPI implements scopes for read/write access to pets, orders, and user data.

Try Apidog today

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. For example, the app requests the inventory:read scope, and your API only returns data if the access token includes this scope.

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. They restrict what an access token can do.

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

Scope in OAuth Flow

1. 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. User Consent:

App "PetFinder" wants to:
- Read your pets
- Read your orders

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

3. Access Token:

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

4. API Request:

GET /v1/pets
Authorization: Bearer eyJhbGc...

200 OK (scope validated)
Enter fullscreen mode Exit fullscreen mode

How Scopes Work

Token Contains Scopes

The access token itself contains the granted scopes, often as a space-delimited string.

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

API Validates Scopes

Check required scopes on every endpoint using middleware:

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

Designing Scopes

Scope Naming Conventions

Resource:Action Pattern:

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

Granular Scopes:

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

Wildcard Scopes:

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

Scope Hierarchy

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

Implementing Scope Validation

Middleware Approach

Implement a reusable middleware for scope validation:

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

Decorator Approach (TypeScript)

For TypeScript, use decorators for cleaner controller logic:

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

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

Scope Validation

GET /v1/pets
Authorization: Bearer token_with_pets:read

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

403 Forbidden
{
  "error": "insufficient_scope",
  "required": ["pets:write"],
  "provided": ["pets:read"]
}
Enter fullscreen mode Exit fullscreen mode

See Modern PetstoreAPI OAuth documentation.

Testing Scopes with Apidog

Apidog supports OAuth scope testing:

  1. Configure OAuth 2.0 auth
  2. Request specific scopes
  3. Test endpoints with different scopes
  4. Validate 403 responses for insufficient scopes

Best Practices

  1. Use granular scopes – Prefer pets:read over read_all
  2. Follow naming conventions – Use resource:action format
  3. Document all scopes – List every scope in your API documentation
  4. Validate on every request – Never trust the client
  5. Return clear errors – Show required vs. provided scopes
  6. Use least privilege – Request only the 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 them 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 scopes in the scope claim for stateless validation.

How granular should scopes be?

Balance granularity and usability. Usually, pets:read and pets:write are sufficient.

Top comments (0)