Skip to content

Build1 publisher3 min readPublished

The /userinfo fallback that quietly made Auth0 a hard dependency on every request

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

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened

  • 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.
  • Most tutorials encourage using the access token for authorization (verify signature, check scopes) and the ID token for user identity, which is fine for a SPA but awkward for a Spring backend that only sees a single bearer token in the Authorization header and does not know which it is.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

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.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories