Skip to content

Sign-in providers

The control plane signs people in with the built-in providers (Google, Apple, GitHub, and the local owner's codes) and with up to 5 org providers: an organisation's own OIDC provider (#1002). Why it works this way: Org identity.

An org provider's fields

Field What it is
id 2 to 32 of a-z, 0-9 and -, made from the name when absent (acme-okta). The provider of each sign-in it makes, so it never changes, and a removed provider's id is not used again.
name On the button: Sign in with name. 1 to 40 characters.
preset okta, entra, googleWorkspace or oidc. Fills the form; only googleWorkspace adds a check (hd).
issuer An https address. Its discovery document must name it as issuer.
clientID The client the organisation registered for this control plane.
accountClaim The claim that names a person for life: sub (the default), or oid for Entra.
tenant Entra: the tid an ID token must have. Taken from an Entra issuer when absent.
scopes openid always; email and profile when none is given.
join Who may join: invite (the default), anyone, or scim (only people SCIM added; invite while SCIM is off).
on Off refuses its new sign-ins. Paired clients go on.
order Its place on the sign-in screen, before the built-in providers.
secret Who set the client secret, when, and its fingerprint (the first 8 hex of its SHA-256). Never the value.
scim {on, match}: SCIM on or off, and the attribute that matches a provisioned person to a sign-in: externalId (the default) or userName. Set with scim/set only.
version One more at each change.
redirectURI In answers only: https://<this control plane>/v1/oidc/callback, to register at the provider.

The client secret is written with signIn/orgProviders/set and never read back. It is sealed with the keeper for secrets at v1/signin/providers/<id>.secret.json.

Saving or checking reads the discovery document and refuses one that lacks an authorization endpoint, a token endpoint and a JWKS address on https, the code flow, PKCE with S256, or a client secret at the token endpoint. Check also reads the JWKS and says how many keys it has (RSA and P-256).

A claimed domain's fields

Field What it is
domain The email domain, in lower case. One domain has one provider.
provider The org provider it is claimed for.
state notProved or proved.
txtName, txtValue The record to add: _agents.<domain> with agents-domain=<32 hex>.
lastGood, failingSince When the record was last found, and since when it is not found.
sendHere Send people here: signIn/discover names the provider for an email at the domain.
require Require this provider: a Google, Apple or GitHub sign-in at the domain cannot join, pair or renew a key.

A control plane claims 20 domains at most. Both switches need a proved domain. The control plane checks each proved domain once a day; after 7 days of failures the claim is notProved and both switches are off.

Methods

Who may call: pairing is a sign-in connection (s:) before it is paired; any is a paired client, for itself; admin is an admin's client.

Method Who What it does
signIn/providers pairing, any Gains orgProviders (id, name, order of each provider that is on) and discover (a domain sends people).
signIn/discover pairing, any {email} → {provider?, name?}.
signIn/orgStart pairing, any {provider, purpose: pair \| addToMe, returnTo: app \| web, loginHint?} → {url}, the /v1/oidc/start address. It ends after 10 minutes.
signIn/ticket pairing, any {ticket, name?, kind?} → {client} (pair) or {signIn} (addToMe). On the connection that started the flow, within 2 minutes, once. To pair, name and kind (mac, iPhone, iPad, or browser for the page's flow).
signIn/orgProviders/list admin Every org provider, with its rev.
signIn/orgProviders/set admin {provider, secret?, rev?}: add (with secret, no rev) or change (with the rev read).
signIn/orgProviders/check admin {id} → {ok, missing, jwksKeys, redirectURI}.
signIn/orgProviders/remove admin {id, rev?}: the secret is deleted, the record stays as a tombstone, its domain claims go. Its people keep their sign-ins and clients.
signIn/domains/list admin Every claim.
signIn/domains/claim admin {domain, provider} → the claim with its TXT record.
signIn/domains/check admin {domain}: look for the record now.
signIn/domains/set admin {domain, sendHere?, require?}.
signIn/domains/preview admin {domain} → the people whose built-in sign-ins at the domain Require refuses. Never the local owner.
signIn/domains/remove admin {domain}.
scim/set admin {provider, on, match?} → the provider.
scim/tokens/list admin {provider} → its tokens, newest first, with no value: id, name, made, ends, lastUse (at most once a minute, with the address), revoked.
scim/tokens/make admin {provider, name, ends?} → {token, value, url}. The value (agscim_…) is shown this once. Refused while two are live.
scim/tokens/revoke admin {id}: refused at once, at every copy.
groups/setRole admin {group, role?: "admin"}: a SCIM group gives the admin role, or none.
people/removeRequests admin The people SCIM deleted, suspended and waiting for people/remove.
people/list any Each person gains scim (what SCIM says of them), provisioned, suspended (by, provider, at, why) and roleFrom (the groups that give the role).

people/setRole refuses a role a group gives, and groups/set and groups/remove refuse a SCIM group's name and members, with managedByProvider.

Failures

Code Name When
-32077 orgSignInRequired A built-in sign-in at a domain that requires its org provider. data has provider and name.
-32082 orgProviderUnavailable No keeper for secrets, no HTTPS address, or a discovery document that lacks what the flow needs.
-32078 personSuspended The person was suspended by their org provider: an org sign-in, or a machine's hello that cannot hear user/suspended.
-32079 managedByProvider A role, or a group's name and members, that an org provider sets.

A person not known, with Who may join at invite, is refused with -32045 and "Ask an admin of this control plane for an invite".

HTTP routes

Both answer 404 on a control plane with no org provider. Every answer has Cache-Control: no-store and Referrer-Policy: no-referrer.

Route What it does
GET /v1/oidc/start?s=<state> 302 to the provider's authorization endpoint with response_type=code, client_id, redirect_uri, scope, state, nonce, code_challenge, code_challenge_method=S256, and login_hint when given (hd for Google Workspace with one domain). A state that ended answers 400.
GET /v1/oidc/callback?code&state Spends the state, exchanges the code (client_secret_basic, or client_secret_post when the provider lists only that), checks the ID token, and makes the ticket. Then 302 to agents://oidc#ticket=<t> (Mac, Remote) or to /#oidc-ticket=<t> (the web page). A refusal goes the same way with error and message (oidc-error, oidc-message on the page). An unknown or used state answers 400.

Notifications

Notification To What it says
control/cutOff each client of a suspended person {why: "suspended", provider}, then the socket closes with 4403 suspended.
control/notice admins {kind, words, user?, group?}: lastAdminKept, groupSharesOrphaned or removeRequested.
user/suspended each machine of the person, that says host.suspend/1 {user, provider, at}, or {user, off: true, at}. At every hello, before the person's settings, and within 5 seconds of a change.

A client of a suspended person is refused at connect with the reason suspended, and keeps its key.

SCIM 2.0

/scim/v2/ on the HTTPS address, with Authorization: Bearer agscim_…. Bodies are application/scim+json, at most 1 MiB. One token makes 50 requests a second at most; one more answers 429 with Retry-After.

Route Methods Notes
/ServiceProviderConfig, /ResourceTypes, /Schemas GET patch and filter (100 at most) supported; bulk, sort, etag, changePassword not.
/Users GET, POST GET: filter with eq on userName, externalId, emails.value, id or active, joined by and; startIndex, count (100 at most); attributes, excludedAttributes. POST makes a provisioned person; a second userName answers 409 uniqueness.
One user: /Users, then /<id> GET, PUT, PATCH, DELETE PATCH add, replace, remove, with or without path (name.givenName, emails[type eq "work"].value, the Enterprise extension's URN); active as true/false or "True"/"False". DELETE suspends and asks an admin; then the user answers 404.
/Groups GET, POST GET: filter with eq on displayName, externalId, id, members; excludedAttributes=members.
One group: /Groups, then /<id> GET, PUT, PATCH, DELETE PATCH members add and remove (by value, or members[value eq "x"]), replace displayName. A member that is not a user of the provider answers 400 invalidValue; a group as a member too.

Errors are RFC 7644's: 400 invalidFilter, invalidValue, invalidSyntax, invalidPath, 401, 404, 409 uniqueness, 413, 429, and 501 for Bulk, /Me and sortBy. The Enterprise User extension and any attribute not read are kept as they came. A password is never kept.

In the store

Key What it holds
v1/signin/providers/<id>.json The provider, or its tombstone (removed).
v1/signin/providers/<id>.secret.json The client secret, sealed.
v1/signin/domains/<domain>.json The claim, and when it was last checked.
v1/oidc/state/<sha256>.json, .used A started flow, its verifier and nonce sealed; spent once. Swept after an hour.
v1/oidc/tickets/<sha256>.json, .used A ticket, with the refresh token sealed; spent once. Swept after an hour.
v1/signin/refresh/<person>/<provider>.json An org sign-in's refresh token, sealed, and its last good check.
v1/scim/tokens/<sha256>.json A SCIM token: its name, provider, who made it, last use and revocation. Never its value.
v1/scim/<provider>/names/<sha256>.json Which person has a userName, so that one userName is one person at every copy.
v1/requests/remove/<person>.json A Remove request, until an admin removes the person or the suspension ends.

The audit

Each of these is an entry, with the actor and no secret: a provider added or changed (the fields changed, and "secret replaced" with its fingerprint), checked or removed; a domain claimed, checked, proved, changed, lost or removed; each org sign-in (joined, paired, or added); each sign-in that Require refused; SCIM turned on or off, each SCIM token made or revoked, and each group's role mark; each SCIM write (the actor scim:<provider> with the token's id, never its value, and the fields changed); and each suspension and its end (by scim, or by refresh with the provider).