Your site says who it is on its own domain, adds the button, and receives a verified OpenID Connect ID token. No client registration, no client secret. In a hurry? The quick start has the whole thing in three copyable steps.
Step 1
Say where sign-ins may return, at /.well-known/id
List the exact addresses wellknown.id may send users back to, at https://your.site/.well-known/id:
Your client_id is simply your domain, your.site. Only you can publish at your domain, so nobody else can claim it, and every return address must be on it or its subdomains. The names follow OAuth’s client metadata (RFC 7591). The same config can also be published in DNS, CBOR-encoded in a _wellknown-id TXT record (wellknownid=cbor:…); if both exist, DNS wins.
It uses your page’s hostname as the client_id; add ?clientId=your.site when a page on a subdomain uses the config on the parent domain. It picks the best way to sign in: the browser’s own FedCM dialog where available, otherwise a pop-up, falling back to a full-page visit if pop-ups are blocked. Every sign-in is a click: through FedCM, the browser’s dialog is followed by a small wellknown.id window, where your user’s device chooses which of their personas signs in and proves their key, so there is no silent sign-in. If the browser didn’t open that window, the failure’s error starts with try_again: ask them to click again. By default users come back to the current page (origin and path), which must be one of your redirect_uris. Set redirectUri to choose another.
The SDK has already redeemed the code with PKCE and checked the token in the browser. Your server must verify it again before trusting it:
signature: EdDSA, against the keys at https://wellknown.id/jwks
iss is https://wellknown.id, aud is your domain, and exp is in the future
sub is the user’s did:key. Use it as their stable identifier.
One person, two keys
When a user reaches you with another did:key
A user may have several wellknown.ids, called personas (work and home, say). Each persona gives one did:key per site, the same on every device that holds it, so two personas are two accounts at your site, and nothing tells you they belong together. Which persona signs is chosen on the user's device: the one used at your site before, unless they pick another. Only the user can join two, at your site, in two ways (and they may have ended up with two by accident, too: a browser that made its own before they brought theirs in):
A succession statement. When they sign in with the new one, they can choose to tell you it succeeds the earlier one. The ID token then carries wellknown_succession: a compact JWS (ES256, typ: wellknown-succession+jwt) signed by the earlier did:key, saying { iss: earlier, sub: new, aud: your domain, iat }. wellknown.id has checked it; check it again (its kid is a did:key, which carries its public key), then move the earlier account to the new sub, or ask your user.
Linking, keeping both. Call WellknownId.linkKey() from a click while they're signed in. It runs a second sign-in, in which they pick their other wellknown.id, and resolves with its result (also sent as wellknown-id-link-success). Link that sub to the account they're signed in to.
wellknown.id keeps no record of either: which keys belong together is between your user and you.
Have a server?
Redeem on your backend, with your own key
Publish a public key in your config and ask for client authentication:
Add backend=/your/redeem/path to the script tag, and the SDK sends the code there instead of redeeming it in the browser. Your server redeems it at /token with a client assertion (RFC 7523): a JWT it signs with that key, with iss and sub set to your domain, aud set to https://wellknown.id, a unique jti and an exp within five minutes. Codes for your site then can’t be redeemed by anyone without your key.
Optional
Take the user’s own token, and verify it yourself
Instead of a code and wellknown.id’s ID token, your page can take the token your user’s wellknown.id signs, with their key for your site: then wellknown.id is sent nothing naming that key. Allow it in your config entry, and ask for it in the script tag:
It’s forbid unless you say so, because standard OpenID Connect libraries can’t verify these tokens and wellknown.id can’t enforce your policy on them: you do both. The SDK checks the token in the page and hands you self_issued (the token), sub and verifier. It’s the same token whether your user signs in in the browser or with kivi on their phone (the code in the login button, or “Use my phone”): kivi signs it, and says so in amr (wallet). Your server checks it again, with that verifier: the request’s nonce was made from it, so a token from someone else’s sign-in won’t pass.
import { verifySelfIssued } from'@wellknown-id/wallet/self-issued';
// POST { token, verifier } from your pageconst claims = await verifySelfIssued(token, { aud: 'your.site', verifier });
// claims.sub: the user's did:key at your site. Take each token once (keep claims.nonce until a minute past claims.exp)
It checks the signature against the did:key the token names (typwellknown-self-issued+jwt, ES256 or EdDSA), iss = sub, aud, the nonce, and a lifetime of at most 120 seconds, with nothing but WebCrypto: Node 20+, Deno, Workers and browsers. The token has SIOPv2’s self-issued ID token claims. The SDKs aren’t published yet: until they are, verifySelfIssued is one file with no dependencies (self-issued.ts), which we’ll send you: write to hello@wellknown.id. A static site with no server can rely on the SDK’s check in the page, for what a page alone decides.
Prefer your own stack?
It’s standard OpenID Connect
Discovery is at https://wellknown.id/.well-known/openid-configuration. Use the authorization code flow with PKCE (S256) as a public client, with your domain as the client_id, scope=openid, a nonce, and a redirect_uri from your config. Only the code flow is supported.
Optional
Say what you require, in karu
Add a policy to your config entry (or at the top level, for every entry on your domain), written in karu, a small policy language people can read. wellknown.id checks it at every sign-in, after the key proof and before it issues a code:
This one takes only P-256 keys, the kind a phone can keep in its secure hardware. Every sign-in proves the user’s key afresh, so context.auth_age_seconds is always 0. Only deny rules count: wellknown.id supplies the allow, and your policy can only narrow it. A turned-down sign-in reaches you as the error access_denied, naming the rules, in wellknown-id-login-failure’s error (access_denied: denied by policy: not_p256). A policy that doesn’t compile refuses every sign-in as invalid_client.
Rules can use principal.id and principal.key_type (P-256 or Ed25519); resource.client_id, redirect_uri and scopes; and context.mode (navigate, popup, popin, fedcm or wallet), config_source (dns or http) and auth_age_seconds.
wellknown.id is in development. Endpoints and formats may still change before launch.