agentkeychain-server
v0.2.1
Published
Server SDK for AgentKeychain — OIDC code flow, ID token verification, and access token introspection for services that accept agent logins.
Maintainers
Readme
agentkeychain-server
Server SDK for AgentKeychain. For services that accept agent logins via OIDC and protect API routes with agent-issued access tokens.
This package provides:
AgentKeychain— OIDC authorization code flow with PKCE (the relying-party side of "Sign in with AgentKeychain").IntrospectionClient— RFC 7662 token introspection for opaque access tokens.TokenValidator+JwksClient— local ID-token (JWT) verification against the auth server's JWKS.requireAuth/requireAuthExpress— Hono and Express middleware that gate routes on a valid access token.
Installation
npm install agentkeychain-server
# or
bun add agentkeychain-serverhono and express are optional peer dependencies — only install the one your app uses.
Sign in with AgentKeychain (OIDC code flow)
import { AgentKeychain } from "agentkeychain-server";
const ak = new AgentKeychain({
clientId: process.env.AKC_CLIENT_ID!, // "akc_client_..."
clientSecret: process.env.AKC_CLIENT_SECRET!,
// endpoint defaults to https://api.agentkeychain.com
});1. Build the authorize URL and redirect
app.get("/auth/agentkeychain/login", (c) => {
const state = crypto.randomUUID();
const { url, codeVerifier } = ak.getAuthorizeUrl({
redirect_uri: "https://yourapp.com/auth/callback",
state,
scope: "openid profile",
});
// Persist `state` and `codeVerifier` server-side (session, signed cookie, etc.)
saveSession({ state, codeVerifier });
return c.redirect(url);
});2. Handle the callback
app.get("/auth/callback", async (c) => {
const code = c.req.query("code");
const state = c.req.query("state");
const session = loadSession();
if (state !== session.state) {
return c.text("invalid state", 400);
}
const tokens = await ak.exchangeCode({
code: code!,
redirect_uri: "https://yourapp.com/auth/callback",
code_verifier: session.codeVerifier,
});
// tokens: { access_token, refresh_token, id_token, token_type, expires_in, scope }
return c.json({ ok: true, expires_in: tokens.expires_in });
});3. Refresh tokens
const refreshed = await ak.refreshToken({
refresh_token: storedRefreshToken,
scope: "openid profile",
});Errors
import { OidcError } from "agentkeychain-server";
try {
await ak.exchangeCode({ ... });
} catch (err) {
if (err instanceof OidcError) {
switch (err.code) {
case "INVALID_CHALLENGE":
case "INVALID_SIGNATURE":
case "LOGIN_NOT_PERMITTED":
case "AGENT_REVOKED":
case "USER_DEACTIVATED":
case "AUTH_FAILED":
case "NETWORK_ERROR":
case "TIMEOUT":
}
// err.authError contains the raw RFC 6749 error body if the server returned one
}
}Protecting API routes (introspection middleware)
The middleware introspects the bearer token on every request and attaches the result to the request/context.
Hono
import { Hono } from "hono";
import { requireAuth } from "agentkeychain-server";
const app = new Hono();
app.use(
"/api/*",
requireAuth({
clientId: process.env.AKC_CLIENT_ID!,
clientSecret: process.env.AKC_CLIENT_SECRET!,
})
);
app.get("/api/me", (c) => {
const claims = c.get("auth"); // V1IntrospectionResponse
return c.json({ sub: claims.sub, scope: claims.scope });
});Express
import express from "express";
import { requireAuthExpress } from "agentkeychain-server";
const app = express();
app.use(
"/api",
requireAuthExpress({
clientId: process.env.AKC_CLIENT_ID!,
clientSecret: process.env.AKC_CLIENT_SECRET!,
})
);
app.get("/api/me", (req, res) => {
const claims = req.auth!;
res.json({ sub: claims.sub, scope: claims.scope });
});Custom error responses
Both middlewares accept an onError hook. Return a Response to override the default 401, or return void to fall through to the default body.
requireAuth({
clientId, clientSecret,
onError: (err) => {
log.warn({ err }, "auth failed");
// return undefined to use default 401
},
});Lower-level building blocks
If you're not on Hono or Express, compose the middleware yourself:
import {
createIntrospectionClient,
validateRequest,
buildErrorResponse,
MissingTokenError,
InactiveTokenError,
} from "agentkeychain-server";
const introspectionClient = createIntrospectionClient({
clientId, clientSecret,
});
async function handler(req: Request) {
try {
const claims = await validateRequest({
authHeader: req.headers.get("authorization") ?? undefined,
introspectionClient,
});
// ... claims is V1IntrospectionResponse
} catch (err) {
const { status, body } = buildErrorResponse(err as Error);
return new Response(JSON.stringify(body), { status });
}
}Verifying ID tokens locally (no network call per request)
For high-volume APIs you can verify ID tokens against the auth server's JWKS instead of introspecting on every request. This is local crypto verification — no network call after the JWKS cache is warm.
import { JwksClient, TokenValidator, TokenValidationError } from "agentkeychain-server";
const jwks = new JwksClient({
jwksUri: "https://api.agentkeychain.com/.well-known/jwks.json",
cacheTtlMs: 10 * 60 * 1000, // 10 minutes
});
const validator = new TokenValidator({
jwksClient: jwks,
audience: process.env.AKC_CLIENT_ID!,
});
try {
const claims = await validator.verifyIdToken(idToken);
// claims: IdTokenClaimsV1 — sub, act, agent_id namespaced claim, etc.
} catch (err) {
if (err instanceof TokenValidationError) {
// err.code: EXPIRED, INVALID_SIGNATURE, INVALID_ISSUER, ...
}
}ID tokens are short-lived and can't be revoked, so use this only when latency matters. For revocable access tokens, prefer introspection.
Direct introspection (without middleware)
import { IntrospectionClient } from "agentkeychain-server";
const client = new IntrospectionClient({
clientId, clientSecret,
});
const result = await client.introspect(accessToken);
// result.active, result.sub, result.scope, ...
if (await client.isActive(accessToken)) { ... }License
Apache-2.0
