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 and the full owner proof chain (owner binding + id_token), so your service can verify both the agent’s identity and the owner claim with a single JWKS fetch.
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>",
"ownerBinding": { "payload": {}, "payloadHash": "...", "signature": "..." },
"idToken": "<RS256 JWT from Alien SSO>"
}| 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 core fields |
ownerBinding | object | Owner binding record for full chain verification |
idToken | string | RS256 JWT from Alien SSO for owner verification |
Signature Computation
The signature is computed over canonical JSON of the core payload fields only (keys sorted alphabetically, no whitespace). The ownerBinding and idToken fields are appended after signing and are not covered by sig:
canonical = JSON.stringify(sortKeysRecursively({ v, fingerprint, publicKeyPem, owner, timestamp, nonce }))
sig = Ed25519.sign(canonical, agentPrivateKey)Using the SDK (Node.js)
Install the SDK package. Zero dependencies — uses only Node.js built-in crypto module.
npm install @alien-id/sso-agent-idBasic Verification
Verifies the agent holds the private key. The owner field is not cryptographically verified — use verifyAgentTokenWithOwner for that.
import { verifyAgentRequest } from "@alien-id/sso-agent-id";
function authenticateAgent(req) {
return verifyAgentRequest(req);
}Owner Verification
Verifies the full chain: agent key → owner binding → id_token → Alien SSO JWKS. This proves the owner claim is backed by the SSO server.
import {
fetchAlienJWKS,
verifyAgentRequestWithOwner,
} from "@alien-id/sso-agent-id";
// Fetch JWKS at startup and cache it (refresh periodically)
const jwks = await fetchAlienJWKS();
function authenticateAgent(req) {
return verifyAgentRequestWithOwner(req, { jwks });
}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)opts.clockSkewMs(number, optional) — Allowed clock skew for future-dated tokens. Default:30000(30 seconds)
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)
ownerVerified: false, // Use verifyAgentTokenWithOwner for true
timestamp: 1774531517000, // When the token was created
nonce: "a1b2c3d4..." // Unique per token
}verifyAgentTokenWithOwner(tokenB64, opts) API
Parameters:
tokenB64(string) — Base64url-encoded tokenopts.jwks(JWKS) — Pre-fetched JWKS fromfetchAlienJWKS()opts.maxAgeMs(number, optional) — Maximum token age. Default:300000opts.clockSkewMs(number, optional) — Allowed clock skew. Default:30000
Returns on success:
{
ok: true,
fingerprint: "f5d9fac4...",
publicKeyPem: "-----BEGIN...",
owner: "0000000301...",
ownerVerified: true, // Owner cryptographically verified
ownerProofVerified: false, // true if human's session proof was present and valid
issuer: "https://sso.alien-api.com",
timestamp: 1774531517000,
nonce: "a1b2c3d4..."
}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 |
Missing field: ownerBinding | Token lacks owner binding (needed for owner verification) |
Missing field: idToken | Token lacks id_token (needed for owner verification) |
Owner binding signature verification failed | Binding was not signed by this agent’s key |
Owner binding agent fingerprint mismatch | Binding references a different agent key |
id_token hash does not match owner binding | id_token doesn’t match the binding |
id_token signature verification failed | RS256 signature invalid against JWKS |
id_token sub does not match token owner | id_token subject differs from claimed owner |
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 (Verified)
Trust any agent owned by specific humans. Uses verifyAgentRequestWithOwner to cryptographically verify the owner claim — without this, any agent could claim any owner address.
import {
fetchAlienJWKS,
verifyAgentRequestWithOwner,
} from "@alien-id/sso-agent-id";
const jwks = await fetchAlienJWKS();
const ALLOWED_OWNERS = new Set([
"00000003010000000000539c741e0df8", // Alice
"00000003010000000000542b891a3c47", // Bob
]);
function requireAuthorizedOwner(req, res, next) {
const result = verifyAgentRequestWithOwner(req, { jwks });
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));Owner Verification
Basic token verification (verifyAgentToken) confirms the agent holds the private key but does not verify the owner field. Any process can generate a keypair and claim any owner address.
Full owner verification (verifyAgentTokenWithOwner) proves the complete chain:
Verification Steps (Performed by the SDK)
- Agent key verification — Ed25519 signature valid, fingerprint matches public key, token is fresh
- Owner binding signature — binding was signed by the same agent key (Ed25519)
- Binding → agent key —
agentInstance.publicKeyFingerprintmatches token fingerprint - Binding → owner —
ownerSessionSubmatches tokenowner - Binding → id_token —
SHA-256(idToken)matchesidTokenHashin binding - id_token signature — RS256 signature valid against Alien SSO JWKS
- id_token → owner —
subclaim matches tokenowner - Owner session proof (optional) — human’s Ed25519 consent signature, if present
When to Use Owner Verification
| Scenario | verifyAgentToken | verifyAgentTokenWithOwner |
|---|---|---|
| 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 |
| Owner-based access control | Not sufficient | Required |
Security Considerations
What Basic Verification Proves (verifyAgentToken)
- The agent holds the Ed25519 private key at the time of signing
- The token was created within the last 5 minutes (configurable)
- The
ownerfield is present but not verified (ownerVerified: false)
What Owner Verification Proves (verifyAgentTokenWithOwner)
All of the above, plus:
- The
ownerclaim is cryptographically verified against Alien SSO (ownerVerified: true) - The agent key was bound to the owner via a signed owner binding
- The Alien SSO server attested the binding via an RS256-signed id_token
- If
ownerProofVerified: true, the human explicitly approved this agent via the Alien App
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
Enabling Agent Access with ALIEN-SKILL.md
To let AI agents discover and authenticate to your service, host an ALIEN-SKILL.md file at a public URL (default: /ALIEN-SKILL.md). This is a markdown file written for the agent, not for humans — the agent reads it and follows the instructions autonomously.
What to Include
Your ALIEN-SKILL.md should contain:
- What the service does — a one-line description the agent can use to decide if it’s relevant
- Base URL — tell the agent to use the same origin it fetched the ALIEN-SKILL.md from
- Prerequisites — link to the Agent ID skill install command (
npx skills add alien-id/agent-id) - How to authenticate — show the
auth-headercommand and theAuthorization: AgentID <token>format - API reference — list your endpoints, methods, required auth, and request/response formats
- Examples — curl commands the agent can adapt
Example
# My Service — Agent Authentication
> You are an AI agent. This service accepts Alien Agent ID tokens.
>
> **Base URL**: Use the same origin you fetched this ALIEN-SKILL.md from.
## Prerequisites
You need an Alien Agent ID. If you don't have one:
\`\`\`bash
npx skills add alien-id/agent-id
\`\`\`
Then follow the bootstrap instructions to get your identity.
## Authenticate
\`\`\`bash
AUTH=$(node /path/to/cli.mjs auth-header --raw)
\`\`\`
## API Reference
| Endpoint | Method | Auth | Description |
| ----------- | ------ | ------- | ----------- |
| `/api/data` | GET | AgentID | Fetch data |
| `/api/data` | POST | AgentID | Submit data |
## Auth Header Format
\`\`\`text
Authorization: AgentID <base64url-encoded-token>
\`\`\`
Tokens are valid for 5 minutes. Generate a fresh one for each request.Hosting
- Next.js: put it in
public/ALIEN-SKILL.md— served as a static file - Express: use
app.use(express.static('public'))with the file inpublic/ - Any server: serve it at any stable URL and share that URL with agents
How Agents Find It
Agents discover your ALIEN-SKILL.md when a human pastes your service URL into the agent’s chat, or when the URL is referenced in the sign-in modal via the @alien-id/sso-react SDK (default skillUrl: "/ALIEN-SKILL.md"). The agent fetches the file, reads the instructions, and authenticates automatically.
If your app uses the SSO React SDK, you can enable the agent tab in the sign-in modal to surface the install command and your service URL directly to users. See SSO React API Reference — Agent Mode for setup.
For a working example, see the Demo App which includes a complete ALIEN-SKILL.md.