ASVS 5.0 V6.8.1 says that if your application supports more than one identity provider, a user must not be able to spoof someone else through a different provider, for example by presenting the same user identifier. The standard fix is to identify users by the pair of IdP ID and the user's ID at that IdP. It is a Level 2 requirement.
Why it matters
Suppose you accept Google and a company SAML IdP, and you match accounts by email. An attacker who controls a weak IdP, or registers at one that does not verify email, can assert ceo@example.com and be treated as the CEO. This is account takeover through the front door.
How to implement it
Make the unique key two columns, and never look users up by email alone for federated sign in.
CREATE TABLE federated_identities (
user_id uuid NOT NULL REFERENCES users(id),
idp_id text NOT NULL, -- issuer or configured provider ID
idp_subject text NOT NULL, -- the sub claim or NameID at that IdP
PRIMARY KEY (idp_id, idp_subject)
);
def resolve_user(claims, idp_id):
row = db.federated_identities.get(idp_id=idp_id, idp_subject=claims["sub"])
if row:
return row.user_id
# first login: create a new user, or link only after a separate proof
return create_user_and_link(idp_id, claims)
Linking an existing local account to a new IdP should require the user to authenticate to the existing account first. Do not auto merge on matching email. Use the validated issuer from configuration as the idp_id, not a value taken from the request.
How to test it
- Configure two test IdPs. Have both assert the same
suband email. Confirm two separate application users result. - Assert an existing user's email from the second IdP and confirm no takeover.
- Check that the
idp_idcomes from your config.
Common mistakes
- Matching on email or username across providers.
- Trusting an unverified email claim.
- Using a client supplied issuer string.
Quick check
Is sub alone enough as a key?
No. Two providers can issue the same sub. The IdP ID must be part of the key.