Skip to content

OAuth2 Claims — Structured Authorization Claims in Access and ID Tokens

DodaTech Updated 2026-06-28 4 min read

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

  1. What is the difference between standard and custom claims?
  2. How do you validate claims in a resource server?
  3. What claims should every JWT have?
  4. How do you handle claim changes (e.g., role change) before token expiry?
  5. Can claims be used for multi-tenant isolation?

Answers:

  1. Standard claims are defined in RFCs (sub, iss, aud, exp); custom claims are application-specific (tenant_id, roles, permissions).
  2. Decode and verify the JWT signature, then validate required claims exist and match expected values.
  3. sub (subject), iss (issuer), aud (audience), exp (expiration), iat (issued at), and jti (token ID).
  4. You can't immediately — the old token remains valid. Use short TTL, token exchange, or claim version numbers with a check API.
  5. Yes. Include tenant_id as 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 is the `acr` claim?

: Authentication Context Class Reference — indicates the authentication method strength (e.g., password, MFA, biometric).

Can I add custom claims to any token type?

: Yes, but avoid adding them to opaque tokens that don't support claims. Use structured tokens (JWT) for custom claims.

How big can a token be with custom claims?

: JWTs should stay under 8KB to avoid HTTP header size limits. Keep claims minimal.

Are claims encrypted?

: No. JWT claims are base64url-encoded, not encrypted. Anyone with the token can read claims. Use JWE for confidentiality.

How do I version my claims?

: Include a claim_version integer. Increment it when claims change. Clients can check this value and request a new token if stale.

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