Safeguard
Application Security•

ASVS 5.0 V10.5.2: Identify Users by the sub Claim, Not Email

Key your user records on the issuer plus sub claim. Emails and usernames can be reassigned, which can hand one person's account to another.

Safeguard Security Team
3 min read

ASVS 5.0 V10.5.2 (Level 2) requires the OIDC client to identify the user from ID Token claims that cannot be reassigned to someone else, normally sub. The identifier is unique only within one identity provider, so the stable key is the pair of issuer and subject.

Why it matters

Emails get recycled when employees leave and domains change hands. A user who signs in as old.name@example.com today might not be the person who held that address last year. If your app joins accounts on email, the new holder inherits the old account and its data.

Email can also be unverified or controlled by the user at some providers, which opens account takeover through a crafted profile.

How to implement it

Store the pair, and look users up on it.

CREATE TABLE identities (
  issuer   text NOT NULL,
  subject  text NOT NULL,
  user_id  uuid NOT NULL REFERENCES users(id),
  PRIMARY KEY (issuer, subject)
);
def find_user(claims):
    return db.query_one(
        "SELECT user_id FROM identities WHERE issuer = %s AND subject = %s",
        (claims["iss"], claims["sub"]))

Treat email as a contact attribute. If you ever link accounts by email, require email_verified to be true and a second confirmation step.

First login and account creation

On the first login for a pair of issuer and subject, you either create a user or link to an existing one. Creating is the simple case. Linking is where account takeover happens, so treat it as a sensitive action. Require the user to prove control of the existing account, for example by signing in to it, before attaching the new identity.

def on_login(claims):
    row = find_identity(claims["iss"], claims["sub"])
    if row:
        return row.user_id
    user = create_user(display_name=claims.get("name"))
    insert_identity(claims["iss"], claims["sub"], user.id)
    return user.id

Notice that the code does not look at email at all. If your product needs email for contact, store it as an editable attribute and verify it by sending a message.

Multiple providers and migrations

If you support several providers, the composite key keeps them apart. If you migrate from one provider to another, keep the old mapping and add a new one through a controlled linking step, instead of matching on email. Some providers offer a migration claim for this purpose, and you should prefer it.

Pairwise identifiers

Some providers issue a different sub per client, called pairwise subject identifiers. That is fine, because the value is still stable for your client. Just never compare it with values from another application.

How to test it

Create a user, then have a different identity provider account present the same email with a new sub. Confirm you do not log in as the first user. Change a user's email at the provider and confirm they keep their account.

Data cleanup

When a user leaves the provider or you remove their account, delete the identity rows too. Orphaned mappings are a quiet way for a recycled identifier to land on an old record.

Common mistakes

  • Primary key on email.
  • Using sub alone when you accept several issuers.
  • Trusting an unverified email for linking.

Quick check

Why combine issuer and sub?

A sub is only unique within its issuer, so two providers can issue the same value for different people.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.