#PacketHunters – That JWT was valid, I still shouldn’t have trusted it? Notes on audience confusion, microservices, and how identity quietly erodes

Ahmmm.. there’s a moment every backend developer hits at some point in their career: you’re deep into an authentication middleware, everything looks clean, the token validates correctly, the signature checks out, the issuer is trusted, the expiration is fine. You move on, confident that identity is “handled”.
Years later, you realize that moment is exactly where things started to go wrong.

I’ve worked with JWT-based identity systems long enough to stop being impressed by tokens that validate correctly – cryptographic validity is table stakes. The real question is whether the token actually makes sense here, in this service, for this action, at this time. And that’s where many modern systems quietly fail.

Audience confusion is not an exotic vulnerability, it’s the natural outcome of treating JWTs as portable identity objects instead of scoped assertions with intent.

The architecture that looks fine on paper

Most of the environments where I’ve seen this happen look perfectly reasonable. There’s an identity provider issuing OAuth or OIDC tokens, an API gateway doing centralized authentication, and a set of microservices behind it.
The gateway validates tokens, forwards requests, and downstream services assume that if traffic made it that far, identity is already settled.

That assumption is comfortable, and it’s fucking wrong!
Downstream services often perform only superficial checks, if any: they decode the token, verify the signature, maybe check expiration, and move on. Audience validation is skipped because “the gateway already did that”, issuer validation is loosened because environments multiply, and scope enforcement becomes vague because mapping scopes to actions takes time.

At that point, identity stops being contextual and becomes ambient. A token is trusted because it exists. Ez!

Where identity boundaries actually collapse

JWTs are designed to be reusable, but reuse without boundaries is the problem. When multiple services accept tokens signed by the same issuer and don’t strictly enforce the intended audience, a token issued for one service can often be replayed against another without resistance.

Nothing breaks actually: no alerts fire, logs look clean.
From the system’s perspective, a valid identity is accessing an endpoint it technically has access to.
From a security perspective, an identity boundary has been crossed without authorization.

Now the “been there, seen things” parts: I’ve seen user-facing tokens accepted by internal services simply because nobody expected them to be sent there. I’ve seen read-only scopes interpreted loosely enough to allow write operations. I’ve seen staging tokens accepted in production because issuer validation was relaxed for convenience.
In all of these cases, the crypto worked exactly as designed. The failure was semantic. SE-MAN-TIC.

Why this keeps slipping through reviews and reviews and reviews

Developers trust what they can verify mechanically: signatures, expirations, algorithms – these are deterministic and testable. Audience and intent are contextual, and context is harder to reason about when systems grow.

Let’s talk about microservices. Microservices amplify this problem: each service owns a small slice of logic, but identity assumptions often span the entire ecosystem. When enforcement is centralized but usage is decentralized, nobody feels responsible for validating intent locally.

JWT libraries don’t help much here.
Many make it trivial to disable audience checks, and examples online often encourage doing so to “get things working”: once a pattern is in place and traffic flows, it rarely gets revisited.
..until something odd shows up in an audit, or worse, during an incident.

Recovery is where the real cost appears

What makes audience confusion particularly painful is recovery, not detection.
When you discover that tokens were accepted where they shouldn’t have been, you immediately lose confidence in your historical data. You can’t easily answer which requests were legitimate, which actions were properly authorized or which identities operated outside their intended scope.

Revoking tokens helps (a bit) going forward, but it doesn’t reconstruct intent retroactively.
Identity logs tell you who authenticated, not whether they should have been there and that ambiguity is expensive, both technically and organizationally.

And this is how I think about JWTs now (TLDR 🤌🏻)

At this point, I treat JWTs as untrusted input that happens to be signed. Every service that consumes a token should explicitly decide whether that token belongs there. Audience, issuer, scope, and context should be enforced as close to the resource as possible, not assumed based on network position or architectural diagrams.
This does make systems stricter and sometimes less convenient. It also makes identity failures localized instead of systemic, which is exactly what you want when recovery matters.

In the end.. stick to the old-but-gold IT rule: trust nothing.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top