Skip to content

OAuth2 Scopes — Granular Permission Control for API Access Tokens

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about OAuth2 Scopes. We cover key concepts, practical examples, and best practices to help you master this topic.

OAuth2 scopes define the specific permissions a client requests, enabling granular access control where tokens are limited to precisely the operations the client needs.

What You'll Learn

How scopes work, scope naming conventions, how clients request scopes, and how resource servers enforce scope-based access control.

Why It Matters

Without scopes, a token either grants full access or no access. Scopes implement the principle of Least Privilege — a read-only integration cannot delete data even if the token is compromised.

Real-World Use

Google Calendar API scopes: https://www.googleapis.com/auth/calendar.readonly (read-only) vs https://www.googleapis.com/auth/calendar (full access). GitHub scopes: repo (full Repository access), repo:read (read-only).

flowchart LR
    A["Client"] -->|"Request token with scopes"| B["Auth Server"]
    B -->|"User consents to scopes"| C["Consent Screen"]
    C -->|"Token with granted scopes"| B
    B -->|"Token + scopes"| A
    A -->|"API call with token"| D["Resource Server"]
    D -->|"Check scope"| E["Enough Permission?"]
    E -->|"Yes"| F["200 OK"]
    E -->|"No"| G["403 Forbidden"]
    style A fill:#dbeafe,stroke:#2563eb
    style B fill:#fef3c7,stroke:#d97706
    style D fill:#dcfce7,stroke:#16a34a
    style G fill:#fecaca,stroke:#dc2626

Scope Naming Conventions

Format Example Description
resource:action reports:read Simple, readable
resource.action reports.read Dot notation
https://api.example.com/auth/resource.readonly URL-based Google-style, avoids collisions

Code Example: Scope-Based Access Control

from flask import Flask, request, jsonify
import jwt

app = Flask(__name__)
SECRET = "your-secret"

# Scope to permission mapping
SCOPE_PERMISSIONS = {
    "reports:read": ["GET"],
    "reports:write": ["POST", "PUT"],
    "reports:delete": ["DELETE"],
    "users:read": ["GET"],
    "users:admin": ["GET", "POST", "PUT", "DELETE"]
}

def require_scope(required_scope):
    def decorator(f):
        def wrapper(*args, **kwargs):
            auth = request.headers.get("Authorization", "")
            token = auth[7:] if auth.startswith("Bearer ") else None
            if not token:
                return jsonify({"error": "Missing token"}), 401

            try:
                payload = jwt.decode(token, SECRET, algorithms=["HS256"])
                token_scopes = payload.get("scope", "").split()
                if required_scope not in token_scopes:
                    return jsonify({
                        "error": "Insufficient scope",
                        "required": required_scope,
                        "granted": token_scopes
                    }), 403
                request.user = payload
            except jwt.InvalidTokenError:
                return jsonify({"error": "Invalid token"}), 401

            return f(*args, **kwargs)
        wrapper.__name__ = f.__name__
        return wrapper
    return decorator

@app.route("/api/reports")
@require_scope("reports:read")
def get_reports():
    return jsonify({"reports": ["q1", "q2", "q3"]})

@app.route("/api/reports", methods=["POST"])
@require_scope("reports:write")
def create_report():
    return jsonify({"message": "Report created"})

if __name__ == "__main__":
    app.run()

Code Example: Client Requesting Specific Scopes

import requests

# Request only the scopes needed
token_response = requests.post("https://auth.example.com/oauth/token", data={
    "grant_type": "authorization_code",
    "code": "auth-code",
    "client_id": "my-app",
    "client_secret": "my-secret",
    "scope": "reports:read users:read"  # Least privilege
})

token_data = token_response.json()
print(f"Granted scopes: {token_data.get('scope')}")
# Output: Granted scopes: reports:read users:read

Code Example: Validating Scopes on Resource Server

@app.route("/api/users/<user_id>", methods=["DELETE"])
@require_scope("users:admin")
def delete_user(user_id):
    # Only tokens with users:admin scope can delete users
    return jsonify({"message": f"User {user_id} deleted"})

Common Mistakes

1. Requesting All Scopes by Default

Asking for scope: "*" or all available scopes violates least privilege. Your app should request only what it needs.

2. Not Checking Scopes on the Resource Server

The authorization server issues scopes, but the resource server must enforce them. Never trust scopes without validation.

3. Using Ambiguous Scope Names

read, write, admin are too vague. reports:read, reports:write are clear and specific.

4. Ignoring Unknown Scopes

If a client requests an unknown scope, the authorization server should reject the request or silently ignore the unknown scope.

5. Not Documenting Scopes

Developers need to know what each scope grants. Document every scope with its permission level and the data it accesses.

Practice Questions

  1. What problem do scopes solve in OAuth2?
  2. What is the recommended format for scope names?
  3. How does the client request specific scopes?
  4. Where are scopes enforced — authorization server or resource server?
  5. What happens if the requested scope exceeds what is granted?

Answers:

  1. Scopes enable granular permissions so a token grants only the access the application needs.
  2. resource:action (e.g., reports:read) or URL-based for public APIs.
  3. By including the scope parameter in the authorization request (space-separated list).
  4. Both. The authorization server records granted scopes. The resource server validates scopes on each request.
  5. The granted scopes in the token may be a subset of what was requested (user or server can reduce scopes). The client should check granted scopes.

Challenge: Design a scope system for a document management API. Define scopes for reading, creating, updating, deleting documents, and admin functions. Show how a client requests minimal scopes and how the server enforces them.

FAQ

What is the difference between a scope and a role?

A scope is a permission (can read reports). A role is a collection of scopes (admin = read + write + delete). Scopes are granular; roles bundle scopes.

How are scopes delivered in a token?

As a space-separated string in the scope claim of the JWT or in the introspection response. Example: 'scope': 'reports:read users:read'

Can scopes be added to a token after issuance?

Generally no. To add scopes, the client must re-authorize with the additional scopes and get a new token.

What is the 'openid' scope?

The openid scope is required for OpenID Connect. It signals that the client wants an ID token for authentication in addition to the access token.

How many scopes should a token typically have?

As few as needed for the client to function. A reporting app might have 2-3 scopes. Avoid tokens with 20+ scopes — split the client instead.

Mini Project

Create a Flask resource server that validates scopes on every endpoint. Define scopes for a task management API (tasks:read, tasks:write, tasks:delete, admin). Test with tokens that have different scope sets.

What's Next

Now learn about Refresh Tokens — long-lived credentials that obtain new access tokens without user interaction.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro