Enterprise SSO with SAML
When a customer says "our staff use Entra ID, can your product support that?", this is the answer. Your application does not change.
How it fits together
EAuth is the service provider. The customer's identity provider vouches for the person, and EAuth gives your application the same OpenID Connect sign-in as always, with the organisation in the tokens.
employee → your app → EAuth → customer's IdP → EAuth → your app
(Entra ID, Okta, Google)
Single sign-on belongs to an organisation: that is who buys it, the employer.
Setting one up
In the console, open the application's Organisations, then the customer's organisation.
- Verify the organisation's email domain, under Email domain. The domain is how people are sent to the provider, and the provider may only vouch for addresses in it.
- Under Single sign-on, choose Set up single sign-on. EAuth's side appears: an entity ID and a reply URL (the Assertion Consumer Service URL), and metadata to download with both and EAuth's certificate for this connection.
- The customer's administrator creates an application for it at their provider with those two values, or reads in the metadata. The provider sends the address as the NameID or as an email attribute, and signs its answers.
- Paste the provider's federation metadata into the console, or enter its entity ID, sign-in URL and signing certificate by hand.
- Turn on.
The provider must take sign-in requests by HTTP redirect, as Entra ID, Okta and Google Workspace do. Each connection has its own entity ID, reply URL and key pair, so one customer's setup never touches another's.
Who is sent to the provider
When your application names the organisation. https://eauth.me/authorize with
organization (its id or slug) sends a person who is not signed in straight
on to the provider, without anything to type. This is how a "Sign in with
SSO" button in your product works:
await auth.signIn({ organization: "acme-ag" });
The sign-in is then for that organisation or for none: a person who reaches
another organisation's provider on the way comes back with access_denied.
When the address is in the domain. On EAuth's sign-in page, Use single sign-on takes the typed address to its organisation's provider, and so does signing in without a password. Registering with an address in the domain goes to the provider too, since it creates the account there.
Everybody else signs in as before.
When your application asks for a recent sign-in. With prompt=login,
the provider is asked to authenticate the person again (ForceAuthn), not
only to confirm a session it has, and an answer with an earlier sign-in is
refused. With max_age, the provider's own session counts when it is recent
enough; when it is not, the provider is asked again, to authenticate the
person. The ID token's auth_time is when the provider says the person
authenticated, and a session ends no later than the provider says it should
(SessionNotOnOrAfter).
What happens at the first sign-in
The first answer from the provider creates the account in your application,
confirmed and without a password: the provider vouches for the address. Your
webhook hears user.created.
With Make people members at their first sign-in on, the account also
joins the organisation with the default role, and your webhook hears
organisation.member_added with via sso. Rolling out to two hundred
employees is one step, not two hundred. With it off, the account is there
but joins nothing: an invitation makes it a member, and
until then your application gets access_denied for the organisation.
An account that exists with the same address keeps its sub, its
memberships and everything your application holds for it:
- confirmed (its owner opened a link sent to the address): it is the same person, and the account is linked as it is. Its password and passkeys keep working wherever the organisation allows them; with single sign-on only, below, they no longer count for the organisation.
- never confirmed: it may be the person's own, from before they opened
the mail, or somebody may have registered the address before the
organisation set up single sign-on. So the provider's word takes it over,
and every other way in goes: the password, passkeys, two-factor
authentication and recovery codes, and whatever was signed in with them
(
session.revoked, reasonaccount claimed through single sign-on, sent even when no refresh token of yours ended). End your own session for the account when you hear it, and be wary of anything set up in it before: it may have been somebody else. Access tokens issued before run out within fifteen minutes. The address counts as confirmed from then on. Where the organisation allows passwords, the person can set one again with a reset link.
A member an administrator removed stays out, whatever the provider says, and so does a suspended one.
Single sign-on only
Members sign in to the organisation only through the provider makes it the only door, which is usually what an enterprise buying SSO expects:
- a session signed in with a password or a passkey is sent to the provider
before anything is issued for the organisation (with
prompt=none, your application getslogin_required); - the organisation's refresh tokens end when this takes effect
(
session.revoked, reasonsingle sign-on required), and a code or refresh token that came from another kind of sign-in is refused when it is used, so nothing issued before slips through; - a password typed on the sign-in page for an address in the domain is not even looked at: the page goes to the provider.
What we check
The XML signature handling is a maintained library's job. Signature wrapping has broken implementations at large vendors because XML canonicalisation is subtle in ways that look fine in testing, so it is not written by hand here. The library checks the signature against the certificates you gave (never one inside the answer), the destination, the issuer, the times and the recipient.
On top of that:
The answer belongs to this browser. Every request EAuth sends to the provider is tied to the browser that started it. An answer somebody else obtained, posted from another browser, signs nobody in. Without this, an attacker could sign a victim in to the attacker's account.
Only answers to our own requests. An answer the provider sends on its own (IdP-initiated) is refused, and so is the artifact binding.
Replay. Every request is answered once, and every assertion id is remembered until the assertion expires. A captured answer posted a second time is refused.
Audience. The assertion must name this connection's entity ID.
Domain. The asserted address must be in the organisation's verified domain. Without this, one customer's provider could vouch for an address belonging to another customer's account.
When an answer is refused, the person at the sign-in page is told that it was, and the organisation's page in the console shows why: a signature that does not match the certificate, an audience or reply URL the provider has wrong, an address outside the domain, a provider that did not authenticate the person again when asked to, or a session it ends at once. That is where to look while setting a provider up. Saving new settings clears it.
Attributes
Defaults cover Entra ID and Okta:
| Attribute | |
|---|---|
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress |
|
| Name | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name |
Where a provider differs, set the names under Attributes. Both the full name and the friendly name are matched, because providers disagree about which they send. Without an email attribute, the NameID is taken when it is an address, which is what Entra ID sends by default.
Certificates
The provider's signing certificate is shown with its expiry. Before it rolls the certificate over, paste its new metadata, or both certificates by hand: answers signed with either are accepted until the old one is removed.
Turning it off
Turned off, members sign in with a password or a passkey again. Accounts single sign-on created have neither until they reset a password. Removing the connection deletes its key pair; the provider's application for it stops working. The accounts stay.