OAuth2 Rich Authorization Requests (RAR) — RFC 9396 for Structured Authorization Details
In this tutorial, you will learn about OAuth2 Rich Authorization Requests (RAR). We cover key concepts, practical examples, and best practices to help you master this topic.
OAuth2 Rich Authorization Requests (RAR, RFC 9396) extend authorization requests beyond simple scope strings, allowing clients to request structured authorization details — specific resources, actions, locations, and constraints — as JSON objects.
What You'll Learn
- Authorization details as structured JSON
- RAR in authorization and token requests
- Combining scopes with authorization details
- Consent display for rich requests
- Validating and enforcing authorization details
Why It Matters
Scope strings are too coarse for fine-grained authorization. A scope like read:threats gives access to all threats. RAR lets a client request access to specific threat IDs in a specific tenant with specific actions. DodaTech's API uses RAR to grant partners access to exactly the threat feeds they subscribe to, nothing more.
Real-World Use
A partner integration requests access to read threat indicators for two specific campaigns in a specific region. Instead of the broad read:threats scope, the RAR specifies type: campaign-iocs, campaign_ids: [camp-1, camp-2], and region: apac. The consent screen shows exactly this granular detail.
sequenceDiagram
participant Client as Partner App
participant Auth as Authorization Server
participant API as Threat API
Client->>Auth: Authorization Request + RAR
Note over Client,Auth: authorization_details: [{type: "campaign-iocs", campaigns: ["camp-1","camp-2"], region: "apac"}]
Auth->>Auth: Validate RAR structure
Auth->>User: Consent screen shows granular details
User-->>Auth: Approve
Auth-->>Client: Token with RAR in token
Client->>API: Request + Token (with RAR)
API->>API: Enforce RAR: only camp-1, camp-2, region=apac
API-->>Client: Filtered results
Code Examples
Example 1: Authorization Request with RAR
import json
import requests
def build_rar_authorization_url(client_id, redirect_uri, auth_server,
authorization_details, state=None):
"""
Build authorization URL with Rich Authorization Requests.
Per RFC 9396, authorization_details is a JSON array of objects.
"""
params = {
'response_type': 'code',
'client_id': client_id,
'redirect_uri': redirect_uri,
'authorization_details': json.dumps(authorization_details),
'state': state or secrets.token_urlsafe(16)
}
auth_url = f"{auth_server}/authorize?{urlencode(params)}"
return auth_url
# Example RAR for accessing specific threats
details = [
{
'type': 'threat-analysis',
'actions': ['read', 'export'],
'locations': ['threat-123', 'threat-456'],
'datatypes': ['indicators', 'reports']
},
{
'type': 'remediation',
'actions': ['read'],
'locations': ['campaign-campaign-789']
}
]
auth_url = build_rar_authorization_url(
CLIENT_ID,
'https://app.dodatech.com/callback',
'https://auth.dodatech.com',
details
)
print(f"Authorize URL length: {len(auth_url)} chars")
Example 2: RAR Validation in Authorization Server
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
# RAR type definitions
RAR_TYPES = {
'threat-analysis': {
'actions': ['read', 'write', 'export', 'delete'],
'properties': {
'locations': {'type': 'array', 'items': 'string'},
'datatypes': {
'type': 'array',
'items': {'type': 'string',
'enum': ['indicators', 'reports', 'raw_data']}
}
}
},
'remediation': {
'actions': ['read', 'write', 'execute'],
'properties': {
'locations': {'type': 'array', 'items': 'string'}
}
}
}
def validate_authorization_details(details):
"""Validate RAR authorization_details array."""
if not isinstance(details, list):
return False, "authorization_details must be an array"
for idx, entry in enumerate(details):
type_def = RAR_TYPES.get(entry.get('type'))
if not type_def:
return False, f"Unknown authorization detail type: {entry.get('type')}"
# Validate actions
for action in entry.get('actions', []):
if action not in type_def['actions']:
return False, f"Invalid action '{action}' for type '{entry['type']}'"
# Validate properties
for prop, value in entry.items():
if prop in ('type', 'actions'):
continue
prop_def = type_def['properties'].get(prop)
if not prop_def:
return False, f"Unknown property '{prop}' for type '{entry['type']}'"
return True, "valid"
@app.route('/authorize', methods=['GET'])
def authorize_with_rar():
auth_details = request.args.get('authorization_details')
if auth_details:
try:
details = json.loads(auth_details)
valid, msg = validate_authorization_details(details)
if not valid:
return jsonify({'error': 'invalid_authorization_details',
'error_description': msg}), 400
# Store RAR for consent and token generation
session['authorization_details'] = details
except json.JSONDecodeError:
return jsonify({'error': 'invalid_authorization_details'}), 400
# Continue with standard auth flow...
return render_consent(client_id, scope, authorization_details=details)
Example 3: RAR Enforcement in Resource Server
from flask import Flask, request, jsonify, g
from functools import wraps
app = Flask(__name__)
def enforce_rar(type_name, required_action=None):
"""Decorator to enforce RAR authorization details on endpoints."""
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
token = extract_token(request)
rar_details = token.get('authorization_details', [])
# Find matching RAR entry
matching = [
d for d in rar_details
if d['type'] == type_name
]
if not matching:
return jsonify({'error': 'insufficient_authorization',
'required_type': type_name}), 403
entry = matching[0]
# Check action
if required_action and required_action not in entry.get('actions', []):
return jsonify({'error': 'action_not_authorized',
'required_action': required_action}), 403
# Store RAR context for the handler
g.rar_context = entry
return f(*args, **kwargs)
return decorated
return decorator
@app.route('/api/threats/<threat_id>')
@enforce_rar('threat-analysis', required_action='read')
def get_threat(threat_id):
"""Get threat data, filtered by RAR locations."""
rar = g.rar_context
allowed_locations = rar.get('locations', [])
threat = get_threat_data(threat_id)
# Enforce location restrictions
if allowed_locations and threat_id not in allowed_locations:
return jsonify({'error': 'location_not_authorized'}), 403
# Enforce datatype restrictions
allowed_datatypes = rar.get('datatypes', ['indicators', 'reports'])
filtered = {
k: v for k, v in threat.items()
if k in allowed_datatypes
}
return jsonify(filtered)
@app.route('/api/threats/<threat_id>/remediate', methods=['POST'])
@enforce_rar('remediation', required_action='execute')
def remediate_threat(threat_id):
"""Execute remediation, if authorized by RAR."""
rar = g.rar_context
return jsonify({'status': 'remediated', 'threat_id': threat_id})
Common Mistakes
1. Using String Scopes Instead of Structured Details
RAR replaces or augments scopes. Don't define authorization details as a single string — use proper JSON arrays.
2. Not Defining RAR Types in Advance
Each authorization detail type must be defined in the authorization server's documentation and supported by resource servers.
3. Ignoring RAR in Token Responses
Include authorization_details in token introspection responses and JWT claims so resource servers can enforce them.
4. Making RAR Too Complex
Balance granularity with usability. Very complex RAR structures confuse users on consent screens.
5. Not Validating RAR at Token Exchange
Validate authorization_details at the token endpoint, not just the authorization endpoint.
Practice Questions
- What does RFC 9396 define?
- How does RAR differ from OAuth2 scopes?
- What fields should each authorization detail object contain?
- How do resource servers enforce RAR?
- How is RAR displayed on consent screens?
Answers:
- RFC 9396 defines Rich Authorization Requests — structured JSON authorization details beyond simple scopes.
- Scopes are coarsely named strings. RAR provides structured objects with typed fields, actions, and locations for fine-grained authorization.
- At minimum
typeandactions. Optional fields depend on the type definition (e.g.,locations,datatypes). - Resource servers parse the
authorization_detailsfrom the token, match the type, and enforce action and location constraints. - Each authorization detail object is displayed as a separate line item with its type, requested actions, and specific resources.
Challenge: Design an RAR system for a document management API with types like document:read, document:write, admin:users. Implement validation on the authorization server and enforcement on the resource server.
FAQ
What's Next
Combine RAR with {{< ilink "OAuth" "OAuth2 Scopes" }} for multi-level authorization, and explore {{< ilink "OAuth" "OAuth2 Claims" }} for structured token claims.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro