Service Integration
This guide shows how to add AI agent authentication to any web service. After integration, agents with an Alien Agent ID can authenticate using cryptographically signed tokens — no API keys, no shared secrets, no pre-registration.
How It Works
The token is self-contained: it carries the agent’s public key, so your service can verify the signature without any prior knowledge of the agent.
Token Format
Agents send authentication via the Authorization header:
Authorization: AgentID <base64url-encoded-json>Decoded token payload:
{
"v": 1,
"fingerprint": "f5d9fac49457e9e359078815f7c1c568a56207a4a5c0b05a11ce3cf54bc8d4f8",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----\n",
"owner": "00000003010000000000539c741e0df8",
"timestamp": 1774531517000,
"nonce": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"sig": "<Ed25519-base64url-signature>"
}| Field | Type | Description |
|---|---|---|
v | number | Token version (always 1) |
fingerprint | string | SHA-256 hash of the agent’s public key DER encoding (64 hex chars) |
publicKeyPem | string | Agent’s Ed25519 public key in SPKI PEM format |
owner | string | null | AlienID address of the human who authorized this agent |
timestamp | number | Unix timestamp in milliseconds |
nonce | string | Random 128-bit hex string (replay resistance) |
sig | string | Ed25519 signature (base64url) over canonical JSON of all fields except sig |
Signature Computation
The signature is computed over canonical JSON of the payload (all fields except sig, keys sorted alphabetically, no whitespace):
canonical = JSON.stringify(sortKeysRecursively({ v, fingerprint, publicKeyPem, owner, timestamp, nonce }))
sig = Ed25519.sign(canonical, agentPrivateKey)Option A: Use lib.mjs (Node.js)
Copy lib.mjs into your project or import it directly. Zero dependencies — uses only Node.js built-in crypto module.
import { verifyAgentToken } from "./lib.mjs";
function authenticateAgent(req) {
const header = req.headers.authorization;
if (!header || !header.startsWith("AgentID ")) {
return { ok: false, error: "Missing Authorization: AgentID <token>" };
}
return verifyAgentToken(header.slice(8).trim());
}verifyAgentToken(tokenB64, opts?) API
Parameters:
tokenB64(string) — Base64url-encoded token from the Authorization headeropts.maxAgeMs(number, optional) — Maximum token age in milliseconds. Default:300000(5 minutes)
Returns on success:
{
ok: true,
fingerprint: "f5d9fac4...", // Agent identity (stable across sessions)
publicKeyPem: "-----BEGIN...", // Agent's Ed25519 public key
owner: "0000000301...", // Human owner's AlienID address (or null)
timestamp: 1774531517000, // When the token was created
nonce: "a1b2c3d4..." // Unique per token
}Returns on failure:
{
ok: false,
error: "Token expired (age: 312s)"
}Possible errors:
| Error | Meaning |
|---|---|
Invalid token encoding | Token is not valid base64url JSON |
Unsupported token version: N | Unknown token version |
Token expired (age: Ns) | Token is older than maxAgeMs |
Invalid public key in token | publicKeyPem is not a valid Ed25519 key |
Fingerprint does not match public key | fingerprint doesn’t match SHA-256(publicKeyDER) |
Signature verification failed | Ed25519 signature is invalid — token was tampered |
Option B: Implement Verification Yourself
The verification algorithm is straightforward to implement in any language with Ed25519 and SHA-256 support.
Step-by-Step Verification
1. DECODE
raw = base64url_decode(token)
parsed = JSON.parse(raw)
2. CHECK VERSION
assert parsed.v == 1
3. CHECK TIMESTAMP
age = now_ms() - parsed.timestamp
assert age >= 0 AND age <= 300000 # 5 minutes
4. VERIFY FINGERPRINT
der = parse_spki_pem(parsed.publicKeyPem)
computed = hex(sha256(der))
assert computed == parsed.fingerprint
5. VERIFY SIGNATURE
payload = { v, fingerprint, publicKeyPem, owner, timestamp, nonce }
canonical = canonical_json(payload) # sorted keys, no whitespace
sig_bytes = base64url_decode(parsed.sig)
pubkey = parse_ed25519_public_key(parsed.publicKeyPem)
assert ed25519_verify(canonical, sig_bytes, pubkey)Access Control Patterns
Allow Any Verified Agent
Accept any agent with a valid token:
function requireAgent(req, res, next) {
const result = verifyAgent(req);
if (!result.ok) return res.status(401).json({ error: result.error });
req.agent = result;
next();
}Require Human-Owned Agents
Reject agents that don’t have a human owner:
function requireOwnedAgent(req, res, next) {
const result = verifyAgent(req);
if (!result.ok) return res.status(401).json({ error: result.error });
if (!result.owner) return res.status(403).json({ error: "Human-owned agent required" });
req.agent = result;
next();
}Allow-List by Agent Fingerprint
Pre-register known agent fingerprints:
const ALLOWED_AGENTS = new Set([
"f5d9fac49457e9e359078815f7c1c568a56207a4a5c0b05a11ce3cf54bc8d4f8",
"42fbde2a3f7ca6dfdc61fc74e54227c84ff0a6e85f1a838052d9aa60ca2b527f",
]);
function requireKnownAgent(req, res, next) {
const result = verifyAgent(req);
if (!result.ok) return res.status(401).json({ error: result.error });
if (!ALLOWED_AGENTS.has(result.fingerprint)) {
return res.status(403).json({ error: "Agent not authorized" });
}
req.agent = result;
next();
}Allow-List by Owner
Trust any agent owned by specific humans:
const ALLOWED_OWNERS = new Set([
"00000003010000000000539c741e0df8", // Alice
"00000003010000000000542b891a3c47", // Bob
]);
function requireAuthorizedOwner(req, res, next) {
const result = verifyAgent(req);
if (!result.ok) return res.status(401).json({ error: result.error });
if (!result.owner || !ALLOWED_OWNERS.has(result.owner)) {
return res.status(403).json({ error: "Agent owner not authorized" });
}
req.agent = result;
next();
}Rate Limiting by Agent
const rateLimits = new Map();
function rateLimit(maxRequests, windowMs) {
return (req, res, next) => {
const fp = req.agent.fingerprint;
const now = Date.now();
const entry = rateLimits.get(fp) || { count: 0, windowStart: now };
if (now - entry.windowStart > windowMs) {
entry.count = 0;
entry.windowStart = now;
}
entry.count++;
rateLimits.set(fp, entry);
if (entry.count > maxRequests) {
return res.status(429).json({ error: "Rate limit exceeded" });
}
next();
};
}
// 100 requests per minute per agent
app.use("/api", requireAgent, rateLimit(100, 60000));Deep Verification (Optional)
Basic token verification confirms the agent holds the private key. For higher-security scenarios, verify the full provenance chain back to the human owner.
Proof Bundle
The agent can provide its proof bundle as an additional header:
X-Agent-Proof: <base64url-encoded-proof-bundle>The proof bundle contains the agent’s public key, owner binding, id_token, and SSO base URL.
Verification Steps
1. Token fingerprint == proof.agent.fingerprint
2. proof.ownerBinding.payload — canonical JSON matches payloadHash
3. proof.ownerBinding.signature — Ed25519 verify with proof.agent.publicKeyPem
4. proof.ownerBinding.payload.agentInstance.publicKeyFingerprint == token fingerprint
5. sha256(proof.idToken) == proof.ownerBinding.payload.idTokenHash
6. Fetch SSO JWKS from proof.ssoBaseUrl + "/.well-known/openid-configuration"
7. Verify proof.idToken RS256 signature against JWKSWhen to Use Deep Verification
| Scenario | Basic Token | Deep Verification |
|---|---|---|
| Read-only API access | Sufficient | Not needed |
| Write operations | Sufficient | Recommended |
| Financial transactions | Not sufficient | Required |
| Audit-sensitive operations | Sufficient | Recommended |
| First-time agent registration | Not sufficient | Required |
Security Considerations
What the Token Proves
- The agent holds the Ed25519 private key at the time of signing
- The token was created within the last 5 minutes (configurable)
- The agent claims a specific owner (verified only via deep verification)
What It Does Not Prove (Without Deep Verification)
- That the owner field is truthful (the agent self-asserts it)
- That the agent is currently authorized by the owner
- That the human owner is a real, unique person
Replay Protection
- Tokens include a random
nonceand atimestamp - The 5-minute expiry window limits replay
- For stricter protection, track seen nonces per agent fingerprint
Clock Skew
- Tokens use the agent’s local clock
- The default 5-minute window accommodates reasonable clock drift
- Adjust
maxAgeMsfor known clock skew issues - Reject tokens with negative age (timestamp in the future)
Transport Security
- Always use HTTPS in production
- The token is a bearer credential — capture enables replay within validity window