SSO — SAML and OIDC
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
SSO — SAML and OIDC
Wire Chatly into Okta, Auth0, Microsoft Entra, Google Workspace, JumpCloud, or any IdP that speaks SAML 2.0 or OIDC.
Chatly speaks SAML 2.0 and OpenID Connect. Each identity provider becomes its own SSO connection in the workspace, with its own configuration and its own email domain — the domain is what pins a login to a workspace, and it is unique across the whole platform.
Info — Which one should you pick?
If your IdP supports both, use OIDC. It is the only protocol Chatly can start a login on: a user types their work email on the Chatly login page and we redirect them to the IdP. SAML here is IdP-initiated only — users have to launch Chatly from the provider's app catalogue.
Prerequisites
Owner or Admin in the workspace. Every write on
/v1/ssorequires theworkspace:writepermission, which Owner and Admin hold.Your IdP admin console open in another tab.
The values your IdP needs from us
Read them from the API, or copy them from Workspace → SSO → "Give these to your identity provider" in the dashboard. Do not hand-assemble them.
GET /v1/sso/setup — Bearer token
{
"saml": {
"entityId": "https://api.chatlychat.com/v1/auth/sso/saml",
"connections": [
{
"id": "01920f3c-...",
"acsUrl": "https://api.chatlychat.com/v1/auth/sso/saml/01920f3c-.../acs",
"metadataUrl": "https://api.chatlychat.com/v1/auth/sso/saml/01920f3c-.../metadata"
}
]
},
"oidc": { "redirectUri": "https://app.chatlychat.com/auth/sso/callback" },
"scim": { "baseUrl": "https://api.chatlychat.com/v1/scim/v2" }
}SP-side values
Name | Type | Description |
|---|---|---|
|
| One value for the whole deployment: |
|
| Per connection: |
|
| Per connection: |
|
|
|
Warning — The ACS URL contains the connection id
You cannot register a SAML app before the connection exists in Chatly. Create the connection first (below), then read
GET /v1/sso/setupfor that connection's ACS and metadata URLs.
SAML 2.0
1. Create the connection in Chatly
Dashboard
Workspace → SSO → Add connection, protocol SAML. Four fields:
Name — display only (e.g.
Okta).Email domain — e.g.
acme.com. Assertions whose email is outside this domain are rejected.Sign-in URL — the IdP's SSO endpoint.
Signing certificate — the IdP's PEM certificate, pasted.
API
POST /v1/sso — Bearer token
POST /v1/sso
Authorization: Bearer …
Idempotency-Key: 8f1c…
Content-Type: application/json
{
"provider": "saml",
"name": "Okta",
"domain": "acme.com",
"metadata": {
"kind": "saml",
"entryPoint": "https://acme.okta.com/app/exk.../sso/saml",
"issuer": "http://www.okta.com/exk1a2b3c4",
"certificate": "-----BEGIN CERTIFICATE-----\nMIID…\n-----END CERTIFICATE-----",
"attributeMapping": { "email": "email", "name": "name" },
"jitProvisioning": true,
"jitRole": "agent"
}
}metadata.kind must equal provider, or the write is rejected — a connection that advertises SAML and behaves as OIDC is not a state the API will store.
metadata (kind: saml)
Name | Type | Description |
|---|---|---|
|
| Discriminator. Must match |
|
| The IdP's sign-in URL. |
|
| The IdP entity id — the |
|
| The IdP signing certificate, 64–16384 chars. Pasted, not fetched. Never returned on read — |
|
|
|
|
| Create a membership on first successful login. Set false for a workspace provisioned over SCIM. Default: |
|
| One of |
2. Create the SAML app in your IdP
Use the entity ID and the connection's ACS URL from GET /v1/sso/setup, or point the IdP at the connection's metadata URL and let it read them.
What we require of the assertion
Name | Type | Description |
|---|---|---|
|
| Required. |
|
| Also required. |
|
| Must be our entity id. An assertion minted for another service provider on the same IdP is refused. |
|
| 120s tolerance on assertion validity windows. |
|
| Unconstrained — we do not pin a format. |
Warning — We do not decrypt encrypted assertions
Our SP metadata is published without an SP certificate, so there is no key for an IdP to encrypt to. Leave assertion encryption off. The transport is HTTPS and the assertion is signed.
3. Map attributes
What we read off the assertion
Name | Type | Description |
|---|---|---|
|
| Read from |
|
| Read from |
|
| Configurable as |
The asserted email's domain must equal the connection's domain. This is the control that stops a workspace's own IdP from asserting [email protected] and taking over an unrelated account.
OpenID Connect
1. Create the OIDC app in your IdP
OIDC app settings
Name | Type | Description |
|---|---|---|
|
|
|
|
|
|
|
| Default |
|
|
|
2. Add the connection
You need the issuer (the discovery root, without /.well-known/openid-configuration — we append it), the client_id, and the client_secret.
Dashboard
Workspace → SSO → Add connection, protocol OIDC. Fields: name, email domain, issuer URL, client ID, client secret.
API
POST /v1/sso — Bearer token
POST /v1/sso
Authorization: Bearer …
Idempotency-Key: 8f1c…
Content-Type: application/json
{
"provider": "oidc",
"name": "Auth0",
"domain": "acme.com",
"metadata": {
"kind": "oidc",
"issuer": "https://acme.auth0.com",
"clientId": "abc123...",
"clientSecret": "shhhhh",
"scopes": ["openid", "email", "profile"],
"usePkce": true,
"jitProvisioning": true,
"jitRole": "agent"
}
}metadata (kind: oidc)
Name | Type | Description |
|---|---|---|
|
| Discriminator. Must match |
|
| Discovery root. We fetch |
|
| From the IdP. |
|
| Write-only across the API boundary. Reads return |
|
|
|
|
| Turn off only for an IdP that rejects the PKCE parameters. Default: |
|
| Same shape as SAML. OIDC claim names are standard, so the defaults are almost always right. |
|
| Create a membership on first successful login. Default: |
|
|
|
Managing connections
GET /v1/sso — Bearer token
PATCH /v1/sso/{id} — Bearer token
DELETE /v1/sso/{id} — Bearer token
GET /v1/sso returns every connection with its config redacted — a clientSecretSet / certificateSet boolean in place of the credential — plus a usable flag that is false when the stored config no longer parses.
PATCH takes exactly one field:
{ "enabled": false }That is the switch for taking a misconfigured IdP out of the login path without deleting the configuration you are mid-way through fixing. There is no other editable field: to change an issuer, certificate, or secret, delete the connection and create a new one.
The login flows
Public login endpoints
Name | Type | Description |
|---|---|---|
|
| Body |
|
| Body |
|
| Body |
|
| The IdP form-POSTs |
|
| Body |
|
| SP metadata XML for the IdP to fetch. |
OIDC (SP-initiated). User enters their work email → discover → start → IdP → the IdP redirects the browser to the dashboard callback page with code + state → that page POSTs callback → session.
SAML (IdP-initiated). User clicks the Chatly tile in their IdP → the IdP POSTs the assertion to the connection's ACS → we answer 303 to the dashboard with a handoff code → the dashboard POSTs handoff → session.
RelayState, when the IdP echoes one, is treated as attacker-influencable and is validated against the same origin allowlist as redirectTo. Anything outside CORS_ORIGINS is dropped and the user lands on the dashboard root.
Just-in-time provisioning
With jitProvisioning: true, a first successful SSO login creates the membership — no admin invite. The role is jitRole, the same for everyone on the connection; there is no group-to-role mapping.
An existing user is matched only when they already hold a membership in this workspace. We never create a membership for an existing user as a side effect of SSO, and never match a user who belongs only to some other workspace.
What we store
Per connection: protocol, display name, email domain, the protocol config (with the client secret or IdP certificate stored server-side and never returned), an enabled flag, and timestamps.
We do not store IdP passwords. The IdP issues an assertion or ID token; we verify it cryptographically and mint our own session — an access token (JWT_ACCESS_TTL_SECONDS, 15 minutes by default) and a refresh token (JWT_REFRESH_TTL_SECONDS, 30 days by default).
Certificate and key rotation
SAML. The IdP certificate is a pasted PEM. There is no metadata URL polling and no automatic swap — when your IdP rotates its signing certificate, delete the connection and create a new one with the new certificate, or logins will start failing on signature verification.
OIDC. JWKS is fetched from the discovered jwks_uri and the JOSE client handles key rotation, so rotation is automatic provided your IdP serves both old and new keys during the overlap. The discovery document itself is cached for one hour.
Troubleshooting
Warning — SAML: 'Invalid signature'
The pasted certificate no longer matches what the IdP signs with. There is no auto-refresh: re-create the connection with the current certificate.
Warning — SAML: the login is refused after a valid assertion
The asserted email's domain has to equal the connection's
domainexactly. A user atacme.co.ukcannot sign in through a connection registered foracme.com— that needs its own connection.
Warning — 'This workspace signs in through its identity provider'
That is
/v1/auth/sso/startrefusing a SAML connection. SAML here is IdP-initiated only; open Chatly from the provider's app launcher instead of typing your email on the Chatly login page.
Info — A connection shows as unusable
GET /v1/ssoreturnsusable: falsewhen the stored config no longer parses against the current schema. Delete and re-create it; a partially configured connection fails at/startrather than silently at the callback, which is where the user has already left our site.