OAuth2 Scopes — Granular Permission Control for API Access Tokens
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
- What problem do scopes solve in OAuth2?
- What is the recommended format for scope names?
- How does the client request specific scopes?
- Where are scopes enforced — authorization server or resource server?
- What happens if the requested scope exceeds what is granted?
Answers:
- Scopes enable granular permissions so a token grants only the access the application needs.
resource:action(e.g.,reports:read) or URL-based for public APIs.- By including the
scopeparameter in the authorization request (space-separated list). - Both. The authorization server records granted scopes. The resource server validates scopes on each request.
- 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
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