JWT-minting identity service for the MACP runtime. Implements RFC-MACP-0004 §4 (direct-agent-auth) as a dedicated identity provider so that SDK-based agents can authenticate directly to the runtime with short-lived RS256 bearer tokens.
control-plane ──POST /tokens──► auth-service :3200 ──┐
SDK orchestrators ──POST /tokens──► auth-service :3200 │
│ public keys
macp-runtime (gRPC) ◄────GET /.well-known/jwks.json─────────┘ cached per
MACP_AUTH_JWKS_TTL_SECS
SDK agents (TS / Python) ──Authorization: Bearer <JWT>──► macp-runtime (gRPC)
- Minting: the control-plane
(or any orchestrator built on the TypeScript SDK
or Python SDK)
calls
POST /tokensonce per agent it spawns, passingsender+ scopes. The returned JWT is handed to the agent in its bootstrap payload underruntime.bearerToken. - Bearer presentation: SDK-based agents load the bearer from bootstrap
and present it as
Authorization: Bearer <JWT>on every gRPC call to the runtime. See the SDK auth guides (TypeScript, Python). - Verification: the runtime is configured with
MACP_AUTH_JWKS_URL=http://auth-service:3200/.well-known/jwks.json. It fetches the JWKS (cached perMACP_AUTH_JWKS_TTL_SECS) and validates every incoming JWT's signature +iss+aud+expon each gRPC frame. See the runtime Getting Started and Deployment guides.
This service is not in the hot path of a running session — tokens are minted once per agent at provisioning time, then reused for the session lifetime.
Liveness probe. Returns { "ok": true } with HTTP 200.
Returns the public JWKS (private material is never exposed here).
{
"keys": [{
"kty": "RSA", "alg": "RS256", "use": "sig", "kid": "dev-key-1",
"n": "…", "e": "AQAB"
}]
}Mint a JWT.
Request:
{
"sender": "risk-agent",
"scopes": {
"can_start_sessions": true,
"is_observer": false,
"allowed_modes": ["macp.mode.decision.v1", ""],
"max_open_sessions": 1,
"can_manage_mode_registry": false
},
"ttl_seconds": 3600
}sender(required) — becomes the JWTsubclaim and the authenticated identity the runtime associates with incoming frames.scopes(optional) — serialized verbatim under themacp_scopesclaim.ttl_seconds(optional) — clamped byMACP_AUTH_MAX_TTL_SECONDS. Defaults toMACP_AUTH_DEFAULT_TTL_SECONDSwhen omitted.
Response:
{
"token": "eyJhbGciOi…",
"sender": "risk-agent",
"expires_in_seconds": 3600
}Errors:
400{"error":"sender is required"}ifsenderis missing or empty.400{"error":"ttl_seconds must be a positive number"}ifttl_secondsis non-positive or non-finite.
See .env.example for the complete reference. Minimum in production:
| Variable | Default | Required? | Notes |
|---|---|---|---|
PORT |
3200 |
no | HTTP listen port |
MACP_AUTH_ISSUER |
macp-auth-service |
no | JWT iss. Must match runtime's expected issuer. |
MACP_AUTH_AUDIENCE |
macp-runtime |
no | JWT aud. Must match runtime's expected audience. |
MACP_AUTH_MAX_TTL_SECONDS |
3600 |
no | Upper bound on minted token lifetime. |
MACP_AUTH_DEFAULT_TTL_SECONDS |
300 |
no | Applied when request omits ttl_seconds. |
MACP_AUTH_SIGNING_KEY_JSON |
(ephemeral) | yes in prod | RSA private JWK. If unset, generates an ephemeral keypair on startup (dev only — keys rotate on every restart). |
node -e "const {generateKeyPair, exportJWK} = require('jose'); \
(async () => { \
const { privateKey } = await generateKeyPair('RS256', { extractable: true }); \
const jwk = await exportJWK(privateKey); \
jwk.kid = 'prod-key-1'; \
console.log(JSON.stringify(jwk)); \
})();"Set the output as MACP_AUTH_SIGNING_KEY_JSON. Rotate by generating a new
key with a fresh kid and redeploying; the runtime's JWKS cache refreshes
within MACP_AUTH_JWKS_TTL_SECS.
npm install # one-time
npm run dev # ts-node watch (not restart)
npm test # jest — unit + HTTP integration via supertest
npm run test:coverage
npm run build # compile to dist/
npm start # run the compiled build
npm run typecheck # tsc --noEmitdocker build -t macp-auth-service:local .
docker run --rm -p 3200:3200 macp-auth-service:local
curl http://localhost:3200/healthzThe published CI image is ghcr.io/multiagentcoordinationprotocol/auth-service
(see .github/workflows/docker.yml).
Full documentation lives under docs/:
| Page | Purpose |
|---|---|
| Getting Started | Install, run locally, mint your first token, verify against JWKS |
| Integration Guide | End-to-end wiring with the control-plane, SDK orchestrators, SDK agents, and the runtime |
| Architecture | Module layout, request flow, key lifecycle, design goals |
| API Reference | All three HTTP endpoints, JWT claim structure, error table |
| Deployment | Production checklist, env vars, Docker, Kubernetes, TLS termination |
| Operations Runbook | Key rotation, diagnostics, common failures, incident response |
POST /tokenshas no client authentication in this implementation. It assumes a trusted intra-cluster network. If the service is reachable from anywhere else, put it behind mTLS / a reverse proxy that authenticates callers, or add a shared-secretAuthorizationheader check. Anyone who can hit/tokenstoday can mint a JWT for anysender.- Run with
MACP_AUTH_SIGNING_KEY_JSONsupplied by a secret store (Kubernetes Secret, Vault, etc.) in any shared environment. - Container runs as a non-root user and exposes an HTTP healthcheck; no extra runtime privileges are needed.