<wellknown-id-button></wellknown-id-button> <script src="https://wellknown.id/sdk/web.js" defer></script>
The script is served by wellknown.id, built ahead of time and listed by hash in each release's signed manifest (checking what wellknown.id serves). It's readable from any origin, so you may load it with an integrity attribute of your own. Its types are in the API reference: Web SDK (web.js).
Options
The script's URL takes these as query parameters, and the element takes mode and client-id as attributes, over the script's:
| Parameter | Default | |
|---|---|---|
clientId | the page's hostname | your domain; set it when a page on a subdomain uses the config on the parent domain |
mode | default | default (FedCM where the browser has it, otherwise a pop-up, otherwise a full-page visit), fedcm, popup, popin (a frame over your page) or navigate |
redirectUri | this page (origin and path) | where sign-ins come back to: one of your redirect_uris |
backend | none: the browser redeems the code | your endpoint that redeems codes, on this origin (below) |
selfIssued | off | 1: take the person's self-issued token yourself, where your config allows it (below) |
target | none: the element | the id of an element to put the button in |
Every sign-in is a click. Through FedCM, the browser's dialog is followed by a small wellknown.id window, where the person's device chooses which of their personas signs in and proves their key; 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.
Events
The script dispatches these on window:
| Event | detail |
|---|---|
wellknown-id-login-success | { sub, claims, id_token, access_token, mode }, or with a self-issued token, { sub, claims, self_issued, verifier, mode } |
wellknown-id-login-failure | { error, mode }: a policy's refusal is access_denied: denied by policy: <rules> |
wellknown-id-link-success | as a success, for the second sign-in linkKey() runs |
window.WellknownId.linkKey() links another wellknown.id to the signed-in account: see getting started.
Redeeming on your backend
By default the SDK redeems the code in the browser, with PKCE. To redeem it on your server with your own key, publish a public key in your config entry and ask for client authentication:
{
"redirect_uris": ["https://your.site/"],
"jwks": { "keys": [{ "kty": "OKP", "crv": "Ed25519", "x": "…" }] },
"token_endpoint_auth_method": "private_key_jwt"
}
Add backend=/your/redeem/path to the script's URL, and the SDK sends the code and its PKCE verifier there instead. Your server redeems it at https://wellknown.id/token with a client assertion (RFC 7523): a JWT it signs with that key, with iss and sub your domain, aud 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.
Taking the self-issued token
Instead of a code and wellknown.id's ID token, your page can take the token the person's own device 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's URL:
{
"redirect_uris": ["https://your.site/"],
"self_issued": "allow"
}
<script src="https://wellknown.id/sdk/web.js?selfIssued=1" defer></script>
It's forbid unless you say so, because stock OpenID Connect libraries can't verify these tokens, and wellknown.id can't enforce your policy on a sign-in it never sees: you do both. The SDK checks the token in the page and hands you self_issued (the token), sub and verifier. Your server checks it again with that verifier, since 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';
const taken = new Map(); // nonce → exp: each token is taken once
app.post('/session/direct', express.json(), async (req, res) => {
try {
const { token, verifier } = req.body;
const claims = await verifySelfIssued(token, { aud: 'your.site', verifier });
const now = Date.now() / 1000;
for (const [nonce, exp] of taken) if (exp + 60 < now) taken.delete(nonce);
if (taken.has(claims.nonce)) throw new Error('this token was used already');
taken.set(claims.nonce, claims.exp);
res.json({ sub: claims.sub }); // the person's did:key at your site, as in an ID token
} catch {
res.status(401).json({ error: 'not signed in' });
}
});
verifySelfIssued checks the signature against the did:key the token names (typ wellknown-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 and later, Deno, Workers and browsers. It's one file with no dependencies (its reference); the packages aren't on npm yet, so write to hello@wellknown.id for it. It's the same token whether the person signs in in the browser or with kivi on their phone, which says so in amr (wallet).