> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alforse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Tenant auth — tokens, login, MFA, passkeys, SSO, and password reset.

This page covers the public tenant auth surface (`/auth/*`) used by workspace users, tenant
admins, and direct API integrations.

## The token model

A successful login returns a `TokenPair`:

<ResponseField name="accessToken" type="string" required>
  A short-lived JWT (`JWT_ACCESS_TTL`, default 15 minutes). Send it as
  `Authorization: Bearer <accessToken>` on every authenticated request. It embeds `tenantId`,
  `roleCode`, and `subjectScope` as claims — there is no separate tenant header to set.
</ResponseField>

<ResponseField name="refreshToken" type="string" required>
  A longer-lived JWT (`JWT_REFRESH_TTL`, default 30 days) used to obtain a new access token.
  Browser clients get this automatically as an httpOnly cookie scoped to `/api/v1/auth`; if
  you're integrating server-to-server, read it from the response body instead and store it
  yourself.
</ResponseField>

<ResponseField name="expiresIn" type="string" required>
  Access token lifetime, e.g. `"15m"`.
</ResponseField>

Endpoints marked **Public** below don't require a bearer token (they're how you get one, or they
predate having one). Everything else requires `Authorization: Bearer <accessToken>`.

## Register a tenant

<ParamField body="tenantName" type="string" required>2–80 characters.</ParamField>

<ParamField body="tenantSlug" type="string" required>
  Lowercase, `^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$`. This becomes your login slug.
</ParamField>

<ParamField body="adminName" type="string" required>2–80 characters.</ParamField>

<ParamField body="nameProfile" type="object">
  Optional structured name (`givenName`, `familyName`, `nameOrder`, phonetic fields for CJK
  names, `preferredDisplayName`, `nameLocaleRegion`) — see the request example.
</ParamField>

<ParamField body="email" type="string" required>Lowercased automatically.</ParamField>
<ParamField body="password" type="string" required>8+ characters.</ParamField>
<ParamField body="subjectName" type="string">Initial subject company name.</ParamField>

<ParamField body="planCode" type="string">
  Defaults to `free`. Public registration only ever accepts `free`.
</ParamField>

<ParamField body="turnstileToken" type="string">Required when Turnstile is enabled.</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.alforse.com/api/v1/auth/register \
    -H "Content-Type: application/json" \
    -d '{
      "tenantName": "Acme Leasing",
      "tenantSlug": "acme-leasing",
      "adminName": "Jordan Lee",
      "email": "jordan@acme.example",
      "password": "correct-horse-battery",
      "turnstileToken": "..."
    }'
  ```
</CodeGroup>

Returns an `AuthLoginResult` (see [`me`](#session) below for the `user` shape). Disabled in
production (`PUBLIC_REGISTRATION_ENABLED=false`) — use [Redeem a code](#redeem-a-code) instead.
**Public.**

## Redeem a code

Same response shape as register, plus a `code` field instead of `planCode` — the plan comes from
the code itself.

<ParamField body="code" type="string" required>8–80 characters.</ParamField>

<ParamField body="tenantName" type="string" required />

<ParamField body="tenantSlug" type="string" required />

<ParamField body="adminName" type="string" required />

<ParamField body="nameProfile" type="object" />

<ParamField body="email" type="string" required />

<ParamField body="password" type="string" required />

<ParamField body="subjectName" type="string" />

<ParamField body="turnstileToken" type="string" />

`POST /auth/redeem` · **Public**

## Log in

<ParamField body="tenantSlug" type="string" required />

<ParamField body="email" type="string" required />

<ParamField body="password" type="string" required />

<ParamField body="mfaCode" type="string">6-digit TOTP code, if MFA is enrolled.</ParamField>

<ParamField body="emailCode" type="string">
  6-digit backup code from [`/auth/login/mfa/email`](#mfa), used instead of `mfaCode` when the
  authenticator is unavailable.
</ParamField>

<ParamField body="mfaSetupSecret" type="string">Used during first-time MFA enrollment.</ParamField>

<ParamField body="turnstileToken" type="string" />

```bash theme={null}
curl -X POST https://api.alforse.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"tenantSlug":"acme-leasing","email":"jordan@acme.example","password":"correct-horse-battery"}'
```

Two possible response shapes:

<ResponseField name="(success)" type="AuthLoginResult">
  `{ accessToken, refreshToken, expiresIn, tenant: { id, slug, name }, user }` — see
  [session](#session) for `user`.
</ResponseField>

<ResponseField name="(MFA required)" type="MfaLoginRequired">
  `{ mfaRequired: true, enrollmentRequired, message, secret?, otpauthUrl?, emailFallbackAvailable? }`.
  If `enrollmentRequired` is true, this is the account's first login and it must enroll TOTP
  before proceeding — see [MFA](#mfa).
</ResponseField>

`POST /auth/login` · **Public**

Related:

* `POST /auth/login/resolve` — given just an `email`, a convenience lookup (no password) used by
  UIs to figure out which tenant(s) an email belongs to before showing the login form.
* `POST /auth/login/mfa/email` — re-verifies `tenantSlug` + `email` + `password`, then emails a
  6-digit backup code to use as `emailCode` on `/auth/login`.

Both are **Public**.

## Refresh and logout

<ParamField body="refreshToken" type="string">
  Optional — falls back to the `alforse_tenant_refresh` httpOnly cookie if omitted.
</ParamField>

`POST /auth/refresh` (**Public**) returns a new `TokenPair`. `POST /auth/logout` (requires
bearer token) revokes the refresh token and clears the cookie.

## Session

<ResponseField name="user" type="AuthUserSummary">
  `{ id, email, name, nameProfile, status, roleCode, subjectScope }`
</ResponseField>

<ResponseField name="tenant" type="object">`{ id, slug, name, edition }`</ResponseField>
<ResponseField name="permissions" type="object">Module → permission level map.</ResponseField>
<ResponseField name="featureFlags" type="object">Resolved plan feature flags.</ResponseField>
<ResponseField name="subjects" type="array">Subject companies visible to this user.</ResponseField>

<ResponseField name="settings" type="object">
  `{ timezone, reminderLeadDays, reminderHour, reminderEscalationDays, ownerScopeEnabled, preferences }`
</ResponseField>

`GET /auth/me` is the single call to hydrate a client session after login.

* `PATCH /auth/preferences` — language, locale, timezone, currency, calendar/measurement system,
  first day of week, date/number format. All fields optional.
* `PATCH /auth/profile` — `name` and/or a structured `nameProfile`.

## MFA

| Endpoint                        | Body               | Notes                                            |
| ------------------------------- | ------------------ | ------------------------------------------------ |
| `GET /auth/mfa/status`          | —                  | Whether TOTP is enrolled.                        |
| `POST /auth/mfa/enroll/start`   | —                  | Returns a new TOTP secret + otpauth URL to scan. |
| `POST /auth/mfa/enroll/confirm` | `{ secret, code }` | Confirms enrollment with a 6-digit code.         |
| `POST /auth/mfa/disable`        | `{ code }`         | Requires a current valid 6-digit code.           |

Tenant admins cannot disable the `MFA_REQUIRED_FOR_ADMINS` policy for themselves in production —
see [Configuration](/configuration#mfa).

## Passkeys (WebAuthn)

| Endpoint                               | Body                                     | Notes                                                                                                   |
| -------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `GET /auth/passkeys`                   | —                                        | List your registered passkeys.                                                                          |
| `POST /auth/passkeys/register/options` | —                                        | Get WebAuthn registration options.                                                                      |
| `POST /auth/passkeys/register/verify`  | `{ response, label? }`                   | `response` is the browser's `RegistrationResponseJSON`.                                                 |
| `DELETE /auth/passkeys/:id`            | —                                        | Remove a passkey.                                                                                       |
| `POST /auth/passkeys/login/options`    | `{ email, tenantSlug, turnstileToken? }` | **Public.** Start passkey login.                                                                        |
| `POST /auth/passkeys/login/verify`     | `{ flowId, response }`                   | **Public.** `response` is the browser's `AuthenticationResponseJSON`. Returns a `TokenPair` like login. |

## SSO

| Endpoint                                                  | Notes                                                                                                |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `GET /auth/sso/providers`                                 | **Public.** Lists which of `google` / `linkedin` / `dingtalk` / `lark` / `slack` are configured.     |
| `GET /auth/sso/:provider/start?tenantSlug=&next=&client=` | **Public.** Redirects into the provider's OAuth flow.                                                |
| `GET /auth/sso/:provider/callback`                        | **Public.** Provider redirects back here; Alforse redirects onward with a short-lived exchange code. |
| `POST /auth/sso/exchange`                                 | **Public.** `{ code }` → a `TokenPair` (`code` must be 16+ characters, single use, \~2 minute TTL).  |

## Invitations and password reset

| Endpoint                            | Body                                                                    | Notes                                                                                                                                                                       |
| ----------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /auth/invitations/accept`     | `{ token, tenantSlug?, name, nameProfile?, password, turnstileToken? }` | **Public.** Accepts a member invitation and sets the account's password.                                                                                                    |
| `POST /auth/password-reset/request` | `{ tenantSlug, email, turnstileToken? }`                                | **Public.** Always returns `{ ok: true, deliveryStatus }` — including when the tenant or email doesn't exist (`deliveryStatus: "not_sent"`) — to avoid account enumeration. |
| `POST /auth/password-reset/confirm` | `{ token, tenantSlug, password, turnstileToken? }`                      | **Public.** `token` is 16+ characters, single use, expires 2 hours after it was requested.                                                                                  |
