ClouisleClouisle

Configure Single Sign-On

Configure OIDC, SAML 2.0, or CAS login connections and manage the SSO user lifecycle

SSO is managed by a Super Admin under Site settings → SSO: turn on the global switch first, then add providers one by one. Each provider's protocol configuration is a JSON document whose fields depend on the protocol; prepare the identity provider's callback address, client credentials, and certificate before saving.

Prerequisites:

  1. sso_enabled is on in site settings (off by default) — otherwise the login page never fetches the provider list.
  2. The operator holds admin:sso:read (view) or admin:sso:update (create, edit, delete, test, unlink).
  3. The identity provider can reach Clouisle's public backend address, or the callback never arrives (see the next section).

Callback Address

The callback address you register with the identity provider is:

{BACKEND_ORIGIN}/api/v1/sso/callback/{provider_name}

BACKEND_ORIGIN is the public origin of the request itself — the domain the browser actually reached the backend through (with a reverse proxy, the forwarded external address). A :3000 port here is automatically rewritten to :8000. It is not API_BASE_URL (an internal service address) and is unaffected by FRONTEND_URL; getting it wrong causes a 404 callback or a redirect to the wrong domain.

The code keeps a BACKEND_URL override hook, but that setting is not declared in the settings class, so the BACKEND_URL environment variable cannot override it — the origin always comes from the request. Make sure the identity provider can reach that public domain.

{provider_name} must exactly match the provider name registered in Clouisle (case-sensitive). A mismatched callback address is the most common cause of SSO login failure.

Adding a Provider

In Site settings → SSO, click Add Provider. The dialog has three tabs — Basic Info / Configuration / Attribute Mapping:

FieldRequiredConstraint
Provider NameYes^[a-z][a-z0-9_-]*$: starts with a lowercase letter, then lowercase letters, digits, -, _ only; max 100; globally unique (a duplicate returns 6305)
ProtocolYesoidc (OAuth2/OIDC) / saml2 (SAML 2.0) / cas (CAS)
Display NameYesShown in lists and on the connected-accounts card; max 100
Button TextNoLogin button label, max 50, e.g. "Sign in with Google"
Icon URLNoMust start with http:// or https://; max 512
Enabled—When off, the provider disappears from the login page
Allow Signup—Whether this provider may auto-create users
Require Approval—Whether users created through this provider need admin approval

Provider name and protocol cannot be changed after creation (both are disabled when editing). Name it to match the identity provider configuration — renaming means recreating the provider.

Protocol Configuration

The Configuration tab is a free-form JSON editor pre-filled with a template when you pick a protocol. Field names must match the tables below; a typo is not rejected on save and only surfaces as a configuration error during login.

OAuth2 / OIDC

FieldRequiredDescription
client_idYesClient ID issued by the provider
client_secretYesClient Secret issued by the provider
authorization_urlYesAuthorization endpoint
token_urlYesToken exchange endpoint
userinfo_urlYesUser info endpoint (its response is what login reads)
scopesNoSpace-separated scopes, default openid email profile

Login uses the authorization-code flow with PKCE (S256) and generates a nonce. Common provider endpoints:

{
  "client_id": "your-github-client-id",
  "client_secret": "your-github-client-secret",
  "authorization_url": "https://github.com/login/oauth/authorize",
  "token_url": "https://github.com/login/oauth/access_token",
  "userinfo_url": "https://api.github.com/user",
  "scopes": "read:user user:email"
}

The current implementation has no issuer_url field and performs no ID-Token signature/JWKS validation: user information always comes from the userinfo_url response (validate_id_token exists but has no callers). As a result, a wrong client_secret only shows up when tokens are actually exchanged — see Testing the Connection.

SAML 2.0

