Authentication
Agent Assembly’s aa-api supports two credential paths, side by side:
- API keys — for machines. SDKs, agents, and scripts authenticate programmatically with an API key. This path is unchanged and is available on every deployment.
- Native email/password accounts — for human operators at the dashboard. This is an additive path for people; it never replaces the API key. It is only available on a Postgres-backed deployment (see below).
Both paths mint the same scoped JWT that every RBAC gate already reads, so enabling accounts changes nothing about how authorization works — it only adds a second way for a human to obtain that token.
Which methods a deployment offers
A deployment advertises its available credential methods through a public endpoint, so the dashboard never presents a login form the backend cannot serve:
$ curl http://localhost:7700/api/v1/auth/methods
{"methods":["api_key"]} # in-memory deployment (API key only)
$ curl http://localhost:7700/api/v1/auth/methods
{"methods":["api_key","password"]} # Postgres-backed deployment
password appears only when a Postgres account store is configured. On an
in-memory deployment the native-auth endpoints below respond 503 Service Unavailable and the dashboard shows only the API-key path.
Native accounts require Postgres. Passwords must be stored durably and safely, which an in-memory map cannot do across a restart, so the account endpoints are Postgres-gated. In-memory deployments stay API-key-only. This is a deliberate, surfaced limitation — not a hidden failure.
The API-key path (machines)
Unchanged. A caller exchanges an API key for a scoped JWT:
$ curl -X POST http://localhost:7700/api/v1/auth/token \
-H "Authorization: Bearer <api-key>"
Use this for SDKs and agents. It works on every deployment, with or without Postgres.
The native-account path (human operators)
When a Postgres store is configured, aa-api mounts a set of account endpoints
under /api/v1/auth:
| Endpoint | Method | Purpose |
|---|---|---|
/api/v1/auth/methods | GET | Advertise the available methods (api_key, and password when Postgres-backed). Public. |
/api/v1/auth/login | POST | Email + password → access token (+ refresh cookie). |
/api/v1/auth/register | POST | Register the first (bootstrap) account, or a self-registered account when open registration is enabled. |
/api/v1/auth/invite | POST | Create a single-use invite for a new account. Admin scope required. |
/api/v1/auth/invite/accept | POST | Set the initial password and activate an invited account. |
/api/v1/auth/refresh | POST | Exchange the refresh cookie for a fresh access token. |
/api/v1/auth/logout | POST | Revoke the refresh session and clear the cookie. |
/api/v1/auth/password/reset | POST | Request a password-reset email. Requires the SMTP mailer to deliver mail. |
/api/v1/auth/password/reset/confirm | POST | Consume a reset token and set a new password. |
The access token is short-lived (15 minutes) and returned in the response body;
the refresh token is delivered as an HttpOnly; Secure; SameSite=Strict cookie
scoped to /api/v1/auth, and is rotated on every refresh. remember_me on login
extends the refresh lifetime from 12 hours to 30 days.
First user is admin, then invite-only
On a fresh instance the users table is empty, so registration bootstraps:
- Bootstrap. The first account created via
POST /api/v1/auth/registerbecomes theownerof the single default workspace. Registration is open only for this first account. - After bootstrap. Once any account exists,
registerreturns403(registration closed). New accounts are created by an admin:POST /api/v1/auth/invite(admin scope) mints a single-use, expiring invite token; the invitee sets their password viaPOST /api/v1/auth/invite/accept.
An invite token is returned to the inviting admin exactly once (only its hash is stored) and expires after 7 days. Deliver it to the invitee out of band.
Opening self-registration (optional)
To let anyone self-register (not just the first user), set:
AA_AUTH_OPEN_REGISTRATION=true
The default is false (closed — first-user-then-invite). Only the truthy
spellings 1, true, or yes (case-insensitive) enable it; any other value or
leaving it unset keeps registration closed. When open registration is enabled,
accounts created after the bootstrap owner receive the developer role.
Password policy
Passwords are hashed with argon2id and must be at least 12 characters.
A shorter password is rejected with 422. Login is enumeration-safe: an unknown
email and a wrong password both return a uniform 401, and repeated failures
lock the account (423 with a Retry-After header) after 5 attempts for
15 minutes.
Password-reset email (SMTP)
Password reset (POST /api/v1/auth/password/reset) needs to deliver a reset
token to the account owner by email. aa-api ships a pluggable SMTP mailer
configured entirely through environment variables:
| Variable | Required | Default | Meaning |
|---|---|---|---|
AA_SMTP_HOST | yes, to send mail | (unset) | SMTP relay host. Its presence is what switches on real email delivery. |
AA_SMTP_PORT | no | 587 | SMTP port (submission with STARTTLS). |
AA_SMTP_USER | no | (unset) | Username for authenticated submission. Omit for an unauthenticated relay. |
AA_SMTP_PASS | no | (unset) | Password for authenticated submission. |
AA_SMTP_FROM | no | no-reply@localhost | The From: address stamped on outbound mail. |
When AA_SMTP_HOST is set, aa-api builds a real SMTP transport (STARTTLS,
authenticated when a user + pass are supplied) and password-reset emails are
delivered.
Canonical production sender (AAASM-5521)
The no-reply@localhost default is intentionally a safe, unconfigured
placeholder — it keeps a self-hosted deployment that never wired up SMTP from
looking production-ready. A production deployment of the hosted service must
set AA_SMTP_FROM to a real, authenticated sender on the dedicated
transactional subdomain:
AA_SMTP_FROM=no-reply@mail.agent-assembly.com
This keeps application (transactional) mail off the human Google Workspace
sending reputation, matching the boundary described for the SaaS mailer. The
DKIM/SPF/return-path DNS that makes mail.agent-assembly.com deliverable is
owned by the DNS ticket (AAASM-5517) and is not part of aa-api. Setting
AA_SMTP_FROM to a sender whose domain is not actually verified with the SMTP
provider will send mail that fails authentication — configure the provider and
DNS first. Self-hosters running their own relay should set AA_SMTP_FROM to a
sender on their own verified domain.
When SMTP is not configured
When AA_SMTP_HOST is unset, aa-api falls back to a logging mailer: it
does not send anything, it logs that an email would have been sent (recipient
and subject only — never the token). The deployment still boots and the reset
endpoint still behaves correctly:
POST /api/v1/auth/password/resetalways returns202 Accepted, whether or not the email exists and whether or not mail can be delivered. This is deliberate — the response must never reveal which addresses are registered.- With no SMTP configured, no reset email is actually sent; the operator sees the log line instead. Users cannot self-serve a password reset until SMTP is wired up.
The same fallback applies if AA_SMTP_HOST is set but the transport cannot be
built (a bad host or credential): aa-api logs a warning and falls back to the
logging mailer rather than refusing to start.
Related
- Configuration — environment-variable reference, including the auth and SMTP variables.
- Self-hosting — the Postgres-backed stack that unlocks native accounts.
Last updated: 2026-08-04 by Claude Code