Application authentication
MAIL-73 added the Duckbill™ application principal. MAIL-184 issues that principal after Sign in with Apple. This is not Microsoft Graph or Gmail OAuth. Provider tokens stay out of the app session and are MAIL-74's responsibility.
See ADR 0005 for the cookie and CSRF shape, and ADR 0028 / Sign in with Apple for the IdP.
User
Migration 0002_users.sql creates inbox_agent.users:
| Column | Notes |
|---|---|
user_id | UUID primary key |
identity_subject | Apple sub (MAIL-184) or owner ops subject. Not a mailbox provider id |
status | active or disabled |
created_at / updated_at | Timestamps |
Row-level security is enabled. anon and authenticated are revoked when those roles exist. The table is not on the Supabase Data API.
Repository: createUserRepository in @inbox-agent/persistence.
Session
SESSION_SECRET (32+ characters, already required by loadRequiredEnv) signs an HTTP-only cookie named ia_session.
Cookie flags: HttpOnly, Path=/, SameSite=Lax, Max-Age 12 hours. Secure is set when the issuer asks for a secure cookie (preview and production).
Claims allowlist:
v, userId, iat, exp, csrf
The public session view returned to API clients is { userId, csrfToken } only. GET /api/me returns that view. The site header reads /api/me and shows the signed-in userId (or Sign in when there is no session).
Product routes
Unauthenticated /inbox, /inbox/*, and /accounts redirect to /login. Do not leave a 401 triage shell as the only UX. After Sign in with Apple — or the owner ops escape POST /api/session/owner when OWNER_SESSION_ENABLED is true — triage loads. The owner session is not a second product IdP.
There is no product sign-out route yet. The ia_session cookie expires after 12 hours (Max-Age). Clearing the cookie in the browser ends the session.
CSRF
Mutating methods (POST, PUT, PATCH, DELETE) must send X-CSRF-Token matching the csrf claim inside the signed cookie. GET/HEAD/OPTIONS skip CSRF but still require a valid session.
API contract
Every user-facing API call goes through authenticateRequest / handleProtectedApi. Missing or invalid cookies return:
{
"data": null,
"error": {
"code": "UNAUTHENTICATED",
"message": "Authentication required",
"retryable": false
}
}
CSRF failures return 403 with UNAUTHORIZED. Neither response includes provider tokens.
Logging
Reuse MAIL-69 redaction. cookie, token, and authorization field names are already redacted by @inbox-agent/security. Do not add a second redaction implementation.
