Build1 distinct publisher3 min readUpdated
Auth0 access tokens ship without an email claim. One team's fallback put a synchronous identity-provider call inside a per-request filter, and the cache in front of it hid the coupling rather than removing it.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A Spring team discovered that its per-request user-sync filter was falling back to a synchronous HTTP call to Auth0's `/userinfo` endpoint whenever the access token arrived without an `email` claim, according to a writeup published on dev.to and originally on the Jo4 blog [1] [2]. That is not a missing-field bug; it is an architecture decision made by accident, because it puts the identity provider in the serving path of every API call to your own service [13].
Start with the default. An Auth0 access token is minimal by design: issuer, subject, audience, issued-at, expiry, scope [4]. What is not in it is `email`, `name`, `picture`, or `email_verified`, which live in the ID token by default or come back from a separate `/userinfo` call [5]. The usual advice, use the access token for authorization and the ID token for identity, works for a single-page app and is awkward for a backend that sees one bearer token in an `Authorization` header and cannot tell which kind it is [6].
So the team took the tempting shortcut: read `email` from the access token because some tenants are configured to inline it, log a warning when it is blank, and fetch the rest of the profile from `/userinfo` [7]. The fallback was wrapped in a Redis cache and a synchronous lock, so on a hot path most requests hit cache and skipped the HTTP call [8].
Read the miss conditions carefully, because they are the whole story: cold start, after deploys, after Redis restarts, after cache evictions [9]. Those are exactly the moments when traffic is being reintroduced and you have the least headroom. New users missed by definition [10]. And the mobile app's startup fan-out, three to five parallel requests, thundered against the lock, with at least one of them doing the real fetch while the rest waited [11]. Measured, a single user could trigger several `/userinfo` calls per session [12]. Multiplied across the user base and the rate of token rotation, the team's own availability was pinned to Auth0 staying fast and up [13].
The fix is to stop needing the call at request time. Auth0's Post-Login Actions run once per login, on Auth0's side, and can mutate the access token before it is issued [14]. The Action sets custom claims for `email`, `email_verified`, `given_name`, `family_name`, and `picture` [15]. That moves the identity-provider call from once per cache miss per request to once per login [1].
Two details will cost you an afternoon if you miss them. Auth0 silently drops a custom claim named `email` because it collides with the OIDC reserved name; custom claims must be namespaced URIs, and the namespace is symbolic, since nothing fetches it [16] [17]. The backend must then match that namespace exactly, which the team handled by making each claim key a configurable Spring property with a default, so the Action and the filter cannot drift [18]. Once noticed, the whole change took an afternoon [19].
Two things to watch. Claims baked in at login are fixed for the life of the token, so a profile change will not be visible until a new one is issued [2]. And the old fallback path was already logging a warning before anyone acted on it [7]; if you keep it as a safety net, alarm on that log line rather than trusting yourself to read it.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
When measured, a single user could trigger several /userinfo calls per session.
Multiplied across the user base and the rate of token rotations, the pattern amounted to a non-trivial dependency on Auth0 staying fast and available for every API request to the team's own service.
A Spring backend team using Auth0 as an OIDC provider found that when their user-sync filter found no email claim in the token, the default behavior was to fall back to a synchronous HTTP call to Auth0's /userinfo endpoint, per request, with caching but not enough caching.
The account was published on dev.to under the headline 'We Were Hitting Auth0 /userinfo on Every Request. Here's the Fix.' and was originally published on the Jo4 Blog.
Auth0 access tokens are minimal by design; out of the box the claims are approximately iss, sub, aud, iat, exp and scope.
email, name, picture and email_verified are not in the default access token; they live in the ID token by default or come back from a separate /userinfo call.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Detailed but single-source and unquantified
The account is unusually specific for a self-report: decoded token payload, the offending filter code, the Action JavaScript, the @Value claim keys and the rollout escape hatch are all published, which makes the mechanism verifiable by inspection. But there is exactly one source, it is first-party and syndicated from the author's own blog, the vendor-behavior claims (minimal access tokens, stripped non-namespaced claims, Action semantics) are corroborated by nothing else in the cluster, and the impact claim carries no numbers at all.
One self-reported team migration
The only adoption fact in the cluster is this one team moving its own backend from a request-time /userinfo fallback to Action-injected namespaced claims. There is no evidence of the pattern spreading, no reader or vendor uptake signal, no repository, template or product release, and no second organization described.
Headline overstates its own body
Mildly overstated. The headline says /userinfo was being hit 'on Every Request', while the body states that a Redis cache plus sync lock meant most hot-path requests skipped the call and that misses clustered around cold starts, deploys, Redis restarts, evictions, new users and mobile fan-out. 'Non-trivial dependency' is asserted without a single figure. Offsetting the overstatement, the prescriptive part is modest and well-scoped, and the piece volunteers the rollout hazard of a strict no-fallback filter rather than hiding it.
Self-promotional syndication, no vendor stake
Low to moderate. The post is a first-party engineering narrative syndicated from the author's own Jo4 Blog, whose domain also supplies the claim namespace in the examples, so there is a personal-brand and traffic incentive to frame the story as a dramatic catch and clean fix. There is no evidence of vendor sponsorship, product sale or funding interest, and the piece publishes its own mistake, which cuts against pure promotion.
Mechanism credible, magnitude unverified
Confidence is moderate for the mechanics and low for the impact. The code-level pattern and the namespacing requirement are specific enough to be checked by any reader against their own tenant, so the practitioner takeaway is reliable. The claims about how widespread or costly the coupling was rest on one unquantified self-report from a single publisher, with no independent corroboration or metrics in the cluster.
build
Three services you can delete: queue, cache and search in one Postgres1 distinct publisher
build
Your JWT Login Probably Has Exactly One Kill Switch: Log Everyone Out1 distinct publisher
build
The reason your agent gets worse after an hour is that nothing ever leaves the context window1 distinct publisher
build
Valid JSON, Wrong Bucket: Why A Model Answer Is A Proposal, Not A Result1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 20, 2026