Single sign-on
Members sign in through your identity provider instead of through GitHub. SAML 2.0 and OpenID Connect are both supported, per organisation, and you can require one so that GitHub sign-in stops being a way into your tenant.
This is an enterprise feature. It lives in ee/web/sso, under the Antifailure
Enterprise License, and the community build does not contain it.
What you need before you start
Section titled “What you need before you start”An organisation, an owner account in it, and control of the DNS for the email
domains your people use. You cannot claim a domain by typing it: you prove you
control it with a TXT record. Without that rule, an organisation that runs its
own identity provider could assert someone@gmail.com and be linked to whoever
holds that account here.
Connecting SAML
Section titled “Connecting SAML”Your provider needs two URLs from us, and they carry a per-connection identifier rather than your organisation’s name:
Entity ID / Audience https://<your-control-plane>/sso/saml/<handle>/metadataReply URL / ACS https://<your-control-plane>/sso/saml/<handle>/acsBoth appear in the service provider metadata document, which most providers can import directly:
curl https://<your-control-plane>/sso/saml/<handle>/metadataFrom your provider you need its entity ID, its HTTP-Redirect single sign-on URL, and its signing certificate. Paste its metadata document and all three are read out of it. If it publishes two certificates because it is mid-rotation, both are kept: an implementation that holds one certificate has a planned outage every time the provider rotates.
Your provider must send an email address, either as the NameID with the
emailAddress format or as a claim. The claim names Entra ID, Okta, Google
Workspace and the generic email form are all recognised. An assertion carrying
no email address is refused, with a message saying so, because the address is
what links a person to their account here.
What an assertion has to satisfy
Section titled “What an assertion has to satisfy”Every one of these is checked, and each is a real way single sign-on is got wrong:
- The signature. Against the certificate you configured, never against a certificate carried in the document. A response signed by some other key that ships its own certificate is internally consistent and is refused.
- What was actually signed. The assertion is read back out of the exact bytes the signature covered, so a document carrying a second, forged assertion cannot make the verifier and the reader disagree. Signature wrapping is the most common way a SAML implementation that does check signatures is still broken.
- The algorithm. RSA or ECDSA with SHA-256 or better. SHA-1 is refused, and so is any HMAC: an HMAC would let anybody holding a shared secret forge an assertion.
- The audience, against the entity ID above. An assertion your provider issued for a different service is a valid signature over somebody else’s login.
- The validity window, with five minutes of clock tolerance in both directions. Configurable per connection.
- The recipient and
InResponseTo. A response answering a request nobody made is refused, and so is one answering a different request. - Replay. Each assertion identifier is remembered until it expires, using a unique constraint rather than a read followed by a write, so two requests racing with the same assertion cannot both get through.
Connecting OIDC
Section titled “Connecting OIDC”Redirect URI https://<your-control-plane>/sso/oidc/<handle>/callbackYou supply the issuer, the client ID and the client secret. The endpoints are read from the provider’s discovery document when the connection is configured, not on every login: discovery is a network call to somebody else’s service, and putting it on the critical path of every sign-in makes their brief outage your sign-in outage.
PKCE is always used, even though this is a confidential client that holds a secret. The secret stops somebody else redeeming a stolen authorization code from their own server; it does nothing about somebody feeding a stolen code into our callback. The verifier does.
state and nonce are separate values doing separate jobs and both are
required: state is round-tripped through the browser and consumed once,
nonce comes back inside the signed token and binds it to this login rather
than to some other login at the same provider.
The alg: none and algorithm-confusion attacks are both refused by an
allow-list that contains no HMAC algorithm at all, so there is no code path in
which the provider’s published public key could be used as a shared secret.
Claiming a domain
Section titled “Claiming a domain”Add the domain, then create the TXT record you are shown:
_antifailure-verification.<your-domain> TXT <token>Until it is verified, the domain routes nobody and an assertion naming an
address in it is refused with AF-EE-SSO-002. A verified claim is exclusive; an
unverified one is not, so a typo in another organisation cannot stop you
claiming your own domain.
Once verified, /sso/start?email=someone@your-domain sends the browser to your
provider. That endpoint reveals that a domain uses single sign-on and which
connection handles it, which is the same fact the redirect itself announces. It
reveals nothing about any other domain and nothing you have not verified.
Roles from groups
Section titled “Roles from groups”Map a group claim to a role and it is applied on every sign-in, so removing somebody from a group in your directory takes effect at their next login rather than never. Where several groups map, the most privileged wins: taking the first match makes the result depend on the order your provider happened to send the claims.
A role set by hand here is not overwritten by the directory. Somebody promoted in this product stays promoted.
Just-in-time provisioning respects your seat count. When the seats are full the
addition is refused with AF-EE-004 and nothing is removed. A product that
made room by evicting somebody would be turning a billing question into an
outage for a person who did nothing.
Requiring single sign-on
Section titled “Requiring single sign-on”Turn enforcement on and GitHub sign-in stops being a way into your organisation. Someone signing in with GitHub is still signed in, and lands with no organisation rather than being refused outright, which matters for the next section.
Enforcement can only be turned on for a connection that is already enabled, and turning it on issues ten recovery codes, shown once. They are not a separate step you can skip: an organisation that has required single sign-on and has no way back in is a support incident with no self-service fix, and the failure is one bad metadata paste away.
Only hashes are stored. If you lose the codes, turn enforcement off and on again from a session that still works.
Break-glass
Section titled “Break-glass”If your provider is misconfigured or down, an owner can get back in:
- Sign in with GitHub. You land signed in with no organisation.
POST /sso/break-glasswith the organisation and one recovery code.
The code is spent, cannot be used again, and a sso.break_glass.used entry is
written to the audit log with the address and user agent. Only owners: a member
with a recovery code could walk around enforcement for themselves, which is most
of what enforcement is for.
Note what this is not. It is not a second way to authenticate. There is no unauthenticated lookup keyed on a recovery code anywhere in this feature. It is a decision not to apply enforcement to a sign-in that has already happened.
Existing sessions are honoured until they expire, so turning enforcement on does not sign everybody out mid-work.
Configuration
Section titled “Configuration”| Variable | What it is |
|---|---|
AF_EE_SSO_KEY |
32 bytes, base64, encrypting the OIDC client secret and the service provider private key at rest. Generate with openssl rand -base64 32. The control plane refuses to start without it. |
Secrets are sealed with AES-256-GCM under that key, with the organisation ID authenticated as additional data. That last part is not decoration: without it a ciphertext is portable, and anybody able to write a row could copy another tenant’s encrypted client secret into their own connection and have the server decrypt it for them.
Testing against a real provider
Section titled “Testing against a real provider”Everything above is exercised by suites that build their own assertions and their own tokens. That proves the verifier refuses what it should, and it does not prove interoperability, because a fixture written by the same person who wrote the parser agrees with the parser by construction. The things that break against a real provider are the ones nobody thought to put in a fixture: the namespace prefix it happens to use, where it puts the signature, whether it sends the address as a NameID or a claim.
So there is a conformance suite that drives a real Keycloak, and a script that boots one:
eval "$(ee/web/sso/test/keycloak-up.sh)"cd ee/web/sso && node --test test/keycloak.test.tsee/web/sso/test/keycloak-up.sh --downThe eval is required rather than tidy. The script generates a certificate at
run time into a temporary directory outside the repository, and prints both
AF_KEYCLOAK_URL and the NODE_EXTRA_CA_CERTS that names a file which did not
exist until it ran.
The provider has to be HTTPS. This is not a preference: parseIdentityProviderMetadata
refuses an http single sign-on URL and discover refuses an http token
endpoint, because a token exchange over plain HTTP carries a client secret in
clear text. An earlier version of this suite documented a plain HTTP provider
and therefore could not have passed, which is worth recording because a suite
gated behind an environment variable is a suite nobody runs, and a suite nobody
runs is a claim nobody checks.
The suite is deliberately not part of just gate or CI: it boots a container
and takes minutes. Keycloak is also not a substitute for Entra ID or Okta, which
have their own quirks, and docs/plan/STATUS.md is explicit about which of the
three any given row rests on.
What is not here yet
Section titled “What is not here yet”- Encrypted assertions. Signed assertions over TLS are supported; XML encryption of the assertion body is not. If your provider requires it, say so.
- Back-channel logout. Signing out here does not sign you out of your provider.
- Signed AuthnRequests are implemented but the key has to be supplied directly; there is no UI for generating one yet.
Each of those is absent rather than half-present, and none of them is claimed anywhere in the product.