FieldRequiredDescription
sp_entity_idYesService Provider Entity ID (this instance's identifier)
idp_entity_idYesIdP Entity ID
sso_urlYesIdP Single Sign-On URL (HTTP-Redirect binding)
x509_certYesIdP X.509 certificate, PEM content but without the -----BEGIN/END CERTIFICATE----- lines
acs_urlYesACS address; the runtime callback URL overrides it, so this is a placeholder/reference
slo_urlNoSP Single Logout URL
idp_slo_urlNoIdP Single Logout URL
name_id_formatNoNameID format, default urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
{
  "sp_entity_id": "clouisle",
  "idp_entity_id": "https://idp.example.com/metadata",
  "sso_url": "https://idp.example.com/sso",
  "x509_cert": "MIIC...truncated...",
  "acs_url": "https://your-domain.com/api/v1/sso/callback/azure-ad"
}

strict mode is enabled, so SAML responses must pass signature validation; a large clock skew between the IdP and Clouisle fails immediately.

slo_url / idp_slo_url are only written into the SAML configuration — no SP-initiated single logout flow is implemented, so logging out at the IdP does not terminate the Clouisle session.

CAS

FieldRequiredDescription
server_urlYesCAS server base URL
versionNoProtocol version 1 / 2 / 3, default 3, selecting the validation endpoint (/validate, /serviceValidate, /p3/serviceValidate)
service_urlNoHas no effect: the service parameter always uses the runtime callback address

The CAS user identifier comes from <cas:user> in the validation response; version 3 also reads attributes from <cas:attributes> for attribute mapping.

Attribute Mapping

The Attribute Mapping tab is another JSON document that maps provider response data onto the fields Clouisle needs:

Clouisle fieldPurpose
emailAccount email (required; missing it fails the login, see below)
usernameUsername; duplicates get a numeric suffix
avatar_urlAvatar URL

Two path styles:

  • Dot path (default): {"email": "email", "username": "profile.name"} reads nested objects.
  • JSONPath: starting with $, suited to arrays or filtering, e.g. {"email": "$.emails[0].value"}; the first match wins.

The default mapping when adding a provider:

{
  "email": "email",
  "username": "name",
  "avatar_url": "picture"
}

GitHub's payload differs from the OIDC standard claims, so change it to {"email": "email", "username": "login", "avatar_url": "avatar_url"}.

Mapped values are merged with the provider's raw response, and a same-named raw field wins. In other words the mapping only helps for fields that do not already exist in the raw payload; if the provider already returns email, the mapped email is overwritten. Log the raw response first when chasing mismapped fields.

The final data must contain a non-empty email, or new-user creation fails with a validation error (email_required). The provider user identifier comes from OIDC's sub/id, SAML's NameID, or CAS's <cas:user>; an empty identifier also fails the login.

Testing the Connection

Test Connection in the list calls POST /api/v1/admin/sso/providers/{id}/test:

  • Server-side it only builds an authorization URL locally (with a placeholder callback) to check that the required keys are present, then returns the first 100 characters of that URL.
  • It never contacts the identity provider, so a wrong client secret, a mismatched callback address, or an expired certificate are all invisible to it — verify with a real login.
  • A status: error response only means a key is missing or a value cannot be used to build an authorization URL.

Admin Permissions and API

ActionEndpointPermission
List all providers (with config)GET /api/v1/admin/sso/providersadmin:sso:read
Create / update / deletePOST / PUT / DELETE /api/v1/admin/sso/providers[/{id}]admin:sso:update
Test configurationPOST /api/v1/admin/sso/providers/{id}/testadmin:sso:update
Unlink any user's connectionDELETE /api/v1/admin/sso/connections/{connection_id}admin:sso:update
Providers for the login pageGET /api/v1/sso/providersPublic
Start login / callbackGET /api/v1/sso/login/{provider_name}, GET /api/v1/sso/callback/{provider_name}Public
Unlink your own connectionDELETE /api/v1/sso/connections/{connection_id}Any signed-in user

When sso_enabled=false, GET /api/v1/sso/providers returns an empty array even if providers are enabled — if the login page has no buttons, check the global switch first.

General Flow

  1. Enable SSO in site settings and adjust the global policies (allow password login, auto-create, require approval, match by email).
  2. Add a provider: fill in the basic info and pick a protocol.
  3. Enter the protocol JSON on the Configuration tab.
  4. Map email, username, and avatar to provider fields on the Attribute Mapping tab.
  5. After saving, add the callback address Clouisle shows to the identity provider's allowlist/redirect URIs.
  6. Verify three cases in an incognito window: a new user, an existing user, and a failing callback.
SSO connection configuration
SSO connection configuration

User Lifecycle

Global settings and provider switches combine with OR semantics:

OutcomeRule
Link an existing accountsso_match_by_email=true (default) and the returned email matches an existing user; linking also ensures that user joins the default team
Create an accountEither the global sso_auto_create_users or the provider's Allow Signup is on
Creation refusedGlobal auto-create off and the provider's Allow Signup off → 6302 (sso_registration_disabled)
Approval requiredEither the global sso_require_approval or the provider's Require Approval is on → the account is pending and cannot log in
SSO-onlyGlobal sso_allow_password_login=false → password login returns 6306

Newly created SSO users behave as follows:

  • They have no local password (hashed_password is an empty string), so password login is impossible.
  • Their email is marked verified immediately (the provider's email is trusted).
  • The username comes from the mapped username; if missing, the local part of the email; if that is missing too, user_<first 8 chars of the provider user id>; duplicates get a numeric suffix.
  • The role comes from the provider's default_role_id (API-only; there is no field for it in the UI), falling back to the global default role; the user is then added to the default team via default_team_id.

Common Errors

CodeMeaning
6300Provider not found
6301SSO login session expired (sessions last 10 minutes; abandoning authorization mid-way expires it)
6302Registration not allowed for this provider (both auto-create and Allow Signup are off)
6303The provider rejected authentication or callback validation failed
6304Missing configuration / unsupported protocol
6305Provider name already exists
6306The site is SSO-only and password login is disabled

On a failed callback the backend redirects to /sso-callback?error=..., where error may be sso_provider_not_found, sso_session_expired, or sso_login_failed; a deactivated or pending-approval account yields inactive or pending_approval.

Troubleshooting

No SSO Buttons on the Login Page

  1. Check that sso_enabled is true.
  2. Check that the provider itself is enabled.
  3. Request GET /api/v1/sso/providers directly — an empty response means the frontend renders no buttons.

Callback Errors / Stuck on the Same Page

  1. Verify the callback domain and provider name match the configuration exactly.
  2. Confirm the public address (from the request/reverse proxy) is reachable by the identity provider.
  3. Check whether the authorization code expired (a long pause gives 6301).
  4. Look at sso_login_failed entries and messages in the audit log.

Login Succeeds but Fields Are Mismapped

  1. Check the path style per Attribute Mapping (dot path vs $ JSONPath).
  2. Remember that a same-named raw field overrides the mapped value.
  3. Non-standard providers such as GitHub need explicit mapping (login, avatar_url).

SAML Validation Fails

  1. The certificate must be PEM content with the header/footer lines removed.
  2. Confirm the IdP and server clocks are in sync (excessive skew invalidates the response).
  3. Confirm idp_entity_id exactly matches the Entity ID in the IdP metadata.

Nobody Can Reach the Admin Console After Disabling Password Login

This is the classic SSO-only accident: confirm at least one Super Admin has an SSO connection bound before switching. If you are already locked out, set sso_allow_password_login back to true server-side and restart.

How is this guide?

On this page