Skip to main content

Agent Auth (AAuth) Protocol

AAuth is an authorization framework for autonomous agents acting on behalf of users and organizations. It gives agents and resources cryptographic identities, binds every token to a key (proof-of-possession), and signs every request with RFC 9421 — HTTP Message Signatures: cryptographic signatures over specific HTTP request components (method, path, selected headers, body digest). The verifier rebuilds the signature input from the request and checks the signature — so every call has a replay-proof proof of possession.

Specification status

AAuth is an emerging protocol under active development. LumoAuth tracks the current draft and may adjust as the standard evolves.

Overview

AAuth extends OAuth 2.1 for agents with cryptographic identities. Unlike traditional OAuth clients that require user interaction for each authorization, AAuth supports:

  • Autonomous operation — agents act independently once authorized
  • Cryptographic identity — all parties have verifiable identities via public-key cryptography
  • Proof-of-possession — every token is bound to a key, preventing token theft
  • HTTP Message Signing (RFC 9421) — every request is signed, protecting integrity and replay
  • Progressive authentication — from "no user" to "user actively present"
  • Delegation — agents delegate capabilities to sub-agents
  • Token exchange — agents exchange tokens across trust boundaries

Key concepts

Parties in AAuth

PartyDescriptionIdentity
AgentAn autonomous software entityHTTPS URL with cryptographic identity
ResourceA protected service or APIURI with cryptographic identity
Authorization ServerIssues tokens after verifying identitiesLumoAuth organization endpoint
UserHuman who authorizes agentsOptional, depending on auth level

Token types

AAuth defines three JWT token types. RFC 7519 — JWT is a signed JSON payload; all AAuth tokens are JWTs bound to a public key:

  • Resource Token (resource+jwt) — proves the resource's identity to the authorization server. Issued to resources, short-lived (5 minutes), bound to the resource's public key via cnf.jkt.
  • Auth Token (auth+jwt) — authorizes an agent's access to a specific resource. Issued to the agent for a specific agent+resource+user combination, 1 hour default, bound to the agent's public key via cnf.

Authentication levels

LevelDescriptionUser interactionUse cases
noneNo user identityNonePublic APIs, anonymous access
authenticatedUser logged in onceInitial onlyGeneral agent tasks
authorizedUser explicitly approvedExplicit consentSensitive operations
userUser actively presentPer-requestHigh-security operations

Protocol flows

Flow 1: Agent registration

Before participating in AAuth, agents and resources must register with the authorization server.

Portal UI

You can register agents and resources through the Organization Portal at /orgs/{orgId}/portal/aauth/agents, or via the API.

Flow 2: Direct authorization (no user)

For none or authenticated level access, no user interaction is needed:

POST https://app.lumoauth.dev/orgs/{orgId}/api/v1/aauth/agent/token
Content-Type: application/json
Agent-Auth: [HTTP Message Signature]

{
"request_type": "auth",
"agent_token": "eyJhbGc...",
"resource_token": "eyJhbGc...",
"scope": "read write"
}

Response 200:
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write",
"refresh_token": "aauth_refresh_abc123def456"
}

For authorized or user level access:

Worked example: AI email agent

A complete walkthrough of an AI email agent reading and sending mail for a user.

Scenario

ComponentIdentifierDescription
Agenthttps://email-agent.aiservices.comAI email assistant
Resourcehttps://api.mailservice.comEmail API service
Auth Serverhttps://app.lumoauth.dev/orgs/acme-corpLumoAuth organization
Useralice@acme-corp.comAlice wants to use the agent

Step 1: Register the agent (one-time)

In the Organization Portal, go to /orgs/acme-corp/portal/aauth/agents/create and configure:

  • Name: AI Email Assistant
  • Identifier: https://email-agent.aiservices.com
  • JWKS URI: https://email-agent.aiservices.com/.well-known/jwks.json
  • Redirect URIs: https://email-agent.aiservices.com/oauth/callback
  • Allowed Scopes: email:read email:send email:delete
  • Signing Algorithms: ed25519, rsa-pss-sha512
  • Features: ✅ User Authorization, ✅ Delegation, ✅ Token Exchange

Step 2: Register the resource (one-time)

At /orgs/acme-corp/portal/aauth/resources/create:

  • Name: Corporate Email API
  • Identifier: https://api.mailservice.com
  • JWKS URI: https://api.mailservice.com/.well-known/jwks.json
  • Supported Scopes: email:read, email:send, email:delete
  • Default Auth Level: authorized (requires explicit user consent)

Step 3: Agent generates a key pair

AAuth uses Ed25519 by default. Ed25519 is a modern elliptic-curve signature scheme — short keys, fast, widely supported. The agent generates a key pair per user session:

// Email agent code (Node.js)
const crypto = require('crypto');

// Generate Ed25519 key pair for this user session
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519', {
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

