An identity provider (IdP) is a service that allows users to authenticate using external accounts, such as Apple ID, Facebook, or other OIDC compatible IdP. By integrating with an identity provider, you can enable your users to sign in to your application using their existing accounts, rather than requiring them to create a new account specifically for your application.

To use an identity provider in Contember, you will need to configure the identity provider in the system and then provide a way for users to initiate the authentication process. This can typically be done by providing a login button or link that redirects the user to the identity provider's authentication page.

Once the user has authenticated with the identity provider, they will be redirected back to your application, where Contember will handle the rest of the authentication process. If the user's identity can be successfully verified, they will be logged in to your application.

IdP configuration

Provider types

Contember ships three IdP types, selected by the type argument of addIDP:

typeProviderConfiguration
oidcAny OpenID Connect–compliant provider (Keycloak, Microsoft Entra, Auth0, Apereo CAS, …)OIDC configuration
facebookFacebook LoginFacebook configuration
appleSign in with AppleApple configuration

All three are built on the same OIDC machinery and accept a common subset of the OIDC configurationscope (claims), responseType, and additionalAuthorizedParties — differing mainly in the credential fields and how the issuer is discovered. The richer OIDC options (claimMapping, fetchUserInfo, returnOIDCResult, tokenEndpointAuthMethod, and revalidation) are honored only by type: "oidc" — Facebook and Apple silently ignore them. The walkthroughs below use oidc.

Adding new IdP

To add a new identity provider (IdP) in Contember, you will need to use the addIDP mutation provided by the tenant API. This mutation allows you to specify the details of the identity provider you want to add, including its type, configuration, and options.

Example how to use the addIDP mutation to add a new OIDC identity provider:

mutation {
	addIDP(
		identityProvider: "oidc-provider",
		type: "oidc",
		configuration: {
			url: "https://oidc-provider.com/.well-known/openid-configuration",
			clientId: "YOUR_CLIENT_ID",
			clientSecret: "YOUR_CLIENT_SECRET",
			responseType: "code",
			claims: "openid email"
		},
		options: {
			autoSignUp: true,
			exclusive: false
		}
	) {
		ok
		error {
			code
			developerMessage
		}
	}
}
claims vs scope

Note that claims in configuration are actually "scopes" in OIDC terminology. This will be fixed in a future version.

In this example, the identityProvider field is set to "oidc-provider", which is a custom slug that you can use to identify the identity provider in your application. The type field is set to "oidc" to indicate that this is an OIDC identity provider.

The configuration field should include the URL of the provider's OpenID Connect configuration, as well as the client ID and client secret provided by the provider. The responseType and claims fields are optional.

The options field allows you to specify additional options for the identity provider, such as whether to automatically sign up users who don't already have an account and whether to allow only this identity provider for authentication.

Options

OptionDefaultNotes
autoSignUpfalseCreate a new account for an unknown e-mail instead of failing.
exclusivefalseOnly this IdP may authenticate the matched accounts; disables linking by e-mail.
initReturnsConfigfalseinitSignInIDP returns the raw provider config to the client.
requireVerifiedEmailfalse(since 2.2) For a non-exclusive provider, only auto-link to / sign in an existing local account by e-mail when the provider asserts the e-mail is verified. Guards against account takeover via an unverified provider claim. See e-mail verification.
assumeEmailVerifiedfalse(since 2.2) Treat this provider's e-mails as verified even when it sends no email_verified claim, so requireVerifiedEmail can stay on for a trusted IdP that never emits the claim. Waives the verified-email gate — use only for a provider you fully trust to vouch for its accounts. See e-mail verification.

If the "ok" in response is false, you will find details in error, possible error codes are following: ALREADY_EXISTS, UNKNOWN_TYPE, INVALID_CONFIGURATION

OIDC configuration

The configuration object passed to addIDP / updateIDP is provider-specific. For type: "oidc" the following fields are supported — only url is strictly required, the rest fall back to the defaults below:

FieldDefaultNotes
urlRequired. The provider's OpenID Connect discovery URL (…/.well-known/openid-configuration), used for issuer discovery.
clientIdOAuth client ID issued by the provider.
clientSecretOAuth client secret. Never returned by the API and redacted in the audit log.
scopeopenid emailSpace-separated scopes requested at authorization. When re-validation uses refresh, offline_access is appended automatically.
claimsDeprecated alias for scope — despite the name these are OIDC scopes, not claims. Prefer scope.
responseTypecodeOIDC response_type. One of code, code id_token, code id_token token, code token, id_token, id_token token, none.
idTokenSignedResponseAlgRS256Expected signing algorithm of the ID token.
tokenEndpointAuthMethodclient_secret_basic(since 2.2) How the client authenticates to the token endpoint. See Token endpoint authentication.
fetchUserInfofalseAfter the token exchange, also call the userinfo endpoint and merge its claims (userinfo wins over the ID token). Needed when the provider only returns some claims from userinfo.
returnOIDCResultfalseExpose the provider's raw token-exchange response in signInIDP's result.idpResponse (access token, raw claims, …).
additionalAuthorizedParties[]Extra azp values accepted in the ID token besides the configured clientId.
timeout5000HTTP timeout (ms) for calls to the provider.
claimMapping(since 2.2) Remap provider claims onto Contember's identity fields. See Claim mapping.
revalidation(since 2.2) Continuous session re-validation. See Session re-validation.

Token endpoint authentication

Available since 2.2

tokenEndpointAuthMethod controls how the client proves its identity when exchanging the authorization code for tokens. The default is client_secret_basic (the secret in an HTTP Basic header). Some providers — notably certain Apereo CAS registrations — only accept the secret in the request body, which requires client_secret_post:

mutation {
  updateIDP(
    identityProvider: "cas-provider",
    mergeConfiguration: true,
    configuration: { tokenEndpointAuthMethod: "client_secret_post" }
  ) { ok error { code developerMessage } }
}

Supported values: client_secret_basic, client_secret_post, client_secret_jwt, private_key_jwt, tls_client_auth, self_signed_tls_client_auth, none.

Claim mapping

Available since 2.2

By default Contember derives the identity from the standard OIDC claims: sub for the stable external identifier, email for the e-mail, name for the display name. A provider whose claims don't follow these defaults can be remapped via claimMapping — without a per-provider code change.

FieldDefaultNotes
externalIdentifiersubClaim used as the stable federation key persisted per identity and matched on every subsequent sign-in. Must resolve to a scalar (string/number); a non-scalar or empty value is rejected at sign-in (see the caution below).
emailemailClaim used as the e-mail address. A non-string value is ignored (treated as absent).
namenameClaim used as the display name. A non-string value is ignored.
attributesKeyKey of a nested object whose properties are lifted to the top level before mapping. For providers that nest their claims — notably Apereo CAS userinfo, which returns them under attributes.

Each mapping value is a claim name, with dot-paths into nested objects supported (e.g. user.id, address.region).

Mapping the external identifier safely

externalIdentifier is the key that ties an IdP account to a Contember identity on every sign-in, so it must be stable and unique per user.

  • It must resolve to a scalar. A claim that resolves to an object/array, or to an empty string, is rejected at sign-in (IDP_VALIDATION_FAILED) — mapping such a value through would otherwise collapse multiple users onto a single key (account takeover). When the mapped claim is simply absent, Contember falls back to the signed sub.
  • Claims lifted via attributesKey (or fetched from userinfo) are not covered by the ID-token signature. A signed ID-token claim always wins over an attributes-level claim of the same name, but if you map externalIdentifier to a claim that only exists in userinfo / attributes, the federation key is no longer anchored to the signed subject. Prefer a signed claim where possible.
Example: Apereo CAS

Apereo CAS exposes user attributes nested under attributes in its userinfo response and, in many deployments, expects the client secret in the request body. The two new options combine to consume it without any code change — fetch userinfo, unwrap attributes, map the stable oid to the external identifier, and post the secret:

mutation {
  addIDP(
    identityProvider: "cas-provider",
    type: "oidc",
    configuration: {
      url: "https://cas.example.edu/cas/oidc/.well-known/openid-configuration",
      clientId: "YOUR_CLIENT_ID",
      clientSecret: "YOUR_CLIENT_SECRET",
      scope: "openid email profile",
      tokenEndpointAuthMethod: "client_secret_post",
      fetchUserInfo: true,
      claimMapping: {
        attributesKey: "attributes",
        externalIdentifier: "oid",
        email: "email",
        name: "name"
      }
    },
    options: { autoSignUp: true }
  ) { ok error { code developerMessage } }
}

Facebook configuration

type: "facebook" uses Contember's built-in Facebook endpoints; you supply the credentials from your Facebook app. It honors the common OIDC options (scope, responseType, additionalAuthorizedParties) but not the oidc-only ones (claimMapping, fetchUserInfo, returnOIDCResult, tokenEndpointAuthMethod, revalidation).

FieldDefaultNotes
clientIdRequired. Facebook app ID.
clientSecretRequired. Facebook app secret. Redacted in the audit log and never returned by identityProviders.
urlRequired by the shared schema — but the Facebook issuer is built in, so the value is not used for discovery. Set it to https://www.facebook.com/.well-known/openid-configuration/.

Besides the standard redirect/callback flow, signInIDP also accepts a Facebook JS SDK response in data instead of a callback url:

data: {
  authResponse: {
    accessToken: "…",      # from FB.login()
    signedRequest: "…"
  }
}

Contember verifies the signedRequest HMAC with the app secret and then fetches id,email,name from the Graph API.

Apple configuration

type: "apple" uses Apple's fixed discovery endpoint and builds the client secret as a short-lived JWT signed with your Apple private key — so there is no clientSecret or url field. Like Facebook, it honors the common OIDC options (scope, responseType, additionalAuthorizedParties) but not the oidc-only ones (claimMapping, fetchUserInfo, returnOIDCResult, tokenEndpointAuthMethod, revalidation).

FieldDefaultNotes
clientIdRequired. Your Apple Services ID (the OAuth client identifier).
teamIdRequired. Apple Developer Team ID — becomes the client-secret JWT issuer.
keyIdRequired. Key ID of the Apple sign-in key — becomes the JWT kid.
privateKeyRequired. The .p8 private key (PKCS#8 PEM). Redacted in the audit log and never returned by identityProviders.

Contember signs an ES256 client-secret JWT (aud: appleid.apple.com, 10-minute lifetime) per request, so no static client secret is stored.

Updating existing IdP

To update an existing identity provider (IdP) in Contember, you will need to use the updateIDP mutation. This mutation allows you to specify the updated details of the identity provider you want to update, including its configuration and options.

Example how to use the updateIDP mutation to update an existing OIDC identity provider:

mutation {
	updateIDP(
		identityProvider: "oidc-provider",
		configuration: {
			url: "https://new-oidc-provider.com/.well-known/openid-configuration",
			clientId: "NEW_CLIENT_ID",
			clientSecret: "NEW_CLIENT_SECRET",
			responseType: "code",
			claims: "openid email profile"
		},
		options: {
			autoSignUp: false,
			exclusive: false
		}
	) {
		ok
		error {
			code
			developerMessage
		}
	}
}

In this example, the identityProvider field is set to "oidc-provider", which is the custom slug that you used to identify the identity provider when you added it to Contember.

The configuration field should include the updated details for the identity provider, such as the URL of the provider's OpenID Connect configuration, the client ID and client secret provided by the provider, and any optional parameters such as responseType and claims.

The options field allows you to specify updated options for the identity provider, such as whether to automatically sign up users who don't already have an account and whether to allow only this identity provider for authentication.

If the "ok" in response is false, you will find details in error, possible error codes are following: NOT_FOUND, INVALID_CONFIGURATION

configuration is replaced wholesale by default

By default updateIDP replaces the entire configuration object — any field you don't pass is dropped. To change a single field while keeping the rest, pass mergeConfiguration: true: each key you provide is merged into the stored configuration, and a key set to null is removed. Omitting configuration entirely leaves it untouched (handy when you only want to change options). The merge is shallow — a nested object such as claimMapping or revalidation is replaced as a whole, not deep-merged.

Temporarily disabling and enabling an IdP

In Contember, you can enable or disable an identity provider (IdP) using the enableIDP and disableIDP mutations. These mutations allow you to control whether an identity provider is available.

To disable an identity provider, you can use the disableIDP mutation like this:

mutation {
	disableIDP(identityProvider: "oidc-provider") {
		ok
		error {
			code
			developerMessage
		}
	}
}

In this example, the identityProvider field is set to "oidc-provider", which is the custom slug that you used to identify the identity provider when you added it to Contember.

When you execute the disableIDP mutation, it will return a response indicating whether the operation was successful. If the operation was not successful, the ok field will be set to false and the error field will contain details about the error that occurred. Possible error code is NOT_FOUND.

To enable a previously disabled identity provider, you can use the enableIDP mutation like this:

mutation {
	enableIDP(identityProvider: "oidc-provider") {
		ok
		error {
			code
			developerMessage
		}
	}
}

The enableIDP mutation works in a similar way to the disableIDP mutation, with the identityProvider field specifying the custom slug of the identity provider you want to enable.

When you execute the disableIDP mutation, it will return a response indicating whether the operation was successful. If the operation was not successful, the ok field will be set to false and the error field will contain details about the error that occurred. Possible error code is NOT_FOUND.

Listing configured IdPs

The identityProviders query returns every configured provider — enabled or disabled. It requires the idp:list permission (granted to PROJECT_ADMIN and SUPER_ADMIN by default), so it is meant for administrative tooling, not the public login screen.

query {
  identityProviders {
    slug
    type
    disabledAt
    configuration
    options {
      autoSignUp
      exclusive
      initReturnsConfig
      requireVerifiedEmail
      assumeEmailVerified
    }
  }
}

disabledAt is null for an active provider and carries the timestamp of the last disableIDP otherwise. The returned configuration has its secrets stripped — OIDC/Facebook clientSecret and Apple privateKey are never exposed — so it is safe to surface in an admin UI.

IdP authentication

A two-step exchange: initSignInIDP produces the redirect URL and any state your client must remember, the user authenticates at the provider, and signInIDP consumes the callback URL to mint a session.

Both mutations carry their provider-specific arguments inside a data: Json field. Older redirectUrl, idpResponse, and sessionData top-level arguments still work but are deprecated and should not be used in new integrations.

Step 1 — initiate

mutation {
  initSignInIDP(
    identityProvider: "oidc-provider",
    data: { redirectUrl: "https://my-app.dev/finish-auth" }
  ) {
    ok
    error { code developerMessage }
    result {
      authUrl
      sessionData
      idpConfiguration
    }
  }
}

The identityProvider field is the slug used when the IdP was added. data.redirectUrl is where the provider should send the user after authentication. The exact data shape is provider-specific — for oidc, facebook and apple the keys are redirectUrl (required) and an optional responseMode (the OAuth response_mode, e.g. query or form_post).

The response carries:

  • authUrl — redirect the browser here.
  • sessionData — opaque blob you must keep alongside the browser session (carries state, nonce, code verifier, …). Pass it back into signInIDP verbatim.
  • idpConfiguration — only set when the IDP was created with initReturnsConfig: true; useful for clients that need to drive the auth flow themselves.

Error codes: PROVIDER_NOT_FOUND, IDP_VALIDATION_FAILED.

Step 2 — complete

Once the IDP redirects back to your redirectUrl, take the full callback URL (with ?code=…&state=…) and submit it together with the sessionData from step 1:

mutation {
  signInIDP(
    identityProvider: "oidc-provider",
    data: {
      url: "https://my-app.dev/finish-auth?code=ABC123&state=XYZ789",
      sessionData: { nonce: "123456", state: "XYZ789" },
      redirectUrl: "https://my-app.dev/finish-auth"
    },
    expiration: 60,
    options: { trustForwardedClientInfo: true }  # optional, see proxy-trust.md
  ) {
    ok
    error { code developerMessage }
    result {
      token
      person { id email }
      idpResponse
    }
  }
}

data.url is the full callback URL the IDP sent the user to. data.sessionData is the blob returned by initSignInIDP. data.redirectUrl must match what was passed to initSignInIDP.

The response result.token is the session API key — store it client-side and use it as the Bearer token for subsequent requests. idpResponse exposes the provider's raw token-exchange response when you need access tokens / userinfo claims.

{
  "ok": true,
  "error": null,
  "result": {
    "token": "XXX",
    "person": { "id": "user-uuid", "email": "john.doe@example.com" }
  }
}

Error codes: INVALID_IDP_RESPONSE, IDP_VALIDATION_FAILED, PERSON_NOT_FOUND, PERSON_DISABLED, PERSON_ALREADY_EXISTS.

Personal IdP connections

Available since 2.2

Once a person has signed in (or linked an account) through an IdP, the federation is stored as a connection on that person — one record per link. A person can be connected to several providers, and may even hold more than one connection to the same provider. A signed-in person can list and disconnect their own connections; administrators can inspect another person's.

Listing your connections

The connections hang off the Person type, so the calling person reads their own through me. It is always visible to the person themselves — no special permission is required:

query {
  me {
    person {
      identityProviders {
        id
        createdAt
        externalIdentifier
        identityProvider {
          slug
          type
          disabledAt
        }
      }
    }
  }
}

id is the connection id (not the provider) — pass it to disconnectMyIdentityProvider to remove a specific link. externalIdentifier is the stable federation key (the provider's sub, see claim mapping). The nested identityProvider is a public view of the provider — slug, type and disabledAt only, never its configuration or secrets. person is null for a non-person caller (e.g. an API key), which therefore has no connections to show.

Viewing another person's connections

The same field is reachable on any Person, e.g. through personById:

query {
  personById(id: "person-uuid") {
    identityProviders {
      id
      externalIdentifier
      identityProvider { slug }
    }
  }
}

For a person other than the caller this requires the person:viewIdp permission against the target's roles — SUPER_ADMIN sees everyone; PROJECT_ADMIN sees members whose roles fall within their allowed-input roles, the same scoping as session visibility. When the viewer lacks visibility the field returns an empty list rather than erroring, so listing many people does not abort on a single forbidden target.

Disconnecting a connection

disconnectMyIdentityProvider(id) removes one of the calling person's own connections, addressed by the connection id from the listing above:

mutation {
  disconnectMyIdentityProvider(id: "connection-uuid") {
    ok
    error { code developerMessage }
  }
}

To guard against accidental lock-out, a connection cannot be removed while it is the person's last remaining sign-in method — that is, they have no password, passwordless sign-in is not usable for them under the tenant policy, and no other enabled IdP connection remains. A connection to a disabled provider does not count as a usable fallback, since it cannot be used to sign in.

codeMeaning
NOT_FOUNDThe caller has no connection with that id.
NOT_A_PERSONThe caller is not a person (e.g. an API key) and so has no connections to disconnect.
LAST_AUTH_METHODRemoving it would leave the person with no way to sign in. Set up a password or another sign-in method first.

A successful disconnect is recorded in the audit log as idp_disconnect.

Session re-validation

Available since 2.2

By default the IdP is consulted only at sign-in. After that the Contember session lives on its own and is simply re-prolonged on every request — so if the user is de-provisioned at the IdP, or their IdP session is revoked, Contember keeps the session valid until it expires on its own.

For OIDC providers you can opt in to continuous re-validation: the session is bound to the IdP session at sign-in (the token set is stored encrypted) and periodically re-checked against the IdP on the verify path. If the IdP says the session is gone, the Contember session is revoked.

It is off by default — without it, IdP sessions behave exactly as before. Enable it per IdP under configuration.revalidation:

mutation {
  updateIDP(
    identityProvider: "oidc-provider",
    mergeConfiguration: true,
    configuration: {
      revalidation: {
        enabled: true,
        method: "refresh",
        softRefreshThreshold: 0.5,
        minInterval: "10 seconds",
        mode: "auto"
      }
    }
  ) { ok error { code developerMessage } }
}

Cadence is driven by the access-token lifetime

Re-validation does not run on a fixed interval. It follows the lifetime of the access token the IdP issued (expires_at), which is exactly the provider's statement of "trust this until X":

  • Fresh — while the token is well within its lifetime, nothing happens (no IdP call).
  • Soft — once the token passes softRefreshThreshold of its lifetime (default 0.5, i.e. half-expired), a background refresh is triggered. It runs after the response, so the request keeps zero added latency, and the token is renewed before it dies. A revocation discovered here is applied by disabling the api_key, so it takes effect on a subsequent request — the request that triggered the refresh is still served.
  • Expired — once the token has actually expired, the refresh becomes blocking: the request waits for the IdP, and a revoked grant fails it immediately.

Setting mode: "blocking" makes every re-validation synchronous (zero revocation lag, at the cost of latency on the re-validation tick) — useful for regulated deployments. A burst of requests past the threshold triggers only one refresh (minInterval single-flight floor).

Revocation latency in auto mode

In the default auto/background mode the revocation is not guaranteed on the very next request:

  • the revoke writes disabled_at to the primary, but the verify path reads the api_key from a (possibly lagging) read replica, so a revoked session keeps authorizing until replication catches up;
  • the minInterval single-flight floor suppresses re-discovery in the meantime.

So in the soft window the worst-case exposure is roughly (1 − softRefreshThreshold) of the remaining token lifetime plus a round-trip and replica lag — bounded by the token expiry, after which the refresh becomes blocking. If you need near-immediate logout, use mode: "blocking" and/or short access-token lifetimes.

Bounded fail-open on a transient failure

A transient IdP/DB failure during a blocking refresh fails open (the session is kept) and spends the claim, so the next attempt is throttled by minInterval. This means an already-expired token can keep being served for up to one minInterval while the IdP is unreachable — an intentional anti-hammer / availability trade-off, not a revocation. These fail-opens are audited (see Audit).

Re-validation methods

methodHow it re-checksNotes
refresh (default)OIDC refresh-token grantNeeds a refresh token, so offline_access is requested automatically. Rotates the token and advances the lifetime on each refresh.
userinfoCalls the userinfo endpoint with the stored access tokenNo refresh token, so the stored access token is never rotated. Once it passes the soft threshold the session re-probes the IdP on (at most) every minInterval, indefinitely, until the IdP rejects the token (HTTP 401 → revoked).
introspectionRFC 7662 token introspectionSame cadence as userinfo — the token is not rotated, so the session keeps introspecting every minInterval until the IdP reports it inactive.
Note

With userinfo/introspection the session does not self-expire: because no token is rotated, re-validation keeps probing the IdP at the minInterval cadence for the life of the session. Choose a minInterval that the IdP can sustain, or prefer refresh where the IdP issues refresh tokens.

A definitive IdP rejection (invalid_grant, inactive token) revokes the session and disables its api_key. Transient failures (network, IdP unavailable) fail open — the session is kept and retried later, so IdP downtime does not log everyone out. Disabling the IdP itself also revokes its sessions on the next check.

Configuration reference

FieldDefaultNotes
enabledfalseMaster switch. false = pre-2.2 behaviour (no session stored, no re-validation).
methodrefreshrefresh | userinfo | introspection.
softRefreshThreshold0.5Fraction (0–1) of the token lifetime after which the proactive background refresh starts.
minInterval"10 seconds"Single-flight floor between re-validation attempts (Postgres interval).
fallbackInterval"5 minutes"Throttle used when the IdP returns no token expiry (Postgres interval).
modeautoauto (background before expiry, blocking once expired) | blocking (always synchronous).
onFailurerevokeAction on a revoked grant.
Encryption key required

Refresh tokens are stored encrypted at rest, so method: "refresh" needs a configured encryption key (CONTEMBER_ENCRYPTION_KEY). Without one, a token-bearing session is simply not persisted — the session falls back to plain behaviour with no re-validation rather than storing secrets in plaintext or failing sign-in.

Single Logout

Available since 2.2

When a session was federated through an OIDC provider, signing out can also terminate the session at the IdP (OpenID Connect Single Logout), so the user is not silently re-authenticated by a still-live SSO session. Both flows build on the federated session captured by session re-validation, so they take effect only when configuration.revalidation.enabled is set (that is when the IdP sid is stored at sign-in).

RP-initiated logout (front-channel)

signOut returns a logoutUrl when the session was federated and the IdP advertises an end_session_endpoint:

mutation { signOut { ok logoutUrl } }

Redirect the browser to logoutUrl to finish the logout at the IdP. It is null for plain/password sessions and for IdPs without an end_session_endpoint (where only a local logout happens).

If you use the bundled admin client, this redirect is opt-in so a federated logout does not unexpectedly bounce existing apps through the IdP. Enable it per logout with the idpLogout flag — <LogoutTrigger idpLogout> or useLogout()({ idpLogout: true }); without it the client performs a local-only logout and redirects to / as before.

Set where the IdP returns the browser afterwards with configuration.postLogoutRedirectUri — it must be registered as a post-logout redirect URI at the IdP:

{
  "url": "https://idp.example.com",
  "clientId": "",
  "clientSecret": "",
  "postLogoutRedirectUri": "https://app.example.com/logged-out"
}

Back-channel logout

For IdP-initiated logout — the IdP terminates the SSO session and notifies Contember directly — Contember exposes a public receiver endpoint:

POST /oidc/backchannel-logout?provider=<slug>
Content-Type: application/x-www-form-urlencoded

logout_token=<signed JWT>

Register this URL as the provider's back-channel logout URI at the IdP, putting the Contember provider slug in the ?provider= query parameter (the IdP knows only which client it is calling, not the Contember provider record). The endpoint is unauthenticated but acts only on a logout_token whose signature, issuer, audience and freshness validate against that provider: a token targeting a sid revokes that session; a sub-only token revokes all of that subject's federated sessions. It answers 200 on success, 400 for an invalid token, 404 for an unknown provider slug, and 501 when the provider does not support back-channel logout.

Audit

Available since 2.2

Every IdP management call is recorded in the audit log:

MutationAudit typeevent_data
addIDPidp_create{identityProvider, type, configurationKeys, options} — only the names of the configuration keys, never the secret values
updateIDPidp_update{before, after} with configuration collapsed the same way
disableIDPidp_disable{identityProvider}
enableIDPidp_enable{identityProvider}
disconnectMyIdentityProvideridp_disconnect{id, slug} — the removed connection id and the provider slug

Sign-in attempts via IdP are recorded as idp_login. When session re-validation revokes a session, it is recorded as idp_session_revoked with event_data: {reason} (e.g. invalid_grant, inactive, idp_disabled); a token rotation is recorded as idp_session_revalidated.

Single Logout adds two more (since 2.2): signing out a federated session that produced an RP-initiated logout URL is recorded as idp_logout_initiated (event_data: {all}), and an IdP-initiated back-channel logout as idp_backchannel_logout (event_data: {sid, sub, revokedCount}). The back-channel audit write is best-effort — it never fails the logout — so a missing entry means observability was lost, not that the logout did not happen.

A fail-open — re-validation could not be performed but the session was kept — is recorded as idp_session_revalidation_failed (with success: true, since it is not a security failure) and an error_code:

error_codeMeaning
revalidation_errorThe IdP could not be reached / returned a transient error; the session was kept and will be retried. Throttled to one entry per minInterval per session, so a prolonged IdP outage surfaces here rather than silently.
config_invalidThe stored IdP configuration could not be parsed at re-validation time; the session was kept.
encryption_disabledRe-validation is enabled on the IdP but no encryption key is configured, so at sign-in the token-bearing session was not stored — the session degraded to a plain, non-revalidated one. Recorded once per such sign-in.

Alerting on idp_session_revalidation_failed lets you distinguish "sessions are being re-validated" from "re-validation has been silently failing open".