ASVS 5.0 V10.3.3 requires that when an access decision needs to identify a unique user from a JWT or introspection response, the resource server uses claims that cannot be reassigned to another user. In practice that is the combination of iss and sub. It is a Level 2 requirement.
Why it matters
Emails, usernames and phone numbers get recycled. A person leaves, their address is reissued, and the new owner signs in. If your user table is keyed on email, the new person inherits the old person's account and data. With several identity providers, the same sub value can also appear under different issuers for different people.
How to implement it
Store the pair as the external identity and link it to your internal user ID.
CREATE TABLE identities (
issuer text NOT NULL,
subject text NOT NULL,
user_id uuid NOT NULL REFERENCES users(id),
PRIMARY KEY (issuer, subject)
);
const row = await db.oneOrNone(
"SELECT user_id FROM identities WHERE issuer = $1 AND subject = $2",
[claims.iss, claims.sub]);
Treat email and preferred_username as display data. If you must link by email (for example on first sign in), require a verified email from an issuer you trust and ask the user to confirm.
How to test it
Create two users at different issuers that share a sub string and confirm they map to different accounts. Change a user's email at the identity provider and confirm they keep their account and a new person using the old email does not get it.
Common mistakes
- Using
emailas a primary key or login match. - Using
subalone in a multi-issuer setup. - Trusting
email_verifiedfrom any issuer.
Quick check
Why is sub alone risky with several issuers?
Each issuer assigns its own subjects, so equal values from different issuers can belong to different people.