skip to content

In OpenID Connect, why must a UserInfo response's `sub` be compared with the ID token's `sub` before use?

level: middleimportance: must knowfreq 55%

answer

  1. two statements, one person, maybe not
  2. the credential may not be theirs
  3. exact string comparison, no normalisation
  4. mismatch discards the whole response
  5. substitution, not expiry

basics

~20 s

Because the access token used to call UserInfo may have been issued for a different end user. The subject values must match exactly, and if they do not, none of the response values may be used at all.

solid answer

~40 s

The UserInfo endpoint answers for whoever the presented access token was issued for — it does not know which sign-in the calling relying party has in hand. So two subjects meet in one place: the `sub` the ID token asserted about the person who authenticated, and the `sub` in the response. The specification requires them to match exactly; if they do not, the response values must not be used. The failure it stops is **substitution**: an access token belonging to another end user gets attributed to the authenticated one, and the relying party writes somebody else's attributes onto that account. Nothing in TLS or in the bearer credential detects this — it is a comparison the relying party writes itself, and a missing four-line check is exactly how it goes wrong.

code

pseudocode · 9 lines
pseudocode
function useUserInfo(idTokenClaims, userInfoBody):
    if userInfoBody has no member "sub":
        reject "no subject to compare - unusable"

    if userInfoBody.sub is not exactly equal to idTokenClaims.sub:
        reject "subject mismatch - discard every value in the body"

    # only now may any member be read, merged or cached
    return merge(idTokenClaims, userInfoBody)

go deeper

for a junior

The takeaway to carry: the endpoint answers for whoever the presented credential belongs to, so the relying party checks that this is the same person who just signed in.

for a middle

Be able to state the rule and the reason in one breath — exact match against the sign-in statement's subject, discard everything on mismatch, because the credential and the sign-in arrive on different paths.

for a senior

Show where the check lives in a real codebase and how you stop it being skipped: one chokepoint that returns matched claims or throws, plus a log line and an alert when a mismatch actually fires.

for a principal

The wider judgment is which invariants must be unskippable across every integration your organisation writes, and whether that is achieved by review, by a shared client, or by making the unchecked path impossible to express.

A relying party that has just completed a sign-in holds an ID token: a signed statement naming, in its `sub` member, the person who authenticated. It then calls the UserInfo endpoint with the access token from the same exchange and gets back a JSON object that also carries `sub`. The rule is short and absolute: those two values must match exactly, and if they do not, nothing in the response may be used. ## Why the two values can ever differ The endpoint's answer is determined entirely by the credential presented to it. It resolves the grant behind that access token, finds the end user that grant belongs to, and answers about that person. It has no idea which sign-in the caller is currently processing, because the caller never tells it. So the pairing of "the person who just authenticated here" with "the person this access token was issued for" is an assumption the relying party makes, not a fact the protocol carries. In a well-behaved flow the assumption holds. The comparison exists for the flows where it does not. ## The substitution, step by step 1. An attacker obtains an access token issued for **their own** account at the same provider — no compromise required, it is their account. 2. They get the relying party to use that credential in the context of a session that was authenticated as **somebody else**, or they get their own authentication response to be processed while a victim's artefact is in play. 3. The relying party calls UserInfo, receives the attacker's claims, and merges them into the authenticated account — or, worse, keys its local user record on the `sub` that came back. 4. Two identities are now crossed. Which way the damage runs depends on which value the relying party trusted afterwards, and both directions are bad: the attacker's attributes land on the victim's account, or the victim's session becomes reachable through the attacker's subject. The comparison collapses the whole family: if the values disagree, the relying party knows the credential and the sign-in are not about the same person, and refuses. ## Writing the check - Compare the two values as an **exact string comparison**. There is no normalisation step, no case folding, no trimming — a subject identifier is an opaque string chosen by the provider. - Compare against the `sub` from the **ID token of this sign-in**, not against whatever the relying party last stored for the session. - On mismatch, **discard the entire response**. There is no partial-trust path: you may not keep the members that look plausible, because a response that is about the wrong person is wrong in every member, `sub` included. - Treat a missing `sub` as a mismatch. Without it there is nothing to compare, so there is nothing to trust. - Run the comparison before any merge, cache write or rendering — a value that reaches storage before the check has already escaped it. ## What the check does not do This is one specific safety property, and answers that oversell it are as wrong as answers that omit it: | the check does | the check does not | |---|---| | tie the response to the sign-in you are processing | say anything about whether the access token is still valid | | stop attributes being attributed to the wrong account | verify the response's origin or integrity by itself | | work on a plain JSON response as well as a token-shaped one | replace validating the ID token that produced the `sub` you compare against | The authenticity of a plain JSON response comes from the TLS connection to the provider's own endpoint; the subject comparison is about *who*, not about *from whom*. ## Why interviewers reach for it It is the one rule on this endpoint that a candidate cannot have absorbed by copying a working integration, because an integration that omits it works perfectly every single day until someone attacks it. Asking for it tests whether the candidate understands that the two artefacts arrive on different paths for different reasons, and that the only thing binding them together is a comparison somebody has to write.

  • The response is a signed token and its signature verifies. Does that remove the need for the comparison?
    No. A signature proves the provider produced the body and that nobody altered it in transit. It says nothing about which end user the presented access token was issued for — a genuine, correctly signed response about the wrong person verifies perfectly. Origin and subject are separate properties, and only the comparison settles the second.
  • Where in a relying party's code should the comparison live?
    In the one place that turns a UserInfo body into claims the rest of the application can see, before any merge, cache write or render. Scattering it across call sites guarantees one site eventually forgets it; a single gateway function that either returns matched claims or throws is the shape that survives maintenance.
  • Two members of the response look obviously correct for the signed-in user, but `sub` does not match. May the relying party keep those two?
    No. A mismatch means the body describes a different end user, so every member in it is suspect — including the ones that happen to resemble what you expected. Partial acceptance reintroduces the whole attack with extra steps. Discard the body and treat the call as failed.

saying these in an interview costs you the question

  • Thinks TLS to the provider already guarantees the response is about this user.
  • Keeps the plausible-looking members and drops only the mismatched subject.
  • Believes a verified signature makes the comparison unnecessary.
  • Explains a mismatch as the access token having expired.
  • Keys the local account record on the subject the response returned.
  • Normalises or lowercases both values before comparing them.