Preview: wellknown.id is in development and isn’t live yet

Developers · Quick start

Sign-in in three steps.

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.

{
  "version": 1,
  "relying_parties": [{
    "redirect_uris": ["https://your.site/"],
    "client_name": "Your Site"
  }]
}

Can’t serve a file there? The same config can go in a DNS TXT record instead: see publishing it in DNS.

Step 2

Add the button to your page

Put the button where you want it, and load the SDK from wellknown.id:

<wellknown-id-button></wellknown-id-button>
<script src="https://wellknown.id/sdk/web.js" defer></script>

Then, in your page’s own script, send the ID token to your server when sign-in succeeds:

window.addEventListener('wellknown-id-login-success', (e) => {
  fetch('/session', { method: 'POST', body: e.detail.id_token });
});
window.addEventListener('wellknown-id-login-failure', (e) => {
  console.warn('Sign-in failed:', e.detail.error);
});

The page must be one of your redirect_uris (here, your home page). The SDK picks the browser’s own sign-in dialog where there is one, or a pop-up.

Step 3

Verify the ID token on your server

It’s a standard OpenID Connect ID token, so a stock library checks it. Here it is in Node with jose and Express (npm install express jose):

import express from 'express';
import { createRemoteJWKSet, jwtVerify } from 'jose';

const ISSUER = 'https://wellknown.id';
const jwks = createRemoteJWKSet(new URL(`${ISSUER}/jwks`));
const app = express();

app.post('/session', express.text(), async (req, res) => {
  try {
    const { payload } = await jwtVerify(req.body, jwks, { issuer: ISSUER, audience: 'your.site' });
    // payload.sub is the user's did:key at your site: their account. Start your session here.
    res.json({ sub: payload.sub });
  } catch {
    res.status(401).json({ error: 'not signed in' });
  }
});

app.listen(3000);

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:

{
  "version": 1,
  "relying_parties": [{
    "redirect_uris": ["https://your.site/"],
    "client_name": "Your Site",
    "self_issued": "allow"
  }]
}

Ask for it in the script tag, and send the token with its verifier to your server:

<script src="https://wellknown.id/sdk/web.js?selfIssued=1" defer></script>
window.addEventListener('wellknown-id-login-success', (e) => {
  const { self_issued, verifier } = e.detail;
  fetch('/session/direct', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ token: self_issued, verifier }),
  });
});

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)) throw new 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";

Put it in your config entry as a JSON string:

{
  "version": 1,
  "relying_parties": [{
    "redirect_uris": ["https://your.site/"],
    "client_name": "Your Site",
    "policy": "deny not_p256 if principal.key_type != \"P-256\";\ndeny 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.