> ## Documentation Index
> Fetch the complete documentation index at: https://docs.knoxcall.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Single Sign-On (SAML)

> Federate KnoxCall with Entra ID, Okta, Google Workspace or JumpCloud over SAML 2.0 — what KnoxCall publishes, which claims it reads, how domains are proven, and how Single Logout works.

# Single Sign-On (SAML)

KnoxCall is a SAML 2.0 **service provider**. Your identity provider authenticates the person; KnoxCall accepts the signed assertion, links or creates the account, and issues a session scoped to the workspace whose IdP asserted.

Set-up is the same four moves everywhere:

1. **Prove the email domains** your IdP will assert. See [Verifying a domain](/sso/domains).
2. **Give your IdP the KnoxCall URLs** below (or upload our metadata document).
3. **Bring the IdP's sign-in URL and signing certificate back** to KnoxCall.
4. **Test with one account** before you assign the app to everyone.

Per-provider walkthroughs: [Microsoft Entra ID](/sso/entra-id) · [Okta](/sso/okta) · [Google Workspace](/sso/google-workspace) · [JumpCloud](/sso/jumpcloud).

<Warning>
  **If you configured KnoxCall before 8 September 2026, re-upload our SP metadata to your IdP.** The document changed in three ways that an IdP only learns about by re-reading it: KnoxCall now **signs AuthnRequests**, publishes an **encryption** key so your IdP can encrypt assertions to us, and publishes a **Single Logout** endpoint. An app configured from the old document will not offer token/assertion encryption at all, may reject our now-signed requests as unexpectedly signed, and cannot have Single Logout switched on — none of which is visible from KnoxCall's side. Re-uploading the metadata (or re-importing the metadata URL, which is a snapshot in Entra, Okta and JumpCloud alike) is the whole fix.
</Warning>

## What KnoxCall publishes

Replace `acme` with your workspace slug. Every URL is on your own workspace origin.

| Field                              | Value                                                                   |
| ---------------------------------- | ----------------------------------------------------------------------- |
| **SP entity ID** (audience)        | `https://acme.knoxcall.com/saml/acme/metadata`                          |
| **Metadata document**              | `https://acme.knoxcall.com/saml/acme/metadata`                          |
| **ACS / Sign-on URL** (Reply URL)  | `https://acme.knoxcall.com/saml/acme/acs` — HTTP-POST binding           |
| **Login URL** (SP-initiated start) | `https://acme.knoxcall.com/saml/acme/login`                             |
| **Single Logout URL**              | `https://acme.knoxcall.com/saml/acme/slo` — HTTP-POST and HTTP-Redirect |

The dashboard shows the ACS, metadata and login URLs ready to copy under **Settings → Single sign-on**. The metadata document is public and unauthenticated: you can fetch it before anything is configured, which is what makes it usable as step one.

**Your metadata document is the authoritative copy of all five values.** Fetch it and read them off it rather than assembling them by hand — a self-hosted or single-tenant KnoxCall serves the same endpoints on a different origin, and the entity ID your IdP is configured with has to be byte-identical to the audience KnoxCall enforces.

<Note>
  These are the `default` connection's URLs — the connection every workspace starts with — and they are permanent: metadata you upload today never needs replacing, and adding a second identity provider later does not disturb them. The `/saml/<workspace>/…` alias **is** that connection: it resolves there whenever a connection named `default` is active, so its entity ID, its ACS and the audience it enforces are the alias's, byte for byte. A connection with any other handle publishes its own SP identity at `/sso/<workspace>/<connection>/…` instead. See [more than one identity provider](#more-than-one-identity-provider).
</Note>

## Importing your IdP's metadata

The exchange runs both ways. Your IdP publishes a SAML 2.0 metadata document of its own, carrying the entity ID, the sign-on endpoints, the logout endpoint and every signing certificate at once — and KnoxCall will read it, so that none of those has to be transcribed by hand. A truncated certificate and an entry point copied from the wrong tenant are the two commonest reasons a first login fails.

Hand KnoxCall the document itself, or the address it is published at:

```http theme={"dark"}
POST /admin/saml/connections/<id>/metadata
{ "xml": "<EntityDescriptor …>" }
```

```http theme={"dark"}
POST /admin/saml/connections/<id>/metadata
{ "url": "https://idp.example.com/…/federationmetadata.xml" }
```

One or the other, never both — *"Send either { xml } — the document itself — or { url }, where to fetch it from — but not both."* The `<id>` is the connection's id, which is in the address bar when you have the connection open.

**The call previews by default; nothing is written until you send it again with `"apply": true`.** The preview reports:

* **entry point and entity ID** — what would be stored, plus every HTTP-Redirect and HTTP-POST `SingleSignOnService` endpoint the document publishes (redirect first, which is the one KnoxCall would use);
* **the Single Logout endpoints**, same shape;
* **the certificates**, each with its fingerprint, its validity dates, whether it has expired, and a state: `new`, `already_active`, or `retired` — that last one matters, because applying the import brings a certificate you deliberately retired back into service, and the preview is where you find that out;
* **the NameID formats** the document offers. A format is a label, not the value: offering the email-address format is expected — it is the one KnoxCall's sign-in request names, and the provider guides tell you to keep it — and it says nothing about whether the NameID *value* is the person's login address, which is what [choosing the NameID](#choosing-the-nameid) is about. So when the document offers it and nothing has signed in through the connection yet, the preview carries a notice asking you to **run a test sign-in and read the NameID**, not one saying the NameID is an address. Once people have signed in, the notice is decided by the NameIDs they actually sent, exactly as on the connection's page;
* **changes** — field by field, the stored value against the incoming one.

An import never clears what the document does not carry: an export made before you switched Single Logout on has no `SingleLogoutService`, and KnoxCall leaves your existing SLO URL exactly where it was rather than reading the absence as "turn it off". It never retires a certificate either — rotation stays add-first — and it does not touch the domain list.

What the document must be: a single `<EntityDescriptor>` in the SAML 2.0 metadata namespace, carrying an `<IDPSSODescriptor>`. A whole-federation `<EntitiesDescriptor>` is refused rather than guessed at, and so is the SP half — that is the document KnoxCall gives *you*. Every refusal, with its code, is in [refusals from the IdP metadata import](/sso/troubleshooting#refusals-from-the-idp-metadata-import).

<Note>
  **The import is an API call today.** The connection's **Certificates** tab does not offer it yet — its **Add a certificate** box is the by-hand path. KnoxCall does store the URL you imported from, but **nothing re-reads it on a schedule**: when your IdP rolls its signing certificate, run the import again, or paste the new certificate. That is [rotation, with overlap](#certificate-rotation-with-overlap), unchanged.
</Note>

## What KnoxCall requires of the assertion

| Property                | Value                                                                                                                                                                               |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| NameID format requested | `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` — a format, not the value; the value should still be a stable identifier (see [choosing the NameID](#choosing-the-nameid)) |
| Assertions              | must be **signed**                                                                                                                                                                  |
| Assertion lifetime      | **5 minutes** maximum, whatever the IdP's own conditions say                                                                                                                        |
| Replay                  | each assertion ID is accepted **once** — a second POST of the same assertion is refused with `saml_replay`                                                                          |
| AuthnRequests           | **signed** by KnoxCall, RSA-SHA256 with SHA-256 digests                                                                                                                             |
| Encrypted assertions    | **accepted, never required** — encryption is your app's choice, and a plain signed assertion takes exactly the same path                                                            |
| Binding                 | HTTP-POST to the ACS                                                                                                                                                                |

Our metadata carries the SP certificate twice, as `use="signing"` (so your IdP can verify our AuthnRequests) and as `use="encryption"` (so your IdP can offer assertion/token encryption at all). It is one certificate in both roles, which is what every IdP's upload flow expects.

## The claims KnoxCall reads

**Email.** The address decides whether a person may be admitted at all — its domain must be one you have [verified](/sso/domains) — and it is how KnoxCall finds an existing account the first time someone signs in. After that, the person is recognised by their **NameID** (see [choosing the NameID](#choosing-the-nameid)). KnoxCall reads the address, in order, from: the attribute you named on the connection, then the IdP's own `email` field, then the first of these attributes that is present —

```text theme={"dark"}
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
urn:oid:0.9.2342.19200300.100.1.3     (mail)
urn:oid:1.2.840.113549.1.9.1          (emailAddress)
email
mail
emailAddress
```

— and finally the **NameID** itself when it looks like an address. That last step is a fallback, not a recommendation: send the address as its own attribute, and make the NameID a stable identifier instead. Every provider guide here maps an `email` attribute for exactly that reason.

**Display name.** Same shape: the attribute you named, then the IdP's `displayName`, then `.../claims/displayname`, `.../claims/name`, `urn:oid:2.16.840.1.113730.3.1.241`, `displayName`, `name`, `cn`. Absent, the local part of the email is used.

**Groups.** KnoxCall reads the attribute you name in the connection's `attribute_map.groups`, and otherwise the first of —

```text theme={"dark"}
http://schemas.microsoft.com/ws/2008/06/identity/claims/groups   (Entra ID)
http://schemas.xmlsoap.org/claims/Group
groups
memberOf
member_of
```

Groups only matter if you map them (below). Everything else in the assertion is ignored.

## Choosing the NameID

KnoxCall remembers each person by the **NameID** your identity provider sends, per connection, and asks that question before it looks at the email address. That is what lets someone change their address *inside KnoxCall* without ending up with two accounts.

It only protects against a rename *in your directory* if the NameID does not change when the person is renamed. If the NameID **is** the login address — the default in several identity providers — then renaming someone in your directory (a legal name change, a domain migration) changes the very value KnoxCall recognises them by. Their next sign-in looks like a stranger: KnoxCall creates a **second account and a second membership for the same person**, with its own audit trail and its own grants, and removing one of them does not remove the other. KnoxCall does not merge the two automatically, because merging on anything weaker than the NameID risks merging two different people.

So: **make the NameID a stable directory identifier** — one that stays the same when the person is renamed — and send the email address as its own attribute. Each provider guide shows where:

* [Microsoft Entra ID](/sso/entra-id#3-configure-claims) — the `objectid` source attribute.
* [Okta](/sso/okta#2-configure-saml) — a Custom application username holding the Okta user ID.
* [Google Workspace](/sso/google-workspace#3-service-provider-details) and [JumpCloud](/sso/jumpcloud#2-service-provider-details) — a custom attribute holding a value that does not change.

People who have already signed in keep the account they have; only the identifier that future sign-ins carry changes. Make the change before your next rename, not after it.

**KnoxCall tells you when a connection is set up the way that forks.** The connection's page shows a notice when the NameIDs it has admitted are addresses, and a test sign-in shows it when the assertion it just checked carries one. A metadata import sees no NameID at all — only the formats your provider's document advertises — so before anyone has signed in it says only what it can: if the document offers the email-address format, it asks you to run a test sign-in and read the NameID, because that format is expected and does not tell anyone what the value is. It is a warning, never a refusal — sign-ins keep working, and a workspace may run this configuration knowingly. It clears as people sign in again with a stable identifier.

**Prove it with a test sign-in.** KnoxCall's sign-in request names the email-address NameID format (the table above), and providers treat a requested format differently: Microsoft documents that Entra ID "honors the format in the request" when the request names one, and Okta documents that when the request includes a NameID policy, the application's Name ID format "must match it". Neither documents exactly what value accompanies that format once you have pointed the NameID at a stable identifier. So after changing it, run a sign-in from the connection's Test tab and read the NameID the verdict reports — that request carries the same format as a real sign-in, so what it shows is what your people will send. If it still shows an address, the notice stays up, and it is telling you the truth.

## Roles: what an assertion may and may not do

A SAML assertion is signed by **your** IdP, so as far as KnoxCall is concerned it is an unauthenticated door. It may confer **member** or **readonly** and nothing else:

* The connection's **default role** is what a new member gets. Saving anything else is refused: *"Default role must be one of `member, readonly`. Just-in-time provisioning may never create an owner or an admin — elevate members on the team page."*
* A **group → role map** overrides the default when the assertion carries a mapped group; if several map, the strongest wins. The same bound applies — *"Group `'Engineering'` maps to `'owner'`; an IdP group may only map to `member or readonly` — an IdP-asserted login may never mint an administrator (elevate members on the team page)."*
* **Owner and admin are always granted by a human, on the team page.** No IdP configuration, group name or attribute can produce one.

<Note>
  Group mapping decides the role a person gets when they are **first** added to the workspace. Moving someone between groups in your IdP later does not currently change their KnoxCall role — change it on the team page. Removing their access in the IdP does stop them signing in, and [Single Logout](#single-logout) ends the sessions they already hold.
</Note>

## Domain proof, in one paragraph

Your IdP signs its own assertions, so it could assert any address at all. KnoxCall therefore only lets an assertion claim addresses at domains **you have proven you control**, by publishing a TXT record we give you. Proving `acme.com` covers `eu.acme.com`; proving a subdomain never covers its parent. Without a proven domain, just-in-time provisioning is refused outright and a pre-existing account that is not already a member is refused too. **Only an SSO domain proof admits a login** — a domain you verified for *sending mail* is a different proof about a different question and does not count here. The full flow, the exact record and every refusal are in [Verifying a domain](/sso/domains).

## IdP-initiated sign-in

Off unless you turn it on, per connection.

* **SP-initiated** (the default): the person starts at KnoxCall, on `https://acme.knoxcall.com/saml/acme/login`. Bookmark it, or publish it as the sign-in link for your workspace — KnoxCall sends a signed AuthnRequest, and the assertion that comes back is bound to that request.
* **IdP-initiated**: the person clicks a tile in the IdP's app launcher and an unsolicited assertion is POSTed to the ACS. It answers no request of ours, so anyone holding one can post it into a browser at a time of their choosing. That is why it is opt-in. With it off, an unsolicited assertion is refused with `saml_idp_initiated_disabled`.

The control is the **Accept sign-ins started at the identity provider** checkbox, under *Who may sign in* on the connection's **Connection** tab (`allow_idp_initiated` on the API). Tick it and save.

Use the page rather than the API for this. `PUT /admin/saml` and `PUT /admin/saml/connections/<handle>` are whole-object **replaces**, not patches: a body carrying only `allow_idp_initiated` writes NULL over every field it omits. If you do drive the API, send the whole connection — and send a real boolean, because the string `"true"` is refused: *"Allow IdP-initiated sign-in must be true or false."*

On the IdP-initiated flow only, KnoxCall honours **RelayState** as a deep link — but only as a relative path inside the app. An absolute URL is dropped and the person lands on the default page.

## More than one identity provider

A workspace may run **several active connections at once** — one per identity provider, or one per business unit that brought its own. They do not authenticate for each other: each connection has its own SP identity, and an assertion is only ever accepted by the connection whose audience it names.

The first connection every workspace has is `default`, and it owns the `/saml/<workspace>/…` URLs above. **Every other connection publishes at its own path**, built from its **Handle** on the Connection tab:

| Field                       | Value, for a connection whose handle is `entra`     |
| --------------------------- | --------------------------------------------------- |
| **SP entity ID** (audience) | `https://acme.knoxcall.com/sso/acme/entra/metadata` |
| **Metadata document**       | `https://acme.knoxcall.com/sso/acme/entra/metadata` |
| **ACS / Sign-on URL**       | `https://acme.knoxcall.com/sso/acme/entra/acs`      |
| **Login URL**               | `https://acme.knoxcall.com/sso/acme/entra/login`    |
| **Single Logout URL**       | `https://acme.knoxcall.com/sso/acme/entra/slo`      |

Two things worth knowing before you add the second one:

* **The `/saml/<workspace>/…` alias is the `default` connection.** It resolves to the connection named `default` whenever one is active, so the first IdP keeps working exactly as it did — that connection's own URLs *are* the alias, and nothing you configured before adding the second IdP has to change. The alias answers `409` with `saml_connection_ambiguous` in one case only: **two or more connections are active and none of them is named `default`.** Then it will not guess, and each connection has to be reached at its own `/sso/<workspace>/<handle>/{metadata,acs,login,slo}`. The practical rule follows from that: keep your first identity provider on the connection named `default`, and give the second one its own handle and its own URLs.
* **The dashboard shows each connection's own URLs.** Open the connection under **Settings → Single sign-on** and its **Certificates** tab lists five values for *that* connection — SP entity ID, sign-on / ACS URL, login URL, Single Logout URL and metadata URL — each with a copy button, above a link that opens its metadata document. A connection whose handle is not `default` shows its `/sso/…` set there, not the alias. The metadata document stays authoritative for all five: `https://acme.knoxcall.com/sso/acme/<connection>/metadata` is public, so a browser will do, and `GET /admin/saml/connections/<id>/sp-metadata` returns the same document as a download for an authenticated owner or admin — which is the reason to prefer uploading it to your IdP over typing anything.

### If your IdP was configured with an identifier of its own

The connection form has an optional **SP entity ID**. Leave it empty and KnoxCall uses this connection's own metadata URL, which is what the metadata document publishes and is unique by construction. Set it only when your identity provider already knows KnoxCall by another identifier — and then two rules apply, because that one string becomes both the issuer KnoxCall sends and the audience every assertion must name:

* **It may not be a KnoxCall-shaped URL.** Any value whose path sits under `/saml/` or `/sso/`, on any host, is refused: *"That service-provider entity ID is shaped like one of ours: its path is under /saml/ or /sso/, which is the address family KnoxCall computes SP identities in…"* Otherwise a connection could claim the identity another connection's sign-on URL enforces, and an assertion minted for that one would be accepted here.
* **It must be unique across every KnoxCall workspace**, not only yours: *"That service-provider entity ID is already in use. An SP entity ID is a name in one global namespace…"* The refusal deliberately names no other workspace or connection — you are told the string is taken and nothing more, so pick another identifier your IdP owns, or empty the field and take the computed one.

## Single Logout

KnoxCall supports SLO in **both** directions at one URL per connection, on both bindings. For the `default` connection that is the alias, `https://acme.knoxcall.com/saml/acme/slo`; every other connection ends its own sessions at `https://acme.knoxcall.com/sso/acme/<connection>/slo`. A logout message only ever ends the sessions the connection it arrived at authenticated.

* **Your IdP ends the session.** A signed `LogoutRequest` naming the person's NameID (and, when present, the SessionIndex) ends the sessions **this workspace's** IdP authenticated — access tokens stop being accepted and the refresh family is revoked. Sessions the same person holds in a *different* workspace are untouched: that workspace's IdP and policy never participated.
* **The person signs out of KnoxCall.** If their session was minted by SAML, KnoxCall answers the sign-out with a signed `LogoutRequest` aimed at your IdP's SLO endpoint. The KnoxCall session is revoked either way — never conditionally on the IdP leg.

Three requirements:

* Your IdP's SLO URL is a **connection setting** (`idp_slo_url`) and must be **https**. It is a real destination — a browser is redirected there carrying a NameID, which is usually a person's email address. Empty means "this connection does not do SP-initiated Single Logout"; it never falls back to the sign-in URL.
* A redirect-binding logout message **must be signed**. An unsigned one is refused (`saml_logout_unsigned`) — otherwise anyone who could reach the URL could sign out any user whose email address they could guess.
* Logout messages are accepted for **5 minutes** and **once** (`saml_logout_stale`, `saml_logout_replay`).

Single Logout is deliberately **not** plan-gated: refusing to end a session because a subscription lapsed is the wrong direction of that control.

## Certificate rotation, with overlap

Your IdP's signing certificate expires. Rotate it **add-first** — never retire-then-add.

1. **Add the new certificate while the old one is still active.** Paste it into **Settings → SSO →** your connection **→ Certificates → Add a certificate**. Pasting a certificate **adds** it; it does not replace the one already there.
2. **Check that both are listed as active.** The Certificates tab shows every certificate on the connection with its fingerprint and its expiry date. An assertion signed by *any* active certificate verifies, so while both are listed you are covered whichever key your IdP signs with.
3. **Switch your IdP over to signing with the new key.** In Entra ID, Okta, Google Workspace and JumpCloud this is a setting on the application, not something KnoxCall can trigger.
4. **Sign in once**, to confirm the new certificate is the one being used.
5. **Retire the old certificate.** It stops being able to verify an assertion or a logout message immediately; the row stays for the audit trail, with the time you retired it.

There is no window in which sign-in is down, because at every step at least one active certificate matches the key your IdP is signing with.

### What happens if you retire the last one

You cannot. The writer refuses it: *"This is the connection's only active IdP certificate. Add the new certificate first, then retire this one."* The same rule applies when you try to switch a connection on without one: *"An SSO connection cannot be activated without at least one active IdP certificate."* — and one with no sign-in URL: *"An SSO connection cannot be activated without an IdP entry point."*

That refusal protects you from the obvious mistake, and only from that one. It cannot stop you from keeping a single active certificate that your IdP has already stopped using — which is why the expiry mail below exists.

### The expiry email

Nothing re-reads your IdP's metadata on a schedule (see the note under [importing your IdP's metadata](#importing-your-idps-metadata)), so the certificate KnoxCall holds is the one you last gave it. What KnoxCall does watch is that certificate's own expiry date: it reads `not_after` on every active certificate of every active connection and emails the workspace's **owners and admins** at **30 days**, **14 days**, **3 days**, and again **once it has expired**. Each of those four fires once per certificate. Nobody has to be looking at the page.

Two things the mail will tell you, because they lead to different actions:

* **Another active certificate outlives this one.** Sign-in keeps working. Finish the rotation — confirm your IdP is signing with the newer certificate, then retire this one.
* **This is the only active certificate.** Sign-in still works *today*, and here is the exact reason it will stop: KnoxCall does not reject an assertion because the signing certificate has expired — but your IdP will stop signing with that key when it rotates, and KnoxCall then has nothing to check the signature against. From that moment every SAML sign-in to the workspace fails, and the failure reads as a signature mismatch, not as an expiry. **The remedy is step 1 above: add the new certificate.** If your IdP has already rotated, get its current signing certificate — paste it into **Add a certificate**, or re-run [the metadata import](#importing-your-idps-metadata), which reads every signing certificate the document carries — add it, and sign-in resumes on the next attempt. There is nothing else to switch back on.

If a certificate was uploaded before KnoxCall recorded expiry dates, its expiry is filled in the first time the check runs. A certificate KnoxCall cannot parse at all is reported to the operator rather than assumed healthy.

### Doing it over the API

Adding a certificate out-of-band is `POST /admin/saml/connections/<id>/certificates`; retiring one is `DELETE /admin/saml/connections/<id>/certificates/<certId>`. Both need the **owner or admin** role and a re-authentication in the last 5 minutes, like every other write on this surface.

## Plan and permissions

* **SSO is an Enterprise feature.** If the subscription lapses, **existing connections keep authenticating your members** — only configuration is refused: *"Single sign-on configuration requires a current Enterprise plan. Existing SSO connections keep authenticating your members; adding or changing one needs the subscription brought up to date."*
* Configuring SSO needs the **owner or admin** role plus a re-authentication in the last 5 minutes. Anyone else is refused: *"Managing the SAML connection requires the owner or admin role — this row is the tenant's authentication policy: it names the identity provider allowed to assert who your users are, and the email domains whose accounts it may claim."*
* KnoxCall staff **cannot** write your SSO configuration. It is refused at the same surface an operator would have to use.

## Rate limits and safety rails

* The ACS and the SLO endpoint each accept **30 requests per minute** per (workspace, source address) pair, with separate budgets so a logout storm cannot spend your sign-in allowance.
* The ACS is public, so refusals are deliberately uninformative to the caller; the detail is in your workspace's audit and security events.
* The `/saml/<slug>/…` alias resolves to the connection named `default`, however many others are active alongside it. It refuses to guess — `409`, `saml_connection_ambiguous` — only when two or more connections are active and **none** carries that name. The connection-scoped `/sso/<workspace>/<connection>/…` URLs name one connection and are never ambiguous.

## When it does not work

Every refusal message and every error code, with what to do about each: [SSO troubleshooting](/sso/troubleshooting).
