OAuth2 Claims — Structured Authorization Claims in Access and ID Tokens
In this tutorial, you will learn about OAuth2 Claims. We cover key concepts, practical examples, and best practices to help you master this topic.
OAuth2 claims are key-value pairs in access and ID tokens that carry identity, authorization, and metadata about the authenticated user and the granted token.
What You'll Learn
- Standard OAuth2 and Openid Connect claims
- Custom claims for business authorization
- Claims mapping and transformation
- Claim-based access control in resource servers
- Claims best practices and security
Why It Matters
Claims enable fine-grained access control without additional API calls. A token with roles: [analyst, admin] and department: security tells the resource server everything it needs. DodaTech's threat API uses claims to enforce department-level isolation without database lookups.
Real-World Use
A multi-tenant security platform includes tenant_id, roles, and permissions claims in every access token. The resource server extracts these claims to filter query results, allowing users from Tenant A to never see Tenant B's data.
flowchart LR
A["Auth Server"] --> B["Access Token Claims"]
B --> C["sub: user_123
(user ID)"]
B --> D["iss: https://auth.dodatech.com
(issuer)"]
B --> E["aud: https://api.dodatech.com
(audience)"]
B --> F["roles: ['analyst', 'admin']
(role-based)"]
B --> G["tenant_id: 'tenant_a'
(multi-tenant)"]
B --> H["permissions: ['read:threats', 'write:reports']
(fine-grained)"]
B --> I["exp: 1700000000
(expiry)"]
B --> J["iat: 1699999100
(issued at)"]
Code Examples
Example 1: Standard Claim Validation
import jwt
from jwt import PyJWKClient
def validate_and_extract(token):
jwks_client = PyJWKClient(
'https://auth.dodatech.com/.well-known/jwks.json'
)
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
audience='https://api.dodatech.com',
issuer='https://auth.dodatech.com',
options={
'require': [
'sub', 'iss', 'aud', 'exp',
'iat', 'jti', 'roles'
],
'verify_exp': True
}
)
return payload
payload = validate_and_extract(user_token)
print(f"User: {payload['sub']}")
print(f"Roles: {payload['roles']}")
print(f"Tenant: {payload.get('tenant_id', 'global')}")
# Output: User: user_123
# Output: Roles: ['analyst', 'admin']
# Output: Tenant: tenant_a
Example 2: Custom Claims for Authorization
def create_custom_claims_token(user, private_key):
"""Issue token with custom authorization claims."""
now = datetime.now(timezone.utc)
payload = {
# Standard claims
'sub': user.id,
'iss': 'https://auth.dodatech.com',
'aud': 'https://api.dodatech.com',
'exp': now + timedelta(minutes=15),
'iat': now,
'jti': str(uuid.uuid4()),
# Custom authorization claims
'roles': user.roles,
'tenant_id': user.tenant_id,
'department': user.department,
'clearance_level': user.clearance_level,
'permissions': user.get_effective_permissions(),
'groups': user.group_memberships,
# Context claims
'auth_method': 'password',
'auth_time': int(now.timestamp())
}
token = jwt.encode(payload, private_key, algorithm='RS256')
return token
# Usage
token = create_custom_claims_token(user, PRIVATE_KEY)
Example 3: Claims-Based Access Control
from flask import Flask, request, jsonify, g
from functools import wraps
app = Flask(__name__)
def require_permission(permission):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
token_payload = extract_token(request)
permissions = token_payload.get('permissions', [])
if permission not in permissions:
return jsonify({
'error': 'Insufficient permissions',
'required': permission,
'granted': permissions
}), 403
g.user_id = token_payload['sub']
g.tenant_id = token_payload.get('tenant_id')
return f(*args, **kwargs)
return decorated
return decorator
@app.route('/api/threats')
@require_permission('read:threats')
def list_threats():
# Claims-based filtering
tenant_filter = {'tenant_id': g.tenant_id} if g.tenant_id else {}
threats = db.threats.find(tenant_filter)
return jsonify([t.to_dict() for t in threats])
Common Mistakes
1. Putting Too Much Data in Claims
Tokens are sent with every request. Keep claims minimal — include IDs and references, not full objects.
2. Using Claims from Untrusted Issuers
Only trust claims from your own authorization server. Validate the iss claim strictly.
3. Not Claiming aud Validation
Without audience validation, a token for Service A can be used against Service B.
4. Static Claims That Never Change
Include a iat (issued at) claim so you can detect stale authorization state.
5. Exposing Sensitive Data in Claims
Claims are encoded but not encrypted in standard JWTs. Don't put secrets, passwords, or PII in claims.
Practice Questions
- What is the difference between standard and custom claims?
- How do you validate claims in a resource server?
- What claims should every JWT have?
- How do you handle claim changes (e.g., role change) before token expiry?
- Can claims be used for multi-tenant isolation?
Answers:
- Standard claims are defined in RFCs (sub, iss, aud, exp); custom claims are application-specific (tenant_id, roles, permissions).
- Decode and verify the JWT signature, then validate required claims exist and match expected values.
sub(subject),iss(issuer),aud(audience),exp(expiration),iat(issued at), andjti(token ID).- You can't immediately — the old token remains valid. Use short TTL, token exchange, or claim version numbers with a check API.
- Yes. Include
tenant_idas a claim and use it in resource server queries to filter data.
Challenge: Design a claims system for a multi-tenant security platform with department-level access, clearance levels, and fine-grained permissions. Implement claims-based filtering in a Flask resource server.
FAQ
What's Next
Use claims in {{< ilink "OAuth" "OAuth2 JWT" }} profile for structured tokens, or build a {{< ilink "OAuth" "OAuth2 Resource Server" }} that enforces claims-based access.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro