le-app/directus/auth-contract.md

8.1 KiB

Authentication and account contract

Status labels mean:

  • Verified: directly supported by application source plus the sanitized production exports.
  • Inferred: the source intends the behavior, but a runtime binding, omitted value, or live test is missing.
  • Unresolved: the required implementation or evidence is absent.

The app uses a Directus access token as a user bearer, normally stored in the HTTP-only ma_at cookie. It stores a refresh token in HTTP-only ma_rt and a five-minute recent-authentication marker in HTTP-only ma_ra. In production these cookies are Secure, SameSite=Lax, and path / where set by the current auth routes.

Operation matrix

Operation Application route/module Directus endpoint Credential and required permission Success and important failures Status
Signup POST /api/auth/register; app/app/api/auth/register/route.ts GET /users duplicate lookup; GET /roles Users-role lookup; POST /users; then POST /auth/login The canonical static server credential is DIRECTUS_TOKEN_ADMIN_REGISTER, with legacy service/admin names as fallbacks. It must represent the Registration Bot role/policy or an equivalent identity. Registration policy grants the required role read and user read/create permissions. Validates all fields, email shape, password length >=8, and confirmation. Resolves the Users role, creates an active account with email/username/password, and performs email-based auto-login. Successful login sets ma_at and possibly ma_rt. Permission/configuration failures are logged server-side and returned as generic temporary-unavailable responses. Application flow, build, mocked request contract, and policy are verified; production static credential-to-role binding still requires deployment verification because users/values were omitted.
Duplicate-user check Same route GET /users?filter[...]&fields=id&limit=1 Registration policy requires directus_users.read for id, which is exported. A returned row or create-time uniqueness conflict produces the same generic email-or-username 409. A failed lookup stops signup with a generic 503 instead of continuing with uncertain permissions. Verified source and mocked request behavior; production credential capability requires a live test.
Email login POST /api/auth/login; app/app/api/auth/login/route.ts POST /auth/login No bearer is supplied; local email/password credentials are sent to Directus. No collection permission is used for the auth endpoint itself. On success sets one-hour ma_at, optional 30-day ma_rt, and optionally five-minute ma_ra for a reauth request. Authentication failures are deliberately collapsed to a generic 401. App IP limiter allows 20 attempts/minute per process. Verified implementation; live Directus local-auth behavior is inferred pending integration tests.
Username login Same login route; emailForUsername() in app/lib/directus.ts Static-token GET /users lookup, then POST /auth/login with resolved email The same registration credential used by signup must read directus_users.username,email; Registration policy exports those fields. Missing username gives generic 401. Lookup errors give 503. Resolved email follows email-login behavior. The shared helper accepts DIRECTUS_URL with NEXT_PUBLIC_API_BASE_URL fallback. Source, build, required policy, and mocked request contract are verified; credential binding and end-to-end production success require a deployment test.
Current user GET /api/account, GET /api/me, proxy /api/dx/users/me, middleware; relevant modules under app/app/api/ and app/middleware.ts GET /users/me with varying field lists Current user's access token. Users Self Access policy permits own-row read of id,username,first_name,last_name,email,password,avatar,location,title,description,tags,language,tfa_secret,email_notifications; Users policy additionally permits public-like id,username,first_name,avatar,location. Returns requested profile fields if field permissions allow. Middleware checks id at most once per minute and redirects to reauth on failure. /api/me requests display_name, which is not present in the exported custom user-field metadata or allowed-field lists, so that field's behavior is unresolved. Own-row permissions and route calls are verified.
Token refresh Token storage occurs in register/login and app/lib/auth-cookies.ts Expected Directus endpoint would be /auth/refresh, but no call exists ma_rt is stored, but no application route consumes it. There is no implemented refresh behavior. Expired/invalid ma_at causes middleware to clear ma_at/ma_v and redirect to sign-in. Verified missing implementation.
Logout POST or GET /api/auth/logout; app/app/api/auth/logout/route.ts None No Directus call and no credential required by the handler. Expires only ma_at. It does not revoke the Directus session and does not clear ma_rt, ma_ra, or ma_v. Verified.
Password change POST/PATCH /api/account/password; app/app/api/account/password/route.ts GET /users/me; optionally static-token GET /users; POST /auth/login; PATCH /users/me Current user token needs own-row read and directus_users.update for password; Users Self Access grants both. Username verification additionally needs Registration read permission. Requires current/new values and an eight-character new password. Verifies the current password through local login, then patches only password. External-provider rejection depends on reading provider, but provider is not in the Users Self Access read fields, so this guard may not receive the field. Successful change does not rotate or clear existing app tokens. Main path and permissions verified; provider guard and session effects unresolved.
Profile changes PATCH /api/account and PATCH /api/account/profile; UI primarily uses the latter PATCH /users/me Current user token. Users Self Access update permits first_name,last_name,email,avatar,location,password; it does not permit username. First/last name and location update directly. Email requires ma_ra=1; the marker is cleared after a successful sensitive update. UI always includes email in its payload, so its normal save path requests reauthentication even when email is unchanged. Username is classified sensitive but is never added to the patch payload and is not update-permitted. Verified.
Avatar change POST /api/account/avatar; app/app/api/account/avatar/route.ts POST /files, then PATCH /users/me Current user token. Users policy permits directus_files.create on all fields; Users Self Access permits own avatar update. Requires a configured avatar folder, <=10 MB JPEG/PNG/WebP/GIF, and recognized signature. Uploads file then links its ID. If linking fails, the uploaded file is not cleaned up. Verified except folder value and folder-specific enforcement, which are unresolved.
Reauthentication POST /api/auth/reconfirm; app/app/api/auth/reconfirm/route.ts, or login with reauth=true Optional static-token GET /users, then POST /auth/login Local credentials; username form also requires Registration directus_users.read. Successful reconfirm replaces ma_at and sets ma_ra for five minutes. It does not set/replace ma_rt. Login's reauth path sets ma_ra alongside normal token cookies. Failures are generic 401. Verified implementation; end-to-end username path remains unresolved.

Permission identity model

The export verifies these role-to-policy assignments:

  • Registration Bot role -> Registration policy.
  • Users role -> Users Self Access policy and Users policy, in that sort order.
  • Administrator role -> Administrator policy.
  • Supporter Bot role -> Supporter policy.
  • Public policy has a role-less access assignment.

Because user rows and user-specific access assignments were intentionally omitted, the evidence cannot prove which identity owns any configured static credential. A deployment test must verify each credential's effective policy rather than relying on its environment-variable name.