Source: docs/integration/auth.md
Authentication — API key
The Forgium administrator provides an API key for each environment. Use it with
the Public API (/v1/*):
Authorization: Bearer <FORGIUM_AGENT_API_KEY>
Obtain access
Request these values from the Forgium administrator:
FORGIUM_AGENT_BASE_URL=<value issued for the target environment>
FORGIUM_AGENT_API_KEY=<value issued out of band>
Store the key in the host application's secret manager. Do not commit it,
include it in a capability manifest, or put it in logs.
If a request returns 401, ask the administrator to verify or replace the key.
Forgium-Context verification
Capability endpoints receive a short-lived Forgium-Context Ed25519
JWS. You must validate its issuer, audience, kid, time claims, exact
method and URL, body hash, and replay ID before accepting a request.
Quick start with the reference adapter
The easiest way to verify Forgium-Context is the framework-neutral reference
adapter. It works with any HTTP framework (Express, Hono, Fastify, Workers):
import { verifyForgiumContext, createInMemoryReplayStore } from "@micro-agent/capabilities/capability-context-reference";
// In your HTTP handler:
const result = await verifyForgiumContext({
contextHeader: request.headers.get("Forgium-Context"),
method: request.method,
url: request.url,
body: await request.text(),
jwks: await fetchJwks(), // See "Retrieving JWKS" below
issuer: "https://api.forgium.dev",
audience: "https://your-endpoint.example",
// Required: verification fails closed without an atomic replay store.
rememberJti: async (jti, expiresAt) => {
// Store in KV with TTL (see "Replay protection" below)
const existing = await env.KV.get(`jti:${jti}`);
if (existing) return false;
await env.KV.put(`jti:${jti}`, "1", { expiration: expiresAt });
return true;
},
});
if (!result.ok) {
// result.code is a stable error code safe for logging
return new Response(JSON.stringify({ error: result.code }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
// Use result.claims for business logic
const { subject_id, capability_id, revision } = result.claims;
Stable error codes
The adapter returns one of these codes on failure:
| Code | Meaning |
|---|---|
CONTEXT_MISSING_HEADER | Forgium-Context header is missing or empty |
CONTEXT_INVALID_JWS | Token is not a valid compact JWS |
CONTEXT_INVALID_HEADER | Protected header is invalid |
CONTEXT_UNKNOWN_KEY | kid not found in JWKS |
CONTEXT_INVALID_CLAIMS | Claims structure is invalid |
CONTEXT_CLAIMS_MISMATCH | Claims don't match the request |
CONTEXT_EXPIRED | Token has expired (TTL is 5 minutes) |
CONTEXT_INVALID_SIGNATURE | Signature verification failed |
CONTEXT_BODY_HASH_MISMATCH | Body hash doesn't match claims |
CONTEXT_REPLAY_PROTECTION_REQUIRED | Replay store callback was not configured |
CONTEXT_REPLAY | JTI has been seen before |
These codes are safe to log and return to clients. Never log the JWS token,
claims payload, or request body.
Retrieving JWKS
Retrieve verification keys from
/.well-known/forgium/capability-context/v1/jwks.json. Cache the response
and refresh when kid lookup fails.
async function fetchJwks(): Promise<JsonWebKey[]> {
const response = await fetch(
"https://api.forgium.dev/.well-known/forgium/capability-context/v1/jwks.json"
);
const { keys } = await response.json();
return keys;
}
Key rotation: The JWKS may contain multiple keys. The adapter selects the
key matching the token's kid header. When Forgium rotates keys, the JWKS
will include both old and new keys during the transition period.
Replay protection
You must implement replay protection. The JTI (JWT ID) is unique per
token. Store seen JTIs with TTL equal to the token's expiration.
Production options:
- Cloudflare KV: Store with
expirationoption - D1: Insert with TTL, check for existence
- Durable Object: Use storage with alarm-based cleanup
Testing/development:
import { createInMemoryReplayStore } from "@micro-agent/capabilities/capability-context-reference";
const { rememberJti, cleanup } = createInMemoryReplayStore();
// Pass rememberJti to verifyForgiumContext
// Call cleanup() when shutting down
What the host must verify
The Forgium-Context contains these claims that must match the request:
| Claim | Must match |
|---|---|
iss | Your expected issuer (e.g., https://api.forgium.dev) |
aud | Your endpoint URL or audience identifier |
method | The HTTP method (e.g., POST) |
url | The exact request URL |
body_sha256 | SHA-256 hash of the raw request body |
iat / exp | Current time (with 60s clock skew tolerance) |
jti | Unique ID (for replay protection) |
Additional claims available after verification:
subject_id: The user's opaque identifierconversation_id: The conversation IDcapability_id: The capability that was calledrevision: The capability revision
Security requirements
- Always verify the signature. Never skip cryptographic verification.
- Always check replay. Without replay protection, a captured token can be
reused. - Validate all claims. Don't cherry-pick claims to verify.
- Stop on failure. If verification fails, reject the request entirely.
- Never log secrets. The JWS token, claims, and body are sensitive.
Example with Express
import express from "express";
import { verifyForgiumContext } from "@micro-agent/capabilities/capability-context-reference";
const app = express();
app.post("/v1/people/lookup", express.raw({ type: "application/json" }), async (req, res) => {
const result = await verifyForgiumContext({
contextHeader: req.headers["forgium-context"],
method: req.method,
url: `${req.protocol}://${req.get("host")}${req.originalUrl}`,
body: req.body.toString(),
jwks: await fetchJwks(),
issuer: "https://api.forgium.dev",
audience: `${req.protocol}://${req.get("host")}`,
rememberJti: async (jti, expiresAt) => {
// Your replay store implementation
},
});
if (!result.ok) {
return res.status(401).json({ error: result.code });
}
// Your business logic here
const { subject_id } = result.claims;
// ...
});
Example with Cloudflare Workers
import { verifyForgiumContext } from "@micro-agent/capabilities/capability-context-reference";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const result = await verifyForgiumContext({
contextHeader: request.headers.get("Forgium-Context"),
method: request.method,
url: request.url,
body: await request.text(),
jwks: await fetchJwks(),
issuer: "https://api.forgium.dev",
audience: new URL(request.url).origin,
rememberJti: async (jti, expiresAt) => {
const existing = await env.KV.get(`jti:${jti}`);
if (existing) return false;
await env.KV.put(`jti:${jti}`, "1", { expiration: expiresAt });
return true;
},
});
if (!result.ok) {
return new Response(JSON.stringify({ error: result.code }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
// Your business logic here
return new Response(JSON.stringify({ ok: true }));
},
};
API key authentication
Retrieve verification keys from
/.well-known/forgium/capability-context/v1/jwks.json; never request or
store a private signing key.
Rotate endpoint tokens with the token rotation runbook.
Example
curl -fsS "$FORGIUM_AGENT_BASE_URL/v1/capabilities" \
-H "Authorization: Bearer $FORGIUM_AGENT_API_KEY"