// Convert to JWK format
const publicKeyJwk = {
kty: 'OKP',
crv: 'Ed25519',
x: publicKey.export({ type: 'spki', format: 'der' })
.slice(-32).toString('base64url')
};

// Store private key securely for this session
session.agentPrivateKey = privateKey;
session.agentPublicKeyJwk = publicKeyJwk;

Step 4: Obtain an agent token

The main agent server issues an agent token to Alice's session delegate:

const agentToken = await fetch('https://email-agent.aiservices.com/delegate/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
delegate_sub: `https://email-agent.aiservices.com/session/${alice.sessionId}`,
delegate_public_key: publicKeyJwk,
audience: ['https://app.lumoauth.dev/orgs/acme-corp/api/v1'],
lifetime: 3600
})
}).then(r => r.json());

console.log('Agent Token:', agentToken.access_token);

Resulting agent token claims:

{
"typ": "agent+jwt",
"alg": "EdDSA",
"kid": "agent-key-1"
}
{
"iss": "https://email-agent.aiservices.com",
"sub": "https://email-agent.aiservices.com/session/alice-123",
"aud": ["https://app.lumoauth.dev/orgs/acme-corp/api/v1"],
"exp": 1706817600,
"iat": 1706814000,
"jti": "agent_token_abc123",
"cnf": {
"jkt": "SHA256_thumbprint_of_delegate_public_key"
}
}

Step 5: Resource creates a resource token

The email API creates a short-lived resource token to prove its identity:

const resourceToken = await fetch(
'https://app.lumoauth.dev/orgs/acme-corp/api/v1/aauth/resource/token',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer https://api.mailservice.com`
},
body: JSON.stringify({
resource_identifier: 'https://api.mailservice.com',
audience: 'https://app.lumoauth.dev/orgs/acme-corp/api/v1',
lifetime: 300
})
}
).then(r => r.json());

console.log('Resource Token:', resourceToken.resource_token);

The agent requests authorization with Alice's consent. This requires an RFC 9421 HTTP Message Signature:

const crypto = require('crypto');

// Build request body
const requestBody = {
request_type: 'auth',
agent_token: agentToken.access_token,
resource_token: resourceToken.resource_token,
scope: 'email:read email:send',
redirect_uri: 'https://email-agent.aiservices.com/oauth/callback'
};

// Create HTTP Message Signature
const method = 'POST';
const targetUri = 'https://app.lumoauth.dev/orgs/acme-corp/api/v1/aauth/agent/token';
const contentType = 'application/json';
const bodyBytes = Buffer.from(JSON.stringify(requestBody));
const contentDigest = `sha-256=:${crypto.createHash('sha256').update(bodyBytes).digest('base64')}:`;

const signatureBase = [
`"@method": ${method}`,
`"@target-uri": ${targetUri}`,
`"content-type": ${contentType}`,
`"content-digest": ${contentDigest}`
].join('\n');

const signature = crypto.sign(null, Buffer.from(signatureBase), session.agentPrivateKey);
const signatureB64 = signature.toString('base64');

const agentAuthHeader = `sig1=:${signatureB64}:; label="sig1"; alg="ed25519"; ` +
`keyid="https://email-agent.aiservices.com#key-1"; ` +
`created=${Math.floor(Date.now() / 1000)}; ` +
`covered="@method @target-uri content-type content-digest"`;

// Request authorization
const authResponse = await fetch(targetUri, {
method: 'POST',
headers: {
'Content-Type': contentType,
'Content-Digest': contentDigest,
'Agent-Auth': agentAuthHeader
},
body: JSON.stringify(requestBody)
});

if (authResponse.status === 401) {
const authData = await authResponse.json();
console.log('User authorization required');
console.log('Authorization URL:', authData.auth_url);

// Redirect Alice to authorization URL
redirectUser(authData.auth_url);
}

Alice is redirected to the LumoAuth consent screen, which shows:

  • Agent: AI Email Assistant (https://email-agent.aiservices.com)
  • Will access: Corporate Email API (https://api.mailservice.com)
  • Permissions requested: ☑️ email:read — read emails and metadata; ☑️ email:send — send emails on your behalf
  • Trust indicators: ✅ Verified agent identity ✅ Verified resource identity ✅ Cryptographically secured

This application will act on behalf of Alice (alice@acme-corp.com).

Step 8: Exchange the authorization code for tokens

After Alice approves, she's redirected back with an authorization code:

// Alice is redirected back with authorization code
// https://email-agent.aiservices.com/oauth/callback?code=authz_code_abc123def456

const codeExchangeBody = {
request_type: 'code',
code: 'authz_code_abc123def456',
request_token: 'aauth_req_abc123'
};

// Sign and exchange code for tokens
const tokenResponse = await fetch(targetUri, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Agent-Auth': signRequest(codeExchangeBody) // Using same signing logic
},
body: JSON.stringify(codeExchangeBody)
}).then(r => r.json());

console.log('Auth Token:', tokenResponse.access_token);
console.log('Refresh Token:', tokenResponse.refresh_token);
console.log('Expires in:', tokenResponse.expires_in, 'seconds');

// Store tokens securely
session.authToken = tokenResponse.access_token;
session.refreshToken = tokenResponse.refresh_token;

Step 9: Access the email API

// Read emails
const signEmailRequest = (method, url, authToken) => {
const signatureBase = [
`"@method": ${method}`,
`"@target-uri": ${url}`,
`"authorization": Bearer ${authToken}`
].join('\n');

const signature = crypto.sign(null, Buffer.from(signatureBase), session.agentPrivateKey);
return `sig1=:${signature.toString('base64')}:; label="sig1"; alg="ed25519"; ` +
`keyid="https://email-agent.aiservices.com#key-1"; ` +
`created=${Math.floor(Date.now() / 1000)}; ` +
`covered="@method @target-uri authorization"`;
};

const emails = await fetch('https://api.mailservice.com/v1/emails/inbox', {
method: 'GET',
headers: {
'Authorization': `Bearer ${session.authToken}`,
'Agent-Auth': signEmailRequest('GET',
'https://api.mailservice.com/v1/emails/inbox',
session.authToken)
}
}).then(r => r.json());

console.log('Fetched', emails.length, 'emails');
// Send email
const emailData = {
to: 'bob@example.com',
subject: 'Meeting Summary',
body: 'Here is the summary of our meeting...',
from: 'alice@acme-corp.com'
};

const emailBody = JSON.stringify(emailData);
const emailDigest = `sha-256=:${crypto.createHash('sha256')
.update(Buffer.from(emailBody))
.digest('base64')}:`;

const sendResult = await fetch('https://api.mailservice.com/v1/emails/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${session.authToken}`,
'Content-Type': 'application/json',
'Content-Digest': emailDigest,
'Agent-Auth': signEmailRequestWithBody('POST',
'https://api.mailservice.com/v1/emails/send',
session.authToken, emailDigest)
},
body: emailBody
}).then(r => r.json());

console.log('Email sent:', sendResult.message_id);

Step 10: Refresh the token

const refreshBody = {
request_type: 'refresh',
refresh_token: session.refreshToken,
scope: 'email:read email:send'
};

const refreshResponse = await fetch(targetUri, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Agent-Auth': signRequest(refreshBody)
},
body: JSON.stringify(refreshBody)
}).then(r => r.json());

// Update tokens
session.authToken = refreshResponse.access_token;
session.refreshToken = refreshResponse.refresh_token;

console.log('Tokens refreshed');

Security features

1. Proof-of-possession binding

All tokens are bound to cryptographic keys. A stolen token is useless without the matching private key.

2. HTTP Message Signing (RFC 9421)

Every request is signed, protecting against:

  • Request tampering
  • Replay attacks (with nonce validation)
  • Man-in-the-middle attacks

3. Cryptographic identity verification

All parties have verifiable cryptographic identities with published JWKS endpoints.

4. Delegation chain tracking

The act (actor) claim tracks the complete delegation chain:

{
"sub": "user_12345",
"client_id": "https://primary-agent.com",
"act": {
"sub": "https://delegate-agent.com",
"act": {
"sub": "https://sub-delegate.com"
}
}
}

API reference

Agent token endpoint

POST /orgs/{orgId}/api/v1/aauth/agent/token

Main endpoint for token operations. Requires an RFC 9421 HTTP Message Signature via the Agent-Auth header.

request_typePurposeRequired fields
authRequest new auth tokenagent_token, resource_token, scope
codeExchange authorization codecode, request_token
refreshRefresh auth tokenrefresh_token
exchangeExchange token across boundariessubject_token, resource_token

Discovery endpoints

GET /.well-known/aauth-issuer — authorization server metadata (supported algorithms, token types, endpoints).

GET /.well-known/aauth-agent — agent metadata including JWKS URI and supported capabilities (hosted by the agent).

GET /.well-known/aauth-resource — resource metadata including supported scopes and auth levels (hosted by the resource).

Best practices

For agents

  • Generate a unique key pair per user session — don't reuse keys across users
  • Store private keys securely — use an HSM if possible, encrypt at rest
  • Request only the scopes you need
  • Refresh proactively — before tokens expire
  • Validate all responses — check signatures and claims

For resources

  • Verify proof-of-possession — don't accept tokens without valid signatures
  • Validate token claims — check aud, scope, exp
  • Implement clear error responses
  • Cache validated tokens — to rate-limit verification cost
  • Log security events — failed verifications, insufficient scopes
Security warning

Never store private keys in source code or environment variables in production. Use a Hardware Security Module (HSM) or secure key management service.