Configuration
Set these on the Formation process and restart it:
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
- The browser opens
GET /api/auth/external/start. Formation creates a five-minute launch cookie containing a random state and verifier. - Formation redirects to the authorization URL with
instance_id,state,challenge(base64url SHA-256 of the verifier), andredirect_uri. - The external service authenticates the operator and redirects to
/auth/callback?code=...&state=.... - Formation checks the returned state, then POSTs JSON containing
code,code_verifier, andinstance_idto the exchange URL, authenticated withAuthorization: Bearer MESH_AUTH_EXCHANGE_SECRET. - 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.
Assertion requirements
Formation accepts an ES256 JWT with headertyp: mesh-launch+jwt and a kid selecting
an EC P-256 key from the configured JWKS. The payload must contain:
- Matching
iss, stringaud, andinstance_id. - Nonempty
suband single-usejti. token_use: mesh_launchandschema_version: 1.email_verified: trueand an email for first-time profile creation.- A
capabilitiesarray and, optionally, amesh_rolethat agree with each other (see Roles). iat,nbf, andexpcompatible with the verifier’s time checks: maximum declared lifetime 90 seconds and 30 seconds of allowed clock skew.
Roles
The assertion names the operator role the person signs in as. Formation accepts exactly these combinations and refuses every other one:
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),
internal/admin/jwt.go (repository),
and internal/config/config.go (repository).