Skip to content

OAuth2 Audience Validation — Ensuring Tokens Reach the Right Resource Server

DodaTech Updated 2026-06-28 6 min read

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

  1. What is the purpose of the aud claim in a JWT?
  2. How does audience validation prevent token misuse?
  3. Why must resource servers handle both string and array aud values?
  4. What is the difference between aud and iss claims?
  5. How does multi-audience token support microservice communication?

Answers:

  1. The aud claim identifies the intended recipient of the token. The resource server rejects tokens whose audience does not match its own identifier.
  2. 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.
  3. 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.
  4. iss identifies the token issuer (the auth server). aud identifies the intended recipient (the resource server). Both are essential for token validation.
  5. 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

What happens if the aud claim is missing?

The resource server should reject the token. A missing aud claim means the token was issued without a specific target, which is insecure.

Can I use custom audience values?

Yes. Audience values are opaque strings. Use URI-based values for uniqueness (https://api.service.com/resource) or short identifiers.

How does audience validation work with JWKS?

The resource server fetches the JWKS from the issuer, verifies the token signature, then checks the aud claim against its expected value.

Should the audience match the request URL?

Typically yes. Using the resource server's base URL as the audience is a common pattern. The resource server checks aud against its known URL.

Can a token have multiple audiences?

Yes. The aud claim can be an array of strings. Each resource server in the list can accept the token. This is useful for composite APIs.

How do I debug audience mismatch errors?

Log the token's aud claim, the server's expected audience, and the comparison result. Most JWT libraries have debug modes for validation.

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