OAuth2 Audience Validation — Ensuring Tokens Reach the Right Resource Server
In this tutorial, you will learn about OAuth2 Audience Validation. We cover key concepts, practical examples, and best practices to help you master this topic.
OAuth2 audience validation checks the aud (audience) claim in a token to ensure the token was issued for the specific resource server receiving it, preventing token misuse across services.
What You'll Learn
The aud claim structure, single and multi-audience tokens, resource server audience enforcement, audience mismatch handling, and audience validation in Distributed Systems.
Why It Matters
Without audience validation, a token issued for one service (e.g., email API) can be used to access another service (e.g., billing API) if they share the same authorization server. Audience validation confines tokens to their intended service.
Real-World Use
Auth0 includes the audience parameter in token requests. Google APIs use different audience values for different APIs (drive.google.com vs mail.google.com). Durga Antivirus Pro assigns unique audience values per microservice.
flowchart LR
A["Auth Server"] -->|"Token aud=api.durga.com/threats"| B["Threat Intel API"]
A -->|"Token aud=api.durga.com/scans"| C["Scanner API"]
D["Attacker steals\ntoken for threats"] -->|"Uses at Scanner API"| C
C -->|"Check aud: expected\napi.durga.com/scans\ngot api.durga.com/threats"| E["Reject —\naud mismatch"]
style A fill:#fef3c7,stroke:#d97706
style B fill:#dcfce7,stroke:#16a34a
style C fill:#dbeafe,stroke:#2563eb
style E fill:#fecaca,stroke:#dc2626
Code Example: Audience Validation in Resource Server
import jwt, datetime, os
from flask import Flask, request, jsonify, g
from functools import wraps
app = Flask(__name__)
JWKS_URI = os.environ.get("JWKS_URI", "https://auth.dodatech.com/.well-known/jwks.json")
# Each resource server knows its own audience
RESOURCE_SERVER_AUDIENCE = "https://api.durga-antivirus.com/v1"
def validate_audience(token):
"""Validate the token's audience claim."""
try:
# In production, fetch JWKS and verify properly
payload = jwt.decode(
token,
os.environ.get("AUTH_PUBLIC_KEY", ""),
algorithms=["RS256"],
audience=RESOURCE_SERVER_AUDIENCE,
options={"verify_aud": True}
)
return payload
except jwt.InvalidAudienceError:
return None # Audience mismatch
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
@app.before_request
def authenticate():
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return jsonify({"error": "Unauthorized"}), 401
payload = validate_audience(auth[7:])
if not payload:
return jsonify({
"error": "invalid_token",
"error_description": "Token audience does not match this resource server"
}), 401
g.current_user = payload
@app.route("/api/v1/threats")
def list_threats():
return jsonify({"threats": [], "audience_validated": True})
Code Example: Multi-Audience Token Validation
# Token with multiple audiences
# aud: ["https://api.durga-antivirus.com/v1", "https://api.durga-antivirus.com/v2"]
MULTI_AUDIENCE_SERVERS = [
"https://api.durga-antivirus.com/v1",
"https://api.durga-antivirus.com/v2",
"https://api.durga-antivirus.com/admin"
]
def validate_multi_audience(token):
"""Validate token against multiple allowed audiences."""
try:
# Decode without audience verification first
payload = jwt.decode(
token,
os.environ.get("AUTH_PUBLIC_KEY", ""),
algorithms=["RS256"],
options={"verify_aud": False}
)
# Manual audience check
token_aud = payload.get("aud", [])
if isinstance(token_aud, str):
token_aud = [token_aud]
# Check if any of our audiences match
server_audiences = [
request.host_url.rstrip("/"),
f"{request.scheme}://{request.host}{request.path}"
]
for aud in token_aud:
if any(server_aud.startswith(aud) for server_aud in server_audiences):
return payload
return None
except jwt.InvalidTokenError:
return None
# Custom decorator for specific audience
def require_audience(expected_audience):
"""Decorator to enforce specific audience."""
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.headers.get("Authorization", "")
payload = validate_multi_audience(auth[7:])
if not payload:
return jsonify({"error": "invalid_audience"}), 401
token_aud = payload.get("aud", [])
if isinstance(token_aud, str):
token_aud = [token_aud]
if expected_audience not in token_aud:
return jsonify({
"error": "insufficient_audience",
"required": expected_audience,
"token_audiences": token_aud
}), 403
g.current_user = payload
return f(*args, **kwargs)
return decorated
return decorator
@app.route("/api/v1/admin/config")
@require_audience("https://api.durga-antivirus.com/admin")
def admin_config():
return jsonify({"config": "sensitive"})
Code Example: Token Issuance with Audience
def issue_service_token(service_name, audience, scopes):
"""Issue a token with specific audience."""
now = datetime.datetime.utcnow()
token = jwt.encode({
"iss": "auth.dodatech.com",
"sub": service_name,
"aud": audience,
"scope": " ".join(scopes),
"iat": now,
"exp": now + datetime.timedelta(hours=1),
"jti": secrets.token_hex(16)
}, SECRET, algorithm="HS256")
return token
# Auth server issues tokens with specific audience
@app.route("/oauth/token", methods=["POST"])
def token():
client_id = request.form.get("client_id")
audience = request.form.get("audience", "default")
# Validate client
client = clients.get(client_id)
if not client:
return jsonify({"error": "invalid_client"}), 401
# Validate audience is allowed for this client
if audience not in client["allowed_audiences"]:
return jsonify({"error": "invalid_target"}), 400
token = issue_service_token(client_id, audience, client["scopes"])
return jsonify({
"access_token": token,
"token_type": "Bearer",
"expires_in": 3600,
"audience": audience
})
Common Mistakes
1. Not Validating Audience at All
Many resource servers skip audience validation. Without it, any valid token from the same issuer can access any service. Always validate the aud claim.
2. Using Generic Audience Values
Using "api" or "https://example.com" as the audience is too broad. Use specific values like "https://api.durga-antivirus.com/v1/threats" for precise targeting.
3. Confusing Audience with Issuer
The issuer (iss) is who created the token. The audience (aud) is who should receive it. Both are validated but serve different purposes.
4. Not Handling String vs Array aud
The aud claim can be a string or an array of strings. Your validation code must handle both formats to avoid bypasses.
5. Audience Mismatch on Token Refresh
Refresh tokens carry their own audience. The new access token should have the same audience. Never change the audience during refresh.
Practice Questions
- What is the purpose of the aud claim in a JWT?
- How does audience validation prevent token misuse?
- Why must resource servers handle both string and array aud values?
- What is the difference between aud and iss claims?
- How does multi-audience token support microservice communication?
Answers:
- The aud claim identifies the intended recipient of the token. The resource server rejects tokens whose audience does not match its own identifier.
- A token issued for the threats API has aud=threats.api. If stolen and presented to the scanner API, the scanner API rejects it because the audience does not match.
- The JWT spec allows aud to be either a single string or an array. A naive check that expects only a string will fail on array values and may create a bypass.
- iss identifies the token issuer (the auth server). aud identifies the intended recipient (the resource server). Both are essential for token validation.
- A token can list multiple audiences (["service-a", "service-b"]), allowing it to be used across multiple Microservices in the same trust boundary.
Challenge: Build a multi-service architecture where the auth server issues tokens with specific audiences per service, and each resource server validates its own audience. Include a token-sharing scenario between services and demonstrate audience enforcement.
FAQ
Mini Project
Build an OAuth2 authorization server that issues tokens with audience-specific claims, and two resource servers that each validate their own audience. Include a test that shows a token for one server is rejected by the other.
What's Next
Now learn about Refresh Token Rotation for rotating refresh tokens on each use to prevent token theft.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro