Skip to main content

AAuth Quickstart — JavaScript / Node.js

Authenticate an AI agent and access protected resources from Node.js with the official SDK: @lumoauth/agent. The SDK implements the full AAuth 1.0 protocol. AAuth is the identity protocol for agents: an agent proves who it is by signing each request with its own private key instead of sending a shared secret. The SDK covers Ed25519 (and RSA-PSS-SHA512) key handling, signed HTTP requests, the Content-Digest request body digest, and the four request types of the agent token endpoint (auth / code / exchange / refresh). So you never write a signature by hand. It uses only node:crypto and the global fetch (Node 18+; no native or heavy dependencies).

Signing contract — what the server requires

The agent token endpoint (/aauth/agent/token) enforces a strict request-signing profile (see the AAuth 1.0 spec §4). The SDK produces all of this for you:

  • Agent-Auth: agent_token=<JWT> — the agent token (typ: agent+jwt), bound to your key via its cnf.jwk. The signing key must match this binding.
  • Signature-Input covers @method @authority @path signature-key content-digest content-type authorization (plus @query if and only if the URL has a query string), with both created (within ±60 s) and a nonce (≥ 12 bytes of entropy, base64url). No other signature parameters.
  • Signature value is base64url (label=:…:).
  • Content-Digest is sha-256=:<standard base64>: over the exact body bytes.
Prerequisites
  • A LumoAuth organization (agent identity is available on every plan, nothing to switch on)
  • Node.js 18+
npm install @lumoauth/agent

The AAuth client is in the dedicated @lumoauth/agent package. It is Node-only (it uses node:crypto) and is deliberately kept out of the browser-safe packages.

1. Generate a keypair​

import { generateKeypair } from '@lumoauth/agent';

const { privateKeyPem, jwks } = generateKeypair();
// Persist these:
// privateKeyPem -> your secret manager (Vault, AWS Secrets Manager, Doppler, ...)
// jwks -> publish at https://your-agent.example.com/.well-known/jwks.json
// (or register inline when creating the agent)
console.log(JSON.stringify(jwks, null, 2));

Register the agent (with this JWKS) and a resource in the portal — see Steps 1–3 of the AAuth Quickstart.

2. Request an auth token​

import { AAuthClient } from '@lumoauth/agent';

const client = new AAuthClient({
agentIdentifier: 'https://my-agent.example.com',
privateKeyPem: process.env.AGENT_PRIVATE_KEY, // Ed25519 PKCS#8 PEM
baseUrl: 'https://app.lumoauth.dev', // or LUMOAUTH_URL
orgId: 'acme-corp', // or LUMOAUTH_ORG_ID
});

// Optional: inspect the org's AAuth issuer metadata
const issuer = await client.discoverIssuer();
console.log(issuer.agent_signing_algs_supported); // ["ed25519", "ecdsa-p256-sha256", "rsa-pss-sha512"]

// Present the agent token (Agent-Auth header) + resource token (body).
// agentToken — your `agent+jwt`, bound to the key above via cnf.jwk
// resourceToken — issued by the resource you intend to call
const result = await client.requestAuthToken({
resourceToken,
scope: 'read write',
agentToken,
redirectUri: 'https://my-agent.example.com/oauth/callback', // only needed for consent flows
});

if ('authorizationRequired' in result) {
// User consent needed: send the user to the consent page, then exchange the
// code delivered to your redirectUri.
console.log('Redirect the user to:', result.authorizationUri);
} else {
console.log('auth_token:', result.authToken, 'expires in', result.expiresIn);
}

Authentication failures (bad signature, key/token binding mismatch, expired agent token) throw an AAuthError with code: 'authentication_required' and statusCode: 401.

const tokens = await client.exchangeCode({
code, // ?code=… on your redirect URI
redirectUri: 'https://my-agent.example.com/oauth/callback',
agentToken,
});
// tokens = { requestType: 'code', authToken, expiresIn, tokenType: 'auth+jwt', refreshToken? }

3. Call the protected resource​

signedRequest() sends the auth token as a Bearer header and a request signature that proves possession of the key bound in the token's cnf.jwk. The signature covers the authorization component:

const resp = await client.signedRequest('POST', 'https://api.example.com/v1/data', {
authToken: result.authToken,
data: { hello: 'world' },
});
console.log(await resp.json());

4. Refresh, exchange, revoke​

// Refresh (requires a fresh resource token for the target resource;
// requested scopes must be a subset of the original grant)
const refreshed = await client.refresh({
refreshToken: tokens.refreshToken,
resourceToken: freshResourceToken,
agentToken,
});

// Multi-hop agent→agent token exchange (act chain; must be enabled for the agent)
const downstream = await client.exchangeToken({
authToken: upstreamAuthToken,
resourceToken: downstreamResourceToken,
agentToken,
});

// Revoke (auth tokens by their `jti`, refresh tokens by value)
await client.revoke({ token: tokens.refreshToken, tokenType: 'refresh_token', agentToken });

5. Verifying tokens on your resource server​

Resource servers written in Node can verify incoming auth+jwt bearer tokens against the organization's issuer JWKS:

import { verifyAuthToken, jwkThumbprint } from '@lumoauth/agent';

const claims = await verifyAuthToken(bearerToken, {
issuer: 'https://app.lumoauth.dev/orgs/acme-corp/api/v1',
resource: 'https://api.example.com', // must equal the token's aud
});

// AAuth tokens are proof-of-possession: ALSO verify the request's RFC 9421
// signature against claims.cnf.jwk (compare jkt via jwkThumbprint), or use
// POST {issuer}/aauth/auth/token/verify for revocation-aware introspection.

Manual implementation (no SDK)​

If you cannot use the SDK, the exact wire contract is specified in the AAuth 1.0 spec §4. In short:

  1. Build the signature base over @method @authority @path signature-key content-digest content-type authorization (empty signature-key/authorization values are covered as empty strings).
  2. Append "@signature-params" with only created and nonce.
  3. Sign with Ed25519 (or RSA-PSS-SHA512).
  4. Send Signature-Input/Signature (base64url) plus Content-Digest (standard base64) and Agent-Auth: agent_token=<JWT>.

The SDK's signRequest() and buildSignatureBase() are also exported individually if you only want the signing functions.

Troubleshooting​

ErrorLikely causeFix
authentication_required (401)Missing/invalid Agent-Auth, bad signature, or signing key doesn't match the agent token's cnf.jwkSign with the key that the agent token is bound to
401 on resource tokenResource not registeredVerify resource identifier in portal
Signature verification failedWrong key format or JWKS mismatchRegenerate JWKS from the same private key
404 agent not foundIdentifier mismatchMatch identifier exactly (inc. https:// and trailing slashes)

Next steps​

Standards used on this page​

Plain nameStandard
Signed HTTP requests (HTTP message signatures)RFC 9421
Request body digestRFC 9530

See the standards reference for what each one covers and how LumoAuth uses it.