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:
- Stable per app. The derivation is deterministic, so the same person
signing in to the same app always gets the same
sub. Apps get a normal, stable user identifier; nothing about their integration changes. - Unlinkable across apps. A different
client_idproduces an unrelated hash. There is no computation an app can do on itssubvalues to find matches in another app'ssubvalues. - Stable across email changes. The email is not an input to the hash.
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
- Apps joining on
sub. Two apps — or a data broker who buys exports from both, or an attacker who breaches both — cannot link accounts through the identifier we issue. - The issuer's identifier becoming a tracking key. There is no ChirpAuth-issued ID that follows you across the web the way a "Sign in with..." global identifier does.
- Email harvesting via sign-in. Integrating ChirpAuth gets an app a user, not an email address.
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.