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

# External authentication

> Delegate operator sign-in while preserving explicit identity and access boundaries.

Formation defaults to local email-and-password login. External mode delegates browser
sign-in to a trusted service that implements Formation's **launch-code protocol**.
Formation verifies its signed assertion and creates a short-lived local session.
This is not a generic OIDC discovery, OAuth login, or SAML integration; an identity
provider needs an adapter that implements the protocol below.

External mode disables local password login, first-administrator setup, and local
invitation flows. Configure and test the external service before switching the
instance. It changes operator authentication, not a conversational participant's
[identity links](https://docs.formation.texturehq.com/runtime/identity-linking.md).

## Configuration

Set these on the Formation process and restart it:

| Variable | Purpose |
| - | - |
| `MESH_AUTH_MODE` | `external` enables launch login; default is `local`. |
| `PUBLIC_URL` | Browser-facing HTTPS origin, without a path, query, user information, or fragment. |
| `MESH_INSTANCE_ID` | Instance identifier shared with the external service. |
| `MESH_AUTH_ISSUER` | Exact expected assertion issuer (`iss`). |
| `MESH_AUTH_AUDIENCE` | Exact expected assertion audience (`aud`). |
| `MESH_AUTH_AUTHORIZE_URL` | Browser authorization endpoint that starts the external login. |
| `MESH_AUTH_EXCHANGE_URL` | Server endpoint that exchanges a launch code for an assertion. |
| `MESH_AUTH_EXCHANGE_SECRET` | Secret sent as a Bearer credential to the exchange endpoint. |
| `MESH_AUTH_JWKS_URL` | Endpoint supplying public signing keys in JWKS format. |
| `MESH_CONTROL_PLANE_ORIGINS` | Optional. Extra https origins delegated setup may return a browser to; the `MESH_AUTH_AUTHORIZE_URL` origin is always allowed. |

Every field except `MESH_CONTROL_PLANE_ORIGINS` is required in external mode. `PUBLIC_URL` must be HTTPS even for a
development instance using this flow. Other endpoint URLs require HTTPS except
that configuration permits HTTP in `MESH_ENV=development` or `test`, or for hosts
ending in `.internal`. That hostname exception is a deployment assumption, not
proof that the network is private.

Register the exact callback `PUBLIC_URL/auth/callback` with the external service.
The authorization endpoint must be reachable by the browser; exchange and JWKS
endpoints must be reachable by the Formation server. Keep the exchange secret out of
browser configuration and source control.

## Launch protocol

1. The browser opens `GET /api/auth/external/start`. Formation creates a five-minute
   launch cookie containing a random state and verifier.
2. Formation redirects to the authorization URL with `instance_id`, `state`,
   `challenge` (base64url SHA-256 of the verifier), and `redirect_uri`.
3. The external service authenticates the operator and redirects to
   `/auth/callback?code=...&state=...`.
4. Formation checks the returned state, then POSTs JSON containing `code`,
   `code_verifier`, and `instance_id` to the exchange URL, authenticated with
   `Authorization: Bearer MESH_AUTH_EXCHANGE_SECRET`.
5. The exchange service returns HTTP 200 with `{"assertion":"SIGNED_JWT"}`.
   Formation verifies it, consumes its unique ID against replay, and opens a local
   session before redirecting to `/agents`.

The external service must bind its codes to the instance, redirect, and challenge
and validate the verifier during exchange. Its implementation is outside this
repository. JWKS and exchange HTTP calls each have a ten-second client timeout.

## Assertion requirements

Formation accepts an ES256 JWT with header `typ: mesh-launch+jwt` and a `kid` selecting
an EC P-256 key from the configured JWKS. The payload must contain:

* Matching `iss`, string `aud`, and `instance_id`.
* Nonempty `sub` and single-use `jti`.
* `token_use: mesh_launch` and `schema_version: 1`.
* `email_verified: true` and an email for first-time profile creation.
* A `capabilities` array and, optionally, a `mesh_role` that agree with each
  other (see [Roles](#roles)).
* `iat`, `nbf`, and `exp` compatible with the verifier's time checks: maximum
  declared lifetime 90 seconds and 30 seconds of allowed clock skew.

The external service is responsible for deciding who may launch and with which
role. Formation's operator roles and agent-access checks then govern the resulting
user's API access; see the [operator API](https://docs.formation.texturehq.com/operator-api.md).

### Roles

The assertion names the [operator role](https://docs.formation.texturehq.com/operator-api.md) the person signs in as.
Formation accepts exactly these combinations and refuses every other one:

| `mesh_role` | `capabilities` must | Signs in as |
| - | - | - |
| absent | contain `mesh:admin` | `owner` (issuers that predate the role claim) |
| `owner` | contain `mesh:admin` | `owner` |
| `agent_operator` | contain `mesh:operator` and not `mesh:admin` | `agent_operator` |

Any other `mesh_role` value, including an empty string, is refused; an unknown
role is never rounded to a known one. A non-owner assertion carries
`mesh:operator` instead of `mesh:admin` on purpose: a Formation binary that predates
the role claim requires `mesh:admin`, so it refuses that person rather than
signing them in as the default owner.

The role is applied on **every** launch, not only the first. Demoting someone
in the external service makes them an `agent_operator` at their next launch,
and because Formation reads the role per request, their existing sessions and API
tokens are demoted at the same moment. External sign-in does not apply the
roster's "last enabled owner" guard: the external service is the authority, and
keeping an owner it demoted would grant more than was asserted. Promotion to
owner clears agent grants. Agent grants are not carried by the assertion; an
owner assigns them in Formation (Settings → Operators, or
`PATCH /api/settings/operators/{id}` with `agents` only; a `role` there is
refused with `409 auth_mode`), and an `agent_operator` with no grants sees an
empty agent list. The sign-in audit row records the role and, when it changed, the
previous role — also when the session that follows is refused (a disabled
account) or fails, since the role change is already committed.

## Accounts and sessions

An external user is identified by **`(issuer, subject)`**, never by email.
Different external identities can share an email, and an external identity can
share one with a local account. Sign-in does not claim the older account, change
its password, transfer its API tokens, or rewrite its attribution. The operator
roster can therefore show distinct users with the same email.

External sessions last **15 minutes** and use the secure, HttpOnly, host-only
`__Host-mesh_session` cookie. Session records are held by Formation. Logout or local
session revocation ends that Formation session; this flow does not implement logout
from the external identity provider. Disabling a Formation operator also blocks their
session access.

Switching back to local mode does not give external users passwords. Local
password and invitation lookups consider local accounts only. Preserve existing
identity mappings and use a compatible database/binary when changing auth modes;
do not repair a login issue by merging accounts on email.

Implementation: [`internal/admin/auth_external.go` (repository)](https://github.com/TextureHQ/formation/blob/main/internal/admin/auth_external.go),
[`internal/admin/jwt.go` (repository)](https://github.com/TextureHQ/formation/blob/main/internal/admin/jwt.go),
and [`internal/config/config.go` (repository)](https://github.com/TextureHQ/formation/blob/main/internal/config/config.go).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.