OAuth2 Scopes and Permissions — Fine-Grained Access Control with Token Scoping
In this tutorial, you will learn about OAuth2 Scopes and Permissions. We cover key concepts, practical examples, and best practices to help you master this topic.
OAuth2 scopes define granular permissions that clients request during authorization, enabling resource servers to enforce precise access control based on token claims.
What You'll Learn
Scope definition and hierarchy, scope-to-permission mapping patterns, resource server enforcement, scope negotiation, and dynamic scope expansion for step-up authentication.
Why It Matters
Without scopes, tokens are all-or-nothing — a token grants either full access or no access. Scopes let you issue tokens with specific permissions (read-only, write-limited, admin), following the principle of Least Privilege.
Real-World Use
Google APIs use scopes like https://www.googleapis.com/auth/drive.readonly. GitHub uses repo, user, and admin scopes. Durga Antivirus Pro defines scopes like threat:read, threat:write, and admin:config for its partner API tokens.
flowchart TD
A["Client requests\nscope=threat:read"] --> B["Auth Server"]
B --> C{"User consents\nto threat:read?"}
C -->|"Yes"| D["Issue token with\nscope=threat:read"]
C -->|"No"| E["Reject or\nreduce scope"]
D --> F["Resource Server"]
F --> G{"Token has\nthreat:read?"}
G -->|"Yes"| H["Allow read\noperation"]
G -->|"No"| I["403 Forbidden"]
style D fill:#dcfce7,stroke:#16a34a
style H fill:#dcfce7,stroke:#16a34a
style I fill:#fecaca,stroke:#dc2626
Code Example: Scope Definition and Enforcement
import jwt, datetime
from flask import Flask, request, jsonify, g
from functools import wraps
app = Flask(__name__)
SECRET = "scope-secret"
# Scope hierarchy: admin includes all permissions
SCOPE_HIERARCHY = {
"threat:read": ["threat:read"],
"threat:write": ["threat:read", "threat:write"],
"threat:delete": ["threat:read", "threat:write", "threat:delete"],
"admin": ["threat:read", "threat:write", "threat:delete", "admin:config"]
}
def require_scope(required_scope):
"""Decorator to enforce scope requirement."""
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
token_scopes = getattr(g, "scopes", "").split()
required_scopes = SCOPE_HIERARCHY.get(required_scope, [required_scope])
if not any(s in token_scopes for s in required_scopes):
return jsonify({
"error": "insufficient_scope",
"required_scope": required_scope,
"current_scopes": token_scopes
}), 403
return f(*args, **kwargs)
return decorated
return decorator
@app.before_request
def authenticate():
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return
try:
payload = jwt.decode(auth[7:], SECRET, algorithms=["HS256"])
g.user = payload.get("sub")
g.scopes = payload.get("scope", "")
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
@app.route("/api/v1/threats", methods=["GET"])
@require_scope("threat:read")
def list_threats():
return jsonify({"threats": [], "scopes": g.scopes})
@app.route("/api/v1/threats", methods=["POST"])
@require_scope("threat:write")
def create_threat():
return jsonify({"status": "created"})
@app.route("/api/v1/config", methods=["GET"])
@require_scope("admin")
def get_config():
return jsonify({"config": {}})
Code Example: Scope Request and Consent
@app.route("/authorize", methods=["GET"])
def authorize():
"""Authorization endpoint with scope handling."""
client_id = request.args.get("client_id")
requested_scope = request.args.get("scope", "")
redirect_uri = request.args.get("redirect_uri")
client = get_client(client_id)
if not client:
return "Invalid client", 400
# Check if requested scopes are within allowed scopes
requested_scopes = set(requested_scope.split())
allowed_scopes = set(client["allowed_scopes"])
denied_scopes = requested_scopes - allowed_scopes
if denied_scopes:
# Negotiate scopes down to allowed set
negotiated_scope = " ".join(requested_scopes & allowed_scopes)
if not negotiated_scope:
return jsonify({
"error": "invalid_scope",
"scope": " ".join(denied_scopes)
}), 400
requested_scope = negotiated_scope
# Show consent screen (simplified)
# In production, redirect to a consent page
auth_code = generate_auth_code(client_id, redirect_uri, requested_scope)
return jsonify({
"redirect": f"{redirect_uri}?code={auth_code}&scope={requested_scope}"
})
def generate_auth_code(client_id, redirect_uri, scope):
"""Generate authorization code with scope."""
code = secrets.token_urlsafe(32)
authorization_codes[code] = {
"client_id": client_id,
"redirect_uri": redirect_uri,
"scope": scope,
"expires_at": time.time() + 60
}
return code
Code Example: Dynamic Scope Expansion (Step-Up)
# Step-up authentication — expand scope after MFA
def issue_token_with_scope(user_id, scope):
"""Issue token with specified scope."""
return jwt.encode({
"sub": user_id,
"scope": scope,
"mfa_verified": "mfa" in scope,
"iat": datetime.datetime.utcnow(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, SECRET, algorithm="HS256")
@app.route("/api/auth/step-up", methods=["POST"])
def step_up_scope():
"""Elevate token scope after MFA verification."""
current_token = request.headers.get("Authorization", "").replace("Bearer ", "")
current_payload = jwt.decode(current_token, SECRET, algorithms=["HS256"])
current_user = current_payload.get("sub")
# Verify MFA
mfa_code = request.json.get("mfa_code")
if not verify_mfa(current_user, mfa_code):
return jsonify({"error": "MFA verification failed"}), 401
# Expand scope — add threat:write to existing scopes
existing_scopes = current_payload.get("scope", "")
expanded_scope = f"{existing_scopes} threat:write mfa"
new_token = issue_token_with_scope(current_user, expanded_scope)
return jsonify({
"access_token": new_token,
"scope": expanded_scope,
"expires_in": 3600
})
Common Mistakes
1. Flat Scope Design
Using flat scopes like admin and user without hierarchy. Design hierarchical scopes (threat:read, threat:write, threat:delete) for granular control.
2. Not Validating Scopes at Resource Server
The resource server must check scopes on every request. Scopes in tokens can be modified by the client only if the token is forged, but a valid narrow-scope token should not access broad-scope resources.
3. Scope Bloat
Defining too many scopes confuses users and developers. Start with 5-10 well-named scopes and expand as needed. Group related permissions.
4. Ignoring Scope in Token Refresh
When refreshing a token, the new token should carry the same scopes. An attacker should not be able to escalate scope through refresh.
5. Not Documenting Scopes
Clients need to know what scopes exist and what permissions they grant. Document each scope in your API reference with examples.
Practice Questions
- What is the difference between a scope and a permission?
- How does scope hierarchy simplify authorization?
- What happens when a client requests a scope it is not allowed to have?
- How does step-up authentication expand scopes?
- Why should refresh tokens preserve scope?
Answers:
- A scope is a named group of permissions. For example, threat:write scope might include both read and write permissions. A permission is the atomic access right.
- A hierarchy lets you define higher-level scopes (admin) that include lower-level permissions (read, write). The server checks if any scope in the hierarchy grants access.
- The authorization server should negotiate scopes down to the intersection of requested and allowed scopes, or reject the request if no overlap exists.
- After additional verification (MFA), the server issues a new token with expanded scopes. The client uses this new token for operations requiring higher privilege.
- If a refresh could escalate scope, an attacker who steals a read-only refresh token could obtain a write token. Refresh must preserve the original scope.
Challenge: Build a scope-based authorization system with hierarchical scopes, scope negotiation, step-up authentication, and scope-preserving token refresh.
FAQ
Mini Project
Build an OAuth2 authorization server with hierarchical scopes, a resource server that enforces scopes per endpoint, and a step-up authentication flow that dynamically expands scopes after MFA verification.
What's Next
Now explore OAuth2 Audience Validation for ensuring tokens are used only for their intended resource server.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro