Skip to Content
Integrate Agent SSO
View .md

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>" }
FieldTypeDescription
vnumberToken version (always 1)
fingerprintstringSHA-256 hash of the agent’s public key DER encoding (64 hex chars)
publicKeyPemstringAgent’s Ed25519 public key in SPKI PEM format
ownerstring | nullAlienID address of the human who authorized this agent
timestampnumberUnix timestamp in milliseconds
noncestringRandom 128-bit hex string (replay resistance)
sigstringEd25519 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 header
  • opts.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:

ErrorMeaning
Invalid token encodingToken is not valid base64url JSON
Unsupported token version: NUnknown token version
Token expired (age: Ns)Token is older than maxAgeMs
Invalid public key in tokenpublicKeyPem is not a valid Ed25519 key
Fingerprint does not match public keyfingerprint doesn’t match SHA-256(publicKeyDER)
Signature verification failedEd25519 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 JWKS

When to Use Deep Verification

ScenarioBasic TokenDeep Verification
Read-only API accessSufficientNot needed
Write operationsSufficientRecommended
Financial transactionsNot sufficientRequired
Audit-sensitive operationsSufficientRecommended
First-time agent registrationNot sufficientRequired

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 nonce and a timestamp
  • 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 maxAgeMs for 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
Last updated on