Issuer, audience, and authorized party
All domains, identifiers, and tokens in these examples are fictional.
Providers and relying parties said that everything the printer trusts about the photo service hangs from one value, its issuer. From delegated access to sign-in promised that the printer would refuse an ID token issued to the collage app. Both promises are kept by comparisons that look trivial, one string against another. What matters is what the printer compares against, and how a near match can slip through.
Matching the issuer
The printer compares iss with the issuer stored in the pending attempt, https://auth.photos.example, character for character. The comparison is case-sensitive and makes no adjustments, so each of these fails:
https://auth.photos.example/ trailing slash
https://Auth.Photos.Example different case
http://auth.photos.example not https
https://auth.photos.example/test different path
Some of these would reach the same web server, and that is beside the point. An issuer identifier is a name compared as text, not an address to be tidied up before use. It is also the namespace for every sub the provider issues, so a printer that let near matches through would have to decide which of them are really the same provider, and an attacker would only need one it decided wrongly.
The path is part of the name. OpenID Connect allows several issuers on one host, told apart by path, and treats them as unrelated providers. As Providers and relying parties mentioned, services that host sign-in for many organizations often give each organization its own issuer this way:
https://id.workplace.example/org-7731 one company's issuer
https://id.workplace.example/org-9020 another company's issuer
A service like this often signs every organization's tokens with the same keys. A valid signature then shows that the service issued the token, not which organization it speaks for. Only iss says that. Now imagine a relying party that serves several business customers on this service and, to avoid configuring each one, accepts any issuer beginning with https://id.workplace.example/. It accepts tokens from every organization on the service, including one an attacker can create in a few minutes. In their own organization the attacker controls the directory and can create a user with any name and email address. If the relying party finds accounts by email address, or by sub without its issuer, that user signs in as somebody else.
The safe arrangement lists each accepted issuer exactly. An application that really does welcome any organization on such a service treats each issuer as a separate provider. It stores the issuer with every account link, and never lets a token from one organization sign in to an account linked through another. Supporting several providers returns to keeping providers apart.
Where the expected value comes from matters as much as the comparison. The printer took https://auth.photos.example from its own configuration when you selected Continue, and stored it with the attempt. It never takes the expected issuer from the token being checked, which would only compare the token with itself. At the callback, the printer compared the iss response parameter with the same stored value before it sent the code anywhere. The ID token's iss repeats that comparison on a value the provider signed. Provider discovery shows where the configured issuer and its key set come from.
Checking the audience
Moments after signing in to the printer, you also signed in to the collage app with your photo account. The photo service, which knows that app as the client photo-collage, relied on the session you had just started, asked you nothing, and issued the collage app an ID token about you. Decoded, the claims that matter here read:
{
"iss": "https://auth.photos.example",
"sub": "user-2048",
"aud": "photo-collage",
"exp": 1790845520,
"iat": 1790845220
}
Everything about it is genuine. The photo service signed it with photos-rs-2026-09, its issuer is right, it names you, and it has almost five minutes left to run. If it reached the printer, its signature would verify there too. Only aud shows that it was issued to someone else.
In the printer's website sign-in, a token issued to another client has no easy way in. The printer validates only tokens from its own token requests, and the photo service redeems only codes that were issued to the printer. ID tokens travel other routes, though.
From delegated access to sign-in imagined the printer's phone app running the flow itself and passing what it received to the printer's backend. With OpenID Connect, the app passes its ID token, and the backend checks that the token is addressed to the phone app, a client the printing company registered itself. If the collage app's operator, or anyone who copied a token from the collage app, sent the backend this token instead, the audience check is what refuses it. Older flows that return ID tokens through the browser, covered in Understanding implicit and hybrid integrations, offer still more routes. The audience check is the one that works on all of them.
The claim comes in two forms. With one audience it is usually a plain string. In general it is an array:
"aud": "photo-printer"
"aud": ["photo-printer"]
"aud": ["photo-printer", "photo-collage"]
The printer accepts the first two, which say the same thing. The third contains photo-printer, but the specification requires the printer to reject it anyway, because it lists an audience the printer does not trust. Every party named in aud was meant to receive the same token, and any of them could present it to another. A token addressed to both could come from the collage app as easily as from the printer's own exchange, and nothing in it would show which. An extra audience is acceptable only when the printer has a reason to trust it, such as another registration of its own, and then the printer lists that value in its configuration rather than accepting whatever appears.
The client ID is compared exactly, like the issuer. Photo-Printer is a different client.