FOR DEVELOPERS
Sign in with an agent
Let an AI agent act for a user at your app — within a scope they approve, for a limited time, revocably — without ever giving the agent the user's keys. The user authorizes a capability in their wallet; the agent presents it with a proof it signs itself; you verify both locally, exactly like a normal sign-in →. Human approval stays the root of trust.
Live demo — watch an agent get authorized
See the real flow run against the deployed demo RP
(demo.kunji.cc) and the kunji relay — a genuine code + QR, a real
capability received & decrypted in your browser, a real login. Watch the recorded sample,
or run it live and approve in your own wallet.
What it is
A normal sign-in is a person tapping Approve. A delegated sign-in is a person
authorizing an agent to sign in for them — once, with a capability that is bound to
one app, a chosen scope, and an expiry. The agent holds its own
keypair; the capability is useless to anyone who doesn't hold that agent key
(holder-of-key), so it isn't a bearer token to steal. Your app still only ever sees
the same per-app sub — no new identifier, nothing correlatable.
The capability
An EdDSA JWT signed by the user's per-app key (the key behind their
sub), bound to the agent's public key via the
cnf claim. Self-verifying: the signing key rides in the header, and
you bind it by checking sub === SHA-256(publicKey).
{
"aud": "yourapp.com", // one app only
"sub": "hex SHA-256 of the per-app public key", // the sub you already know
"scope": ["login"], // least privilege; your app interprets it
"iat": 1750000000, "exp": 1750003600, // short-lived
"jti": "…", // revocation id
"cnf": { "jwk": { "kty":"OKP","crv":"Ed25519","x":"…agent public key…" } }
}
Signed by the per-app key, so a forged capability only ever authenticates the forger's
own sub — never a victim's. Scope, audience, expiry and the
cnf key are all inside the signature.
How it works
-
The agent asks for a capability (an
audience+scope) and presents its own public key. - The user approves in the kunji wallet — Security → Authorize an agent — choosing a scope and TTL. The wallet signs the capability with the per-app key.
- The agent signs your challenge with its own key (a holder-of-key proof) and submits the capability + proof to your callback.
-
You verify both locally — the capability chains to the user's
sub; the proof matches thecnfkey and your fresh challenge; scope / audience / expiry / revocation all hold. -
You start a scoped session for that
sub, marked as an agent and limited to the granted scope.
Same trust model as a normal sign-in: no kunji backend in the path, single-use per session, challenge-bound. The only new principal is the agent's key — and it can't widen what the user granted.
Connect once, ask for more later
An agent shouldn't over-ask up front. Start with a narrow scope (say
login); when an action later needs more, your app returns
403 insufficient_scope and the agent steps up — it re-requests
with the broader scope, and the wallet shows a delta-aware re-consent: "already
connected" + only the new permission to approve. No new kunji infrastructure — it reuses the
same deep link (app.kunji.cc/?authorize=…) and return relay as the
first authorization.
See it live →
Opt-in notifications. A channel-less agent (no way to reach you in the moment) can ask you to enable per-app notifications at first consent. Then it pings the wallet via Web Push when it needs a step-up — the push carries only an opaque pointer (a request id), never the scope or request; the wallet fetches + shows it, you approve. Off by default, revocable per app, and kunji never becomes a user directory. Design: docs/push-relay.md.
For relying parties
Add one endpoint that verifies a capability + proof against a session, then approves it.
The verifier is the same EdDSA/JWT scheme as the assertion — drop in the reference
verifyCapabilityAssertion and a
revokedCapabilities denylist.
// POST /kunji/agent ← agent posts { capability, agentProof } for a session
app.post('/kunji/agent', async (req, res) => {
const { sessionId, capability, agentProof } = req.body || {};
const session = await db.get(`loginSessions/${sessionId}`);
const r = await verifyCapabilityAssertion({ // see functions/capability.js
capability, agentProof,
audience: session.audience, // YOUR domain
challenge: session.challenge, // the fresh per-session nonce
isRevoked: (jti) => db.has(`revokedCapabilities/${jti}`),
});
if (!r.ok) return res.status(400).json({ error: r.error });
// single-use: approve the session, keyed by sub, limited to the granted scope
await db.update(`loginSessions/${sessionId}`,
{ status: 'approved', sub: r.sub, scope: r.scope, agent: true });
res.json({ status: 'ok' });
});
Revoke by adding a capability's jti to your denylist; short
TTLs are the safety net. Enforce scope on every agent action — a
capability is permission to do a specific thing, not full account access.
The MCP bridge — drive it from an AI runtime
examples/kunji-mcp is a local
MCP
server that lets Claude (or any MCP client) act for you via a capability. The agent's key
lives only on your machine; nothing but a scoped capability ever leaves the wallet.
claude mcp add kunji -- node /path/to/examples/kunji-mcp/server.js
kunji_authorize(audience, scope)— returns the request you approve in the wallet.kunji_await_capability— blocks until you approve, then loads the capability over the relay.kunji_set_capability(capability)— stores it (validated: holder-of-key + expiry).kunji_login(baseUrl)— signs in at the app as the authorized agent.kunji_stepup(scope)— on a403 insufficient_scope, re-requests a broader scope or avc:credential to present.kunji_request_via_push— Transport ② for a channel-less agent (the opt-in Web Push relay).kunji_status— the agent's public key + the loaded capability.
Reference
Full design + threat model:
docs/agentic-delegation.md. Reference code:
src/lib/capability.js
(mint / proof / verify),
kunji-login-demo/functions/capability.js (RP verifier),
examples/kunji-agent-demo (a plain-Node RP you can run — no
Firebase — that accepts agent logins via POST /kunji/agent), and
examples/kunji-mcp (the bridge). The scope grammar
(docs/scope.md) and both delivery transports
(docs/push-relay.md — same-device step-up and the opt-in Web Push relay)
are implemented; the scope vocabulary may still grow.