Pairwise subjects: giving every app a different you

When you sign in to an app through an OpenID Connect provider, the app receives an ID token whose sub claim identifies you. Most providers issue the same sub for you everywhere — one global identifier, often sitting next to an email claim. That is convenient, and it is also the mechanism by which two unrelated apps (or anyone who obtains both of their databases) can discover they share a user.

ChirpAuth issues pairwise subjects instead: every app sees a different, unrelated identifier for the same person. This post explains how the derivation works, what it prevents, what it doesn't, and what the issuer itself can see.

The derivation

Every ChirpAuth account is keyed by a root id minted at creation: usr_ followed by a random UUID — 122 bits of randomness, generated once, never derived from anything about you. This root id never leaves our servers. It does not appear in tokens, URLs, or emails.

When you sign in to an app, the sub in its ID token is computed as:

sub = "sub_" + base64url( SHA-256( client_id + ":" + root_id ) )

where client_id is the app's identifier (assigned at app registration) and root_id is your UUID root. The hash is one-way: an app holding your sub would have to guess a 122-bit random UUID to recover the root behind it.

Three properties follow directly:

Why email is not the key

Most identity systems quietly treat the email address as the identity. We don't, for two reasons.

First, emails are identifying by nature — handing one to every app would defeat the pairwise scheme in one move. So apps never receive it: there is no email scope, and the ID token carries no email claim. The only scope is openid.

Second, emails change. In Chirp Zero (magic-link sign-in), the email is the authentication factor — an index maps it 1:1 to your root so a magic link knows which account to sign in. But because subjects derive from the UUID and not the address, changing your email is just re-verifying a new address: the root and every per-app sub are untouched, and every app you use still recognizes you. In Chirp One (passkey sign-in), email has no authentication role at all, and each persona presents its own pairwise sub per app.

A worked example

Jane creates a Chirp Zero account. At creation she is assigned the root:

usr_a3f7c891b4e84d2c9f6012345678901a

She signs in to a recipe app registered as cs_prod_9b2e44d1c0f04a7e8d3a55667788990b. Its ID token says:

sub = sub_sFbXFERgjIb9ThDLaxXt7uqkG_Xd7nz_ikaZrJz98oQ

She signs in to a forum registered as cs_prod_51c6aa0eb7d2401fa9e0112233445566. Its ID token says:

sub = sub_1AAzOduIYEYVsrd_a5CuskEmAYxO5TNNJfoRd0W_vVI

(Those are the actual SHA-256 outputs for those inputs — you can verify them in a few lines of any language.)

The recipe app and the forum each have a stable identifier for Jane. Put their user tables side by side and there is no join key: no shared sub, no email column, nothing issued by us that matches. If Jane later changes her email, both sub values above stay exactly the same.

What this prevents

What this does not prevent

Pairwise subjects remove the identifier we issue. They cannot remove identifiers users hand over themselves. If Jane types the same email into both apps' profile forms, picks the same username, or pays both with the same card, the apps can correlate her on that data. Apps can also embed third-party trackers, log IP addresses, or fingerprint browsers — all outside the token and outside our control. Pairwise subjects close one correlation channel, cleanly and by construction. They are not an anonymity system.

What the issuer sees, and keeps

Honesty about the trust model: pairwise subjects protect you from apps, not from the issuer. ChirpAuth holds the root id and can compute any app's sub from it — that is what makes sign-in work, and it is also how account deletion and consent revocation find your records.

What the hosted issuer retains while your account exists: the root, the email-to-root index (Chirp Zero), your passkey public keys (Chirp One), your active sessions, and your consent list — which apps you've approved, kept until you revoke them. Operational security events and diagnostic traces are kept briefly and then expire; there is no durable per-user history of sign-ins, and no analytics. The exact retention windows and the full stored-data inventory, with example rows, live on the privacy page.

If "trust the issuer" is not a sentence you want to rely on, the code is AGPL-3.0 and self-hostable — Lambda, DynamoDB, and SES via Terraform in your own AWS account. The derivation above then runs on your hardware, and the party that can compute your subjects is you.

Apps integrate ChirpAuth once, as a standard OIDC provider, and get pairwise subjects without doing anything: it is the only subject format we issue, on every tier, in both products.