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).
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 itscnf.jwk. The signing key must match this binding.Signature-Inputcovers@method @authority @path signature-key content-digest content-type authorization(plus@queryif and only if the URL has a query string), with bothcreated(within ±60 s) and anonce(≥ 12 bytes of entropy, base64url). No other signature parameters.Signaturevalue is base64url (label=:…:).Content-Digestissha-256=:<standard base64>:over the exact body bytes.
- 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.
Completing the consent flow
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:
- Build the signature base over
@method @authority @path signature-key content-digest content-type authorization(emptysignature-key/authorizationvalues are covered as empty strings). - Append
"@signature-params"with onlycreatedandnonce. - Sign with Ed25519 (or RSA-PSS-SHA512).
- Send
Signature-Input/Signature(base64url) plusContent-Digest(standard base64) andAgent-Auth: agent_token=<JWT>.
The SDK's signRequest() and buildSignatureBase() are also exported individually if you only want the signing functions.
Troubleshooting
| Error | Likely cause | Fix |
|---|---|---|
authentication_required (401) | Missing/invalid Agent-Auth, bad signature, or signing key doesn't match the agent token's cnf.jwk | Sign with the key that the agent token is bound to |
| 401 on resource token | Resource not registered | Verify resource identifier in portal |
| Signature verification failed | Wrong key format or JWKS mismatch | Regenerate JWKS from the same private key |
| 404 agent not found | Identifier mismatch | Match identifier exactly (inc. https:// and trailing slashes) |
Next steps
- AAuth Protocol spec — full technical details
- AAuth 1.0 Specification — normative token formats and the request-signing profile
- AAuth Quickstart — LangChain — embed AAuth inside a LangGraph agent
Standards used on this page
| Plain name | Standard |
|---|---|
| Signed HTTP requests (HTTP message signatures) | RFC 9421 |
| Request body digest | RFC 9530 |
See the standards reference for what each one covers and how LumoAuth uses it.