
JWT sub and aud: Identity and Audience
A technical examination of JWT sub and aud claims, focusing on identity resolution and audience validation for backend and identity engineers.
sub and aud: Getting Identity and Audience Right
In distributed systems, two JWT claims cause the most security failures when misunderstood: sub (subject) and aud (audience). Engineers often treat sub as a universal user ID and aud as a soft hint. Both are wrong. sub is an opaque identifier local to an Issuer, and aud is a strict cryptographic boundary. If you conflate them, you enable token replay attacks and identity collision.
Part 5 of the JWT From the Spec Up series.
The Subject (sub) is Local, Not Global
The sub claim identifies the principal that is the subject of the JWT. The critical mechanism here is scope. sub has no meaning outside the context of the iss (issuer) claim.
Consider two identity providers: Auth0 and Google. Both issue JWTs. Auth0 might assign sub value auth0|12345 to Alice. Google might assign sub value 108987654321234567890 to Alice. If your backend service aggregates users from both providers, you cannot use sub as the primary key in your database. You will create duplicate records or overwrite data because the string values are not globally unique.
The mechanism of resolution requires a composite key. The unique identity of a user in a distributed system is the tuple (iss, sub).
// WRONG: Using sub alone as a unique identifier
const userId = decodedToken.sub;
// CORRECT: Binding sub to its issuer
const userId = `${decodedToken.iss}|${decodedToken.sub}`;This pattern is essential for privacy as well. If you need a "pairwise identifier" (a unique ID for a specific service that doesn't correlate across services), you should not use sub directly. Instead, hash the (iss, sub) tuple with a salt known only to the service. This prevents other services from linking user behavior if they somehow obtain the token.
The Audience (aud) is a Hard Boundary
The aud claim identifies the recipients that the JWT is intended for. As defined in RFC 7519, this is not a suggestion. It is an authorization constraint.
When a client presents a JWT to a backend API, that API must verify that it is listed in the aud array. If the token was issued for Service A, but the client sends it to Service B, Service B must reject the token immediately, even if the signature is valid and the token is not expired.
Why? Because tokens are often issued with limited scopes. A token for Service A might have scope: read_profile. If Service B accepts this token without checking aud, an attacker who compromises a low-privilege token from Service A could use it to access high-privilege endpoints in Service B.
The validation mechanism is simple but non-negotiable:
- Extract the
audclaim from the JWT. - Check if the current service’s identifier is present in the
audlist. - If not, return
401 Unauthorized.
Most JWT libraries do not perform this check by default. They verify the signature and expiration. You must explicitly enable audience validation. Refer to the golang-jwt documentation for specific implementation details on enabling audience checks.
// Go example using golang-jwt
claims, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
// Handle signature error
}
// CRITICAL: Validate audience
if !contains(claims.Audience, "my-service-id") {
return fmt.Errorf("token audience invalid")
}Interaction in Distributed Flows
In a typical microservices architecture, the flow looks like this:
- User logs in to Identity Provider (IdP).
- IdP issues a JWT with
sub(user ID in IdP),iss(IdP URL), andaud(list of allowed services). - Client sends JWT to API Gateway.
- API Gateway validates
aud. If the gateway is in the list, it passes the request. - API Gateway forwards the JWT to Backend Service.
- Backend Service validates
audagain. It must ensure it is in the list.
Some architectures use a "bouncer" pattern where the API Gateway strips the aud claim or replaces it. This is dangerous. If the Backend Service cannot verify the original audience, it loses the ability to detect token misuse. Always validate aud at every hop where the token is used for authorization.
Common Pitfalls
1. Empty aud
If the aud claim is missing or empty, some libraries accept the token. This is a misconfiguration. If your service requires audience validation, reject tokens with no aud claim.
2. Multiple Audiences A JWT can have multiple audiences. This is useful for tokens issued by a central identity provider for multiple downstream services. The validator must check if any of the listed audiences match the current service.
if (!decodedToken.aud.includes('service-a') && !decodedToken.aud.includes('service-b')) {
throw new Error('Invalid audience');
}3. Trusting sub for Access Control
Never use sub to determine permissions. sub tells you who the user is, not what they can do. Permissions should be derived from scope or custom claims, and only after sub and aud are validated.
Practical Takeaways
subis local: Always combinesubwithissto create a unique user identifier.audis a hard boundary: Reject tokens that do not list your service in the audience claim.- Validate at every hop: Ensure every service in the chain validates the
audclaim to prevent token leakage.
FAQ
Can sub be used as a primary key in my database?
No. sub is only unique within the context of its issuer (iss). You must use the tuple (iss, sub) or a mapped internal ID to ensure global uniqueness.
What happens if aud is missing from a token?
If your service requires audience validation, you should reject the token. Accepting tokens without an aud claim can expose your service to unauthorized access from tokens intended for other services.
How do I handle multiple services sharing a token?
You can include multiple values in the aud array. Your validation logic should check if any of the listed audiences match your service's identifier.
Conclusion
Secure JWT implementation relies on strict boundaries. sub is a local key that must be combined with iss for global uniqueness. aud is a hard filter that prevents token leakage between services. Treat them as cryptographic constraints, not optional metadata.
Related posts
JWT Custom Claims: Public, Private, and Custom | JWT From the Spec Up
Understand the distinctions between public, private, and custom claims in JSON Web Tokens, including IANA registry usage and namespacing best practices.
The Seven Registered Claims: iss, sub, aud, exp, nbf, iat, jti
A technical breakdown of the seven standard JWT claims: iss, sub, aud, exp, nbf, iat, and jti, explaining their roles in authentication and authorization.
JWT JTI: Replay Protection and Token Revocation
Learn how the JWT JTI claim enables effective replay protection and token revocation strategies for backend security.