OAuthMCPBridge.Macaroon (oauth_mcp_bridge v0.1.0-dev.0)

Copy Markdown View Source

A minimal, self-owned macaroon — first-party caveats only (no third-party discharge). We own this rather than pull a stale hex dep because it is the security-critical token primitive.

A macaroon is an identifier (public, integrity-protected) plus an ordered list of caveat predicates, authenticated by an HMAC-SHA256 chain:

sig = HMAC(root_key, identifier)
sigᵢ = HMAC(sig_{i-1}, caveatᵢ)

The final sig is the credential. Because each caveat extends the chain, a holder can add caveats to attenuate (restrict) a macaroon offline (attenuate/2) but cannot remove or alter existing ones without the root key. Verification recomputes the chain and then requires every caveat to be satisfied — unknown caveats fail closed, so attenuation only ever narrows authority.

Wire form: URL-safe base64 of a small JSON envelope {i, c, s} (all fields themselves base64url), so a token is one opaque bearer string.

Summary

Functions

Append a first-party caveat, restricting the macaroon (holder-side attenuation).

Parse a bearer string back into a macaroon.

Serialize to a single opaque URL-safe bearer string.

Mint a macaroon binding identifier and caveats under root_key.

Verify the HMAC chain under root_key, then require verifier.(caveat) to return true for every caveat. Returns {:ok, identifier} or {:error, reason}.

Types

t()

@type t() :: %OAuthMCPBridge.Macaroon{
  caveats: [binary()],
  identifier: binary(),
  sig: binary()
}

Functions

attenuate(m, caveat)

@spec attenuate(t(), binary()) :: t()

Append a first-party caveat, restricting the macaroon (holder-side attenuation).

decode(token)

@spec decode(String.t()) :: {:ok, t()} | :error

Parse a bearer string back into a macaroon.

encode(m)

@spec encode(t()) :: String.t()

Serialize to a single opaque URL-safe bearer string.

mint(root_key, identifier, caveats \\ [])

@spec mint(binary(), binary(), [binary()]) :: t()

Mint a macaroon binding identifier and caveats under root_key.

verify(root_key, m, verifier)

@spec verify(binary(), t(), (binary() -> boolean())) ::
  {:ok, binary()} | {:error, :bad_signature | :caveat_unsatisfied}

Verify the HMAC chain under root_key, then require verifier.(caveat) to return true for every caveat. Returns {:ok, identifier} or {:error, reason}.