# SAML SSO (/api-v2/sso)



Enterprise SSO lets your team sign in to the Zumik console through your own identity provider (Okta, Entra ID, Google Workspace, …) over SAML 2.0. You register a connection, hand the IdP Zumik's SP metadata, and your users authenticate at `/auth/saml/{id}/login`. Zumik verifies the IdP's signature on every assertion before establishing a session.

Connection management (`/v2/sso-connections`) requires an admin API key. The login flow itself (`/auth/saml/...`) is unauthenticated — it's the browser-facing SSO endpoints.

## Register a SAML connection [#register-a-saml-connection]

`POST /v2/sso-connections`

<ParamField body="protocol" type="string">
  `saml`.
</ParamField>

<ParamField body="display_name" type="string">
  A label for the connection.
</ParamField>

<ParamField body="saml" type="object">
  `idp_entity_id`, `idp_sso_url`, and `idp_certificate` (the IdP's base64 X.509 signing certificate). The certificate is what assertion signatures are verified against.
</ParamField>

<ParamField body="jit_provisioning" type="boolean" default="false">
  Create a user on first SSO login if their email is unknown.
</ParamField>

The response includes the connection `id` (`sso_...`). Connections are managed with `GET/DELETE /v2/sso-connections/{id}`.

## Hand the IdP your SP metadata [#hand-the-idp-your-sp-metadata]

`GET /v2/sso-connections/{id}/saml-metadata`

Returns the Service Provider metadata XML (entity id and ACS URL) to paste into your IdP. The SP advertises `AuthnRequestsSigned="false"` and `WantAssertionsSigned="true"` — Zumik does not sign AuthnRequests, but it requires and verifies a signature on the assertion.

## The login flow [#the-login-flow]

```
Browser → GET /auth/saml/{id}/login → 302 to IdP (AuthnRequest, redirect binding)
IdP     → POST /auth/saml/{id}/acs   (signed SAMLResponse, RelayState)
Zumik   → verify signature + conditions + replay → mint console session → 302 to console
```

### Start login [#start-login]

`GET /auth/saml/{id}/login`

Builds a SAML 2.0 AuthnRequest (HTTP-Redirect binding, DEFLATE-encoded), records a server-side relay-state entry to bind the eventual response to this request (`InResponseTo`), and `302`-redirects the browser to your IdP's SSO URL.

### Assertion Consumer Service [#assertion-consumer-service]

`POST /auth/saml/{id}/acs`

The IdP posts the form fields `SAMLResponse` (base64) and `RelayState` here. Zumik:

1. **Verifies the XML signature** of the assertion against the connection's `idp_certificate`, and validates the audience, time window, and `InResponseTo`.
2. **Prevents replay** — an assertion id is accepted once; a re-post is rejected.
3. **Establishes the session** — the subject NameID (an email) is matched to a user (or provisioned when `jit_provisioning` is on), and an httpOnly console session cookie is set before redirecting to the console.

A response that fails verification returns `403` with no detail; a binary built without SAML support returns `501`.

### Logout [#logout]

`GET /auth/saml/{id}/logout`

Clears the console session cookie (SP-side single logout) and redirects to the console.

## Errors [#errors]

| Status | Code                 | When                                                                                |
| ------ | -------------------- | ----------------------------------------------------------------------------------- |
| 401    | `invalid_api_key`    | Managing connections without a valid API key.                                       |
| 403    | `insufficient_scope` | Managing connections without an `admin`-scoped key.                                 |
| 403    | —                    | An assertion failed signature/condition verification, or was replayed (login flow). |
| 404    | —                    | Unknown or inactive SAML connection (login flow).                                   |
| 501    | —                    | The server was built without the SAML feature.                                      |

See the full table on [errors](/api-reference/errors).
