Members of an organisation can sign in through its own identity provider (Okta, Microsoft Entra ID, Google Workspace, Keycloak, or any provider that supports OpenID Connect) instead of a password. Single sign-on is a Business plan feature.
Access: Session token only. API keys get 403 with insufficient_scope.
Role: Organisation owner or admin, for the routes under /org/sso.
Set up #
- In your identity provider, create an OpenID Connect web application. Use the
redirectUrifromGET /org/ssoas its redirect URI, and note the client id and client secret. - Add and verify every email domain your members use.
- Save the provider with
PUT /org/sso.
GET /org/sso
PUT /org/ssoPUT takes:
| Field | Type | Description |
|---|---|---|
issuer | string | The issuer URL of the provider. We read <issuer>/.well-known/openid-configuration. It must use https. |
clientId | string | The client id. |
clientSecret | string | The client secret. Leave it out to keep the stored one. It is stored encrypted and never returned. |
enabled | boolean | Turn sign-in on. Needs at least one verified domain and a reachable provider. |
enforce | boolean | Members can no longer sign in with a password. Owners always can, so you cannot lock yourself out. Needs enabled. |
autoProvision | boolean | Create an account and membership the first time someone from a verified domain signs in. Default true. When off, only existing members can sign in. |
defaultRole | string | member (default) or admin: the role of people created by autoProvision. |
Both answer 200 with the settings: the fields above (without clientSecret, with clientSecretSet), domains, pendingDomains, redirectUri and available, which says whether the plan includes single sign-on.
The organisation’s two-factor requirement does not apply to single sign-on: your identity provider decides how people prove who they are.
Domains #
Only people whose email address is on a verified domain can sign in, and a domain can belong to one organisation.
POST /org/sso/domains
POST /org/sso/domains/{domain}/verify
DELETE /org/sso/domains/{domain}POST /org/sso/domains takes {"domain": "example.com"} and returns the settings, where pendingDomains holds the DNS record to publish:
{"pendingDomains": {"example.com": {"record": "_continuous-translations.example.com", "value": "ct-domain-verification=3f9c…"}}}Add that value as a TXT record on that name, then call verify. It answers 422 with domain_not_verified until the record is visible, and 409 with domain_taken if another organisation verified the domain first. Removing the last domain turns single sign-on off.
Signing in #
These two routes need no token. The editor calls them; they are documented for people building their own client.
POST /auth/sso/discover
POST /auth/sso/start
POST /auth/sso/callbackdiscover takes email and answers {"sso": true} when the address is on a domain whose organisation has single sign-on turned on, and {"sso": false} otherwise. The editor’s sign-in form uses it to ask for a password or not.
start takes email and binding, a random string of at least 16 characters that the browser keeps (for example in session storage). It answers {"url": "…"}: send the browser there. The provider redirects back to the redirectUri with code and state; send those and the same binding to callback, which answers like POST /auth/login with a token, expiresAt and user. The binding makes sure only the browser that started the sign-in can finish it.
Audit log #
Setting changes record org.sso_changed, org.sso_domain_verified and org.sso_domain_removed; each sign-in records auth.sso_login; a member created on first sign-in also records org.member_joined.
Errors #
| Status | error | When |
|---|---|---|
| 400 | invalid_state | The state is forged, expired (10 minutes) or was started in another browser. |
| 401 | sso_failed | The provider refused the code, or the ID token’s issuer, audience, expiry or nonce is wrong. |
| 402 | plan_limit | The plan does not include single sign-on. |
| 403 | email_not_verified | The provider says the email address is not verified. |
| 403 | domain_not_allowed | The email address is not on a verified domain. |
| 403 | not_a_member | autoProvision is off and the person is not a member. |
| 403 | sso_required | (POST /auth/login) The organisation requires single sign-on. |
| 404 | sso_not_configured | No organisation with single sign-on owns the email’s domain. |
| 422 | invalid_request | A field is invalid; fields.issuer is invalid, unreachable or not_openid, and fields.sso is incomplete or no_verified_domain. |
See Errors for the error format.