karu is a policy language: embeddable, deny-overrides and default-deny, matching patterns over JSON, with a syntax like Polar's. It interoperates with Cedar (karu import turns Cedar policies into karu). wellknown.id checks your rules at every sign-in it bridges, after the key proof and before it issues a code: at /auth, in FedCM's window, and when a wallet signs in for another device.
Writing one
Rules only ever narrow: each deny that matches turns the sign-in down, and wellknown.id supplies the allow. An allow rule in your policy changes nothing.
deny not_p256 if principal.key_type != "P-256"; deny no_popin if context.mode == "popin";
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. Put it in your config entry as a JSON string (or at the top level, for every entry on your domain):
{
"redirect_uris": ["https://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, or has an import, or is over 32 KiB, refuses every sign-in as invalid_client, so try a change before you publish it.
What rules can look at
Every sign-in is checked against one document:
{
"principal": { "id": "did:key:zDn…", "key_type": "P-256" },
"action": "login",
"resource": { "client_id": "acme.example", "domain": "acme.example", "redirect_uri": "https://acme.example/callback", "scopes": ["openid"] },
"context": { "mode": "popup", "config_source": "http", "auth_age_seconds": 0 }
}
| Field | |
|---|---|
principal.id | the did:key your site will get as sub |
principal.key_type | P-256 or Ed25519 |
principal.actor, context.delegated | where someone acts for the person (delegation): the actor's did:key at your site, and true. deny no_delegation if context.delegated == true; refuses them |
resource.client_id, resource.domain | your domain (both, so a policy may use either) |
resource.redirect_uri, resource.scopes | the request's |
context.mode | navigate, popup, popin, fedcm or wallet. popup and navigate come from your page's own request, so they're your page's word |
context.config_source | where your config came from: dns or http |
context.auth_age_seconds | always 0: every sign-in proves the key afresh |
Where it doesn't apply
A self-issued token your site takes itself never reaches wellknown.id, so wellknown.id checks nothing for it: your site enforces its own rules. wellknown.id's own rules are checked first, and a sign-in they refuse is reported as wellknown.id.<rule>.
Coming
User-authored policies, written by people about what they'll share, with tools that guide them: planned.