[BAP] Bounded Authority Protocol

Your OAuth proves who is calling.
BAP proves what they may do.

If you run an MCP server, an agent, or any API that authenticates callers with OAuth, you already have the who. BAP is the missing half: every request carries a cryptographically signed capability — one operation, one target, one time window — and a fresh proof from the caller’s own key. The resource verifies the envelope and gets facts, never trust. Keys never leave your custody.

Issuersigns the grant
grant
Holderproves the request
proof
Verifierchecks the envelope

Private keys never enter the library — not even in the demo below.

The piece OAuth leaves out

OAuth and MCP authentication answer “which client is this?” — and stop there. What that client may do is left to scopes, hope, and your resource server’s restraint. BAP closes that gap, alongside the OAuth you already run.

A token says who — not what.

A stolen or over-scoped token keeps every power it was granted, on every resource, until it expires. “read” means all reads, everywhere, for the token’s whole life.

With BAP, each capability is a signed grant naming one operation, one target, one window — and each request carries a fresh proof bound to exactly that request.

Trust moves to facts.

Today your resource trusts the transport, the introspection endpoint, and the caller’s good behavior. Nothing cryptographic ties this particular request to this particular permission.

The verifier checks the envelope itself — issuer signature, holder binding, operation match, time window — and returns cryptographic facts, with no reason codes for attackers to probe.

Your keys stay yours.

Authority systems usually want your private key, or a vendor to hold it for you. That’s a new blast radius most teams won’t accept.

BAP never takes key material — issuers and holders sign through their own custody (HSM, KMS, or a browser key store — the demo below uses one). Nothing to hand over, nothing to leak.

Where it pays off

Four places teams already feel the gap between who and what — every one works with the OAuth you already run.

An agent that can spend money

MCP agent · payments API

Hand the agent a token and whatever it decides — or gets injected to decide — the token allows, until it expires.

grant: transfer · acct acme-77 · until 14:15

No proof, no transfer: a hijacked prompt can’t move funds at 14:40, or to a different account — the API enforces the envelope, not the agent’s good behavior.

A third-party tool you didn’t write

your workspace · someone else’s MCP server

Connecting a tool today often means blanket, long-lived scopes over your files or mail — to code you’ve never audited.

grant: read · attachments/ only · this session

Every tool call carries a fresh proof against a narrow grant — the tool, or whoever compromises it, can’t browse an inch past the bound.

Just-in-time elevation

on-call engineer or automation · production

Standing admin rights “just in case,” and break-glass tokens nobody remembers to revoke.

grant: deploy + rollback · staging · 30 min

The window closes itself — yesterday’s grant is cryptographically useless today, with no revocation list to babysit. Key rotation rides the same rails (signKeyTransition).

Support reading one customer

support agent · multi-tenant SaaS

A “support” role token typically reads every tenant’s records — access decided by policy files, not cryptography.

grant: read · tenant #4821 · ticket #90112

Curiosity can’t cross tenants: each read is proven against a grant that names one tenant and one ticket’s lifetime.

The envelope playground

Everything below is live: this page bundles the real signer and verifier packages and signs with a key generated in your browser. No server, no simulation, no mocks.

Issuer grants authority
may
transfer · { kind: "all" }
audience
resource.example.test
window
t=1000 → 2000 (evaluated at 1500)
Holder your agent
request
POST /invoke · amount: 5000
invocation
550e8400-…440000
Verifier the resource
trusts issuer key
verify an envelope to see the fact sheet
Tamper deckEvery button below produces a real, closed INVALID.

Sign and prove first — then break it on purpose.

Wire viewerClick any artifact to inspect its actual bytes.
nothing selected yet

The family lifecycle

Three protocols, one chain of evidence: what the agent is, what was agreed, what this call may do.

Custody boundary

Keys stay in your custody — an HSM, a KMS, or the browser key store this page just used. The library only ever sees a handle.

C1 role gate

A holder handle can never mint its own capability: grants require an issuer-role identity resolved atomically, before sign() is even called.

Wrong-key guard

Every signature is verified against the resolved public key before assembly. A misconfigured custodian fails loudly as signing_failed — never a silent false success.

Closed errors

A closed error-code set; no key material, nonce values, or report content ever appears in an error.

The five signers

FunctionObjectRole
signReportholder proof — the grant passes through untouchedholder
signLocalLoopbackReportlocal-loopback application proof (ba+loopback-proof)holder
signAnchorboundary anchorrole-agnostic
signKeyTransitionkey transitionrole-agnostic
signGrantcapability grantissuer-only

The key-handle contract

One interface, every operation. Every method may be async — remote custodians are first-class.

interface KeyHandle {
  sign(message: Uint8Array): Uint8Array | Promise<Uint8Array>;        // 64-byte Ed25519
  publicKey(): Uint8Array | Promise<Uint8Array>;                       // 32 bytes
  thumbprint(): string | Promise<string>;                              // RFC 7638, base64url
  keyIdentity?(): { keyId, publicKey } | Promise<…>;                  // anchor + transition
  signingIdentity?(): { role: "issuer" | "holder", keyId, publicKey }; // grants — the C1 gate
}
npm i @bounded-authority-protocol/signer    # Node >= 22, one runtime dep: the verifier