This guide walks you from a fresh checkout to a running auth-service with a minted and verified token. By the end you will have the service listening locally, a token minted via curl, and the signature verified against the published JWKS.
For protocol-level context on how agents authenticate to the runtime, see the protocol security documentation.
You need Node.js 20 or later and npm. The project uses TypeScript and jose for JWT signing — both are installed as dependencies.
# macOS
brew install node@20
# Ubuntu / Debian
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# Verify
node --version # v20.x or later
npm --versionClone the repository and install dependencies.
git clone https://github.com/multiagentcoordinationprotocol/auth-service.git
cd auth-service
npm installWith no configuration, the service generates an ephemeral RSA keypair on start and listens on 127.0.0.1:3200. The keypair lives only as long as the process.
npm run devYou should see:
[auth-service] listening on port 3200
[auth-service] issuer=macp-auth-service audience=macp-runtime
[auth-service] key source: ephemeral
[auth-service] JWKS: http://localhost:3200/.well-known/jwks.json
[auth-service] Mint: POST http://localhost:3200/tokens
The key source: ephemeral line is the signal that you did not provide MACP_AUTH_SIGNING_KEY_JSON. That is fine for local development; it is never correct in production.
In production the service requires a pinned signing key so it survives restarts and so the runtime's JWKS cache stays warm. Generate one once, store it in your secret manager, and inject it at process start.
# Generate a production JWK
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))})()"
# Run with the pinned key
export MACP_AUTH_SIGNING_KEY_JSON='{"kty":"RSA","kid":"prod-key-1",...}'
export MACP_AUTH_ISSUER=auth.example.com
export MACP_AUTH_AUDIENCE=macp-runtime
npm run build && npm startSee the Deployment Guide for the full environment variable reference and the production checklist.
The mint flow is a single POST. The service validates the request, clamps the TTL to MACP_AUTH_MAX_TTL_SECONDS, signs a JWT with the in-memory private key, and returns the token together with the resolved TTL.
curl -sS -X POST http://localhost:3200/tokens \
-H 'content-type: application/json' \
-d '{
"sender": "agent://risk",
"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": 600
}'Response:
{
"token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRldi1rZXktMSJ9...",
"sender": "agent://risk",
"expires_in_seconds": 600
}The returned expires_in_seconds reflects the effective TTL after clamping. If you request ttl_seconds: 999999 with the default config, you will get 3600 back, not 999999 — MACP_AUTH_MAX_TTL_SECONDS is authoritative.
The payload carries the requested scopes under the macp_scopes claim, plus the standard JWT claims set by the service.
curl -sS -X POST http://localhost:3200/tokens \
-H 'content-type: application/json' \
-d '{"sender":"agent://risk"}' \
| jq -r .token \
| cut -d. -f2 \
| base64 -d 2>/dev/null \
| jqExample decoded body:
{
"macp_scopes": {},
"iat": 1713800000,
"exp": 1713800300,
"iss": "macp-auth-service",
"aud": "macp-runtime",
"sub": "agent://risk"
}The public key is published at /.well-known/jwks.json. The runtime fetches this endpoint on first use and caches the result for MACP_AUTH_JWKS_TTL_SECS seconds.
curl -sS http://localhost:3200/.well-known/jwks.json | jq{
"keys": [
{
"kty": "RSA",
"n": "ukL3...pQ",
"e": "AQAB",
"kid": "dev-key-1",
"alg": "RS256",
"use": "sig"
}
]
}Note that no private material appears here — only n, e, and the metadata needed for signature verification.
Round-trip the token through jose.jwtVerify to confirm the signature, issuer, and audience match.
node -e "
const jose = require('jose');
(async () => {
const token = process.argv[1];
const jwks = jose.createRemoteJWKSet(new URL('http://localhost:3200/.well-known/jwks.json'));
const { payload } = await jose.jwtVerify(token, jwks, {
issuer: 'macp-auth-service',
audience: 'macp-runtime',
});
console.log(payload);
})();
" "$(curl -sS -X POST http://localhost:3200/tokens \
-H 'content-type: application/json' \
-d '{"sender":"agent://risk"}' | jq -r .token)"If the signature verifies you will see the decoded payload. If it does not, the error is one of JWSSignatureVerificationFailed, JWTExpired, JWTClaimValidationFailed, etc. — the API Reference maps them to root causes.
Configure the Rust runtime to trust tokens issued by this service. Set the issuer, audience, and JWKS URL on the runtime and start it.
export MACP_AUTH_ISSUER=macp-auth-service
export MACP_AUTH_AUDIENCE=macp-runtime
export MACP_AUTH_JWKS_URL=http://127.0.0.1:3200/.well-known/jwks.json
export MACP_AUTH_JWKS_TTL_SECS=60
# (plus the usual runtime config: MACP_ALLOW_INSECURE=1, MACP_BIND_ADDR, etc.)
cargo run --manifest-path ../runtime/Cargo.tomlNow run any gRPC client with the minted JWT as a bearer token. The runtime will fetch your JWKS on the first request and cache it for 60 seconds.
| Error | Cause | Fix |
|---|---|---|
400 sender is required |
Missing or empty sender in request body |
Include a non-empty string for sender |
400 ttl_seconds must be a positive number |
ttl_seconds non-positive, NaN, or Infinity |
Pass a positive finite number, or omit to use the default |
JWSSignatureVerificationFailed at the runtime |
Runtime's JWKS cache is stale after a key rotation | Wait MACP_AUTH_JWKS_TTL_SECS or restart the runtime |
JWTClaimValidationFailed: "iss" claim |
MACP_AUTH_ISSUER mismatch between auth-service and runtime |
Align the two env vars |
JWTClaimValidationFailed: "aud" claim |
MACP_AUTH_AUDIENCE mismatch |
Align the two env vars |
JWTExpired |
Token's exp has passed |
Mint a fresh token; check clock skew between issuer and verifier |
| Ephemeral key rotates every restart | MACP_AUTH_SIGNING_KEY_JSON unset |
Set it from a secret store for any shared deployment |
- Integration Guide — end-to-end wiring with the control-plane, SDK orchestrators, SDK agents, and the runtime
- API Reference — full endpoint surface and JWT claim structure
- Architecture — module layout and signing flow
- Deployment Guide — production configuration and Docker
- Operations Runbook — key rotation and diagnostics