SSO·SCIM integration spec
Values and behavior specification for OIDC, SAML 2.0, and SCIM 2.0 integration.
For the setup steps on screen, see SSO and SCIM.
In the tables, {API} is the Keural API address for the environment you use (for example, https://api.keural.ai). Use the exact address shown on the Organization management → SSO screen.
Registration values summary
| Item | Value |
|---|---|
| OIDC redirect URI | {API}/api/sso/callback |
| OIDC·SAML sign-in start | {API}/api/sso/{orgId}/login |
| SAML SP EntityID | {API}/api/sso/saml/metadata |
| SAML ACS URL | {API}/api/sso/saml/callback |
| SCIM Base URL | {API}/scim/v2 |
Sign-in always starts from Keural (SP-initiated). When you enter an email address from a registered domain on the sign-in screen, you are taken to the sign-in start address. On success, you go to the Keural home screen. On failure, you go to /login?error=sso.
OIDC
| Item | Behavior |
|---|---|
| Flow | Authorization Code |
| authorize request | response_type=code, client_id, redirect_uri, scope=openid email profile, state, nonce |
| state · nonce | One-time use and valid for 10 minutes. If the sign-in window stays open for more than 10 minutes, you sign in again from the beginning. |
| Token exchange | POST to the token endpoint, application/x-www-form-urlencoded. Client authentication is client_secret_post (client_id and client_secret in the body). |
| ID token signature | Only RS256 is accepted. The key is found in jwks_uri by kid. |
| ID token validation | Signature, exp, aud includes client_id, nonce matches, and iss matches the Issuer URL (a trailing / is ignored) |
The email claim in the ID token. If it is missing, the userinfo endpoint is called with Authorization: Bearer <access_token> to read email. | |
| Name | The name claim in the ID token |
| Endpoints | Found at /.well-known/openid-configuration under the Issuer URL. For an IdP that cannot be found, enter the authorization, token, JWKS, and userinfo addresses directly under Show advanced settings. |
| Time limit | 15 seconds per IdP call |
The IdP application must allow client_secret_post. If it allows only client_secret_basic or private_key_jwt, the token exchange fails.
SAML 2.0
| Item | Behavior |
|---|---|
| Request | The AuthnRequest is sent with the HTTP-Redirect binding. The request is not signed. |
| Request contents | Destination = IdP SSO URL, AssertionConsumerServiceURL = ACS URL, ProtocolBinding = HTTP-POST, Issuer = SP EntityID, NameIDPolicy Format=…:nameid-format:unspecified, AllowCreate=true |
| Response | HTTP-POST binding, with SAMLResponse and RelayState. RelayState must be returned exactly as Keural sent it. |
| Signature | The Response or the Assertion must carry a valid signature. Only the certificate registered on the screen is used to verify it. |
| Assertion | Exactly one assertion must be in the response. Encrypted assertions (EncryptedAssertion) are not accepted. |
| Status | StatusCode is urn:oasis:names:tc:SAML:2.0:status:Success |
| Issuer | If you entered an IdP EntityID, it must match the assertion Issuer. |
| Conditions | NotBefore and NotOnOrAfter (clock tolerance 300 seconds), Audience = SP EntityID |
| SubjectConfirmationData | Recipient = ACS URL, NotOnOrAfter (clock tolerance 300 seconds), InResponseTo = the request ID that Keural sent |
An attribute whose Name or FriendlyName is one of the following (case-insensitive): email, mail, emailAddress, E-Mail-Addresses, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress. If none is found, the NameID containing @ is used. | |
| Name | displayName, name, cn, http://schemas.microsoft.com/identity/claims/displayname |
| Certificate | One X.509 certificate, in PEM or Base64 (DER). To replace it, enter the new certificate and save. |
| Metadata | Instead of uploading the IdP metadata XML, enter three values: the IdP EntityID, the IdP SSO URL, and the certificate. |
User matching
| Item | Behavior |
|---|---|
| Criteria for sending to SSO | The part after @ in the email address exactly matches one entry in the Email domains list (case-insensitive). Subdomains must be registered separately. |
| Finding the account | The Keural account is found by the email address (lowercase) that the IdP returned. |
| When no account exists | If Auto-create users on first login (JIT) is on, the account is created. If it is off, sign-in is refused. |
| Joining the organization | If the user is not an organization member, they join as a Member. An account whose seats are full or that belongs to another organization can sign in but does not join. |
| Conditions for saving an email domain | Domains verified by DNS TXT. Public email domains are excluded. |
Domain verification
| Item | Value |
|---|---|
| Record type | TXT |
| Host | The registered domain as is (for example, acme.com). Do not use a prefixed subdomain. |
| Value | keural-verify=<UUID>, exactly as shown on the screen |
| Check timing | Looked up once when you press Confirm verification. |
SCIM 2.0
| Item | Value |
|---|---|
| Base URL | {API}/scim/v2 |
| Authentication | Authorization: Bearer <SCIM token>. The token starts with scim_ and is shown only once when issued. |
| Content-Type | Request: application/scim+json or application/json. Response: application/scim+json |
| Status codes | Returned as the actual HTTP status codes. Error bodies use the SCIM Error format (schemas, status, detail, scimType). |
Endpoints
| Method | Path | Behavior |
|---|---|---|
GET | /Users | List. filter=userName eq "…", startIndex (default 1), count (default 100, maximum 200) |
GET | /Users/{id} | Get one user |
POST | /Users | Create (201). Returns 409 if the same email already exists. |
PATCH | /Users/{id} | Change active, displayName, name.formatted, name.givenName |
PUT | /Users/{id} | Applies displayName, name, and active. If active is omitted, the user is treated as active. |
DELETE | /Users/{id} | Suspends the account (204). Data is not deleted. |
GET | /Groups, /Groups/{id} | Get the two fixed groups. filter=displayName eq "…" |
PATCH | /Groups/{id} | Add, remove, or replace members |
GET | /ServiceProviderConfig, /ResourceTypes, /Schemas | Get supported features |
User attributes
| Attribute | Use |
|---|---|
userName | Email address. If missing, emails[0].value is used. |
displayName · name.formatted · name.givenName+name.familyName | Display name |
active | false suspends the user (blocks sign-in), true restores the user |
id | Keural user ID. Included in the response. |
externalId is not stored. To find a user in the IdP, use the userName filter.
Groups
| id | displayName | When a member is added | When a member is removed |
|---|---|---|---|
keural-admins | Keural Admins | Admin | Member |
keural-members | Keural Members | Member | No change |
The role of the organization owner is not changed by SCIM. Creating or deleting groups (POST or DELETE /Groups) returns 501. The value of a group member is the Keural user ID (id from /Users).
Supported scope
| Feature | Supported |
|---|---|
| PATCH | Yes |
| Filters | userName eq, displayName eq |
| Bulk · sorting · ETag · password change | No |
Errors
| Status | scimType | Cause |
|---|---|---|
400 | invalidValue | Both userName and emails are missing |
401 | — | Token missing or incorrect, or SCIM provisioning is turned off |
404 | — | The user or group does not belong to this organization |
409 | uniqueness | An account with the same email already exists, or seats are exceeded |
501 | — | Creating or deleting groups |