<!-- https://docs.elchi.dev/enterprise-sso | EAuth | Elchi Docs -->

# 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](/organisations): that is who buys
it, the employer.

## Setting one up

In the console, open the application's **Organisations**, then the customer's
organisation.

1. **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.
2. 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.
3. 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.
4. Paste the provider's **federation metadata** into the console, or enter
   its entity ID, sign-in URL and signing certificate by hand.
5. **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:

```js
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](/organisations) 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`, reason `account 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 gets `login_required`);
- the organisation's refresh tokens end when this takes effect
  (`session.revoked`, reason `single 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 |
|---|---|
| Email | `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.
