A file on your domain, a script tag, and a few lines on your server. No registration and no client secret. Replace your.site with your domain throughout. The developer notes explain each step in full, and the documentation has every option and the API reference.
Step 1
Publish /.well-known/id
Serve this as JSON at https://your.site/.well-known/id. It lists the exact addresses sign-ins may return to. Your client_id is your domain.
That checks the signature against wellknown.id’s published keys, that iss is https://wellknown.id, that aud is your domain, and that the token hasn’t expired. Any other OpenID Connect or JOSE library does the same: discovery is at https://wellknown.id/.well-known/openid-configuration.
That’s a working sign-in. The sub is the same every time this person signs in to your site, and different at every other site.
Optional
Take the user’s own token, so wellknown.id never sees their key
Instead of wellknown.id’s ID token, your page can take the token your user’s device signs with their key for your site. Then wellknown.id is sent nothing naming that key. Allow it in your config entry:
On your server, verify it with verifySelfIssued. Stock OpenID Connect libraries can’t: the token is signed by the user’s own key, not by wellknown.id. Our SDKs aren’t published yet, so for now verifySelfIssued is one TypeScript file with no dependencies, self-issued.ts, which we’ll send you: write to hello@wellknown.id. Save it beside your server code (Node 24 runs it as it is; elsewhere, compile it with your usual build), and add this to the server from step 3:
import { verifySelfIssued } from'./self-issued.ts';
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)) thrownew Error('this token was used already');
taken.set(claims.nonce, claims.exp);
// claims.sub is the user's did:key at your site, the same as in an ID token
res.json({ sub: claims.sub });
} catch {
res.status(401).json({ error: 'not signed in' });
}
});
It checks the signature against the did:key the token names, that the token is for your domain, that it answers this page’s request (through the verifier), and that it’s fresh: it lives two minutes. Taking each token once stops a replay. With a direct token, your site enforces its own rules: wellknown.id never sees the sign-in to apply your policy. More in the developer notes.
Optional
Say who may sign in, in karu
karu is a small policy language people can read. wellknown.id checks your rules at every sign-in it bridges, after the key proof and before your site gets a code. Rules only ever narrow: each deny that matches turns the sign-in down. This one has two rules. not_p256 takes only P-256 keys, the kind a phone can keep in its secure hardware. no_popin doesn’t let the sign-in run in a frame inside your page (the SDK’s popin mode).
deny not_p256 if principal.key_type != "P-256";
deny no_popin if context.mode == "popin";
A turned-down sign-in reaches your page as wellknown-id-login-failure, with an error naming the rules: access_denied: denied by policy: not_p256. A policy that doesn’t compile refuses every sign-in, so try a change before you publish it. What rules can look at is listed in the developer notes.
wellknown.id is in development. Endpoints and formats may still change before launch.