kunji

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

  1. The agent asks for a capability (an audience + scope) and presents its own public key.
  2. 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.
  3. The agent signs your challenge with its own key (a holder-of-key proof) and submits the capability + proof to your callback.
  4. You verify both locally — the capability chains to the user's sub; the proof matches the cnf key and your fresh challenge; scope / audience / expiry / revocation all hold.
  5. 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 a 403 insufficient_scope, re-requests a broader scope or a vc: 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.