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
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
}
}
}
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
| Option | Default | Notes |
|---|---|---|
autoSignUp | false | Create a new account for an unknown e-mail instead of failing. |
exclusive | false | Only this IdP may authenticate the matched accounts; disables linking by e-mail. |
initReturnsConfig | false | initSignInIDP returns the raw provider config to the client. |
requireVerifiedEmail | false | (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. |
If the "ok" in response is false, you will find details in error, possible error codes are following: ALREADY_EXISTS, UNKNOWN_TYPE, INVALID_CONFIGURATION
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
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.
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 the supported key is redirectUrl.
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 intosignInIDPverbatim.idpConfiguration— only set when the IDP was created withinitReturnsConfig: 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": "[email protected]" }
}
}
Error codes: INVALID_IDP_RESPONSE, IDP_VALIDATION_FAILED, PERSON_NOT_FOUND, PERSON_DISABLED, PERSON_ALREADY_EXISTS.
Session re-validation
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
softRefreshThresholdof its lifetime (default0.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).
auto modeIn the default auto/background mode the revocation is not guaranteed on the very next request:
- the revoke writes
disabled_atto 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
minIntervalsingle-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.
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
method | How it re-checks | Notes |
|---|---|---|
refresh (default) | OIDC refresh-token grant | Needs a refresh token, so offline_access is requested automatically. Rotates the token and advances the lifetime on each refresh. |
userinfo | Calls the userinfo endpoint with the stored access token | No 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). |
introspection | RFC 7662 token introspection | Same cadence as userinfo — the token is not rotated, so the session keeps introspecting every minInterval until the IdP reports it inactive. |
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
| Field | Default | Notes |
|---|---|---|
enabled | false | Master switch. false = pre-2.3 behaviour (no session stored, no re-validation). |
method | refresh | refresh | userinfo | introspection. |
softRefreshThreshold | 0.5 | Fraction (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). |
mode | auto | auto (background before expiry, blocking once expired) | blocking (always synchronous). |
onFailure | revoke | Action on a revoked grant. |
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.
Audit
Every IdP management call is recorded in the audit log:
| Mutation | Audit type | event_data |
|---|---|---|
addIDP | idp_create | {identityProvider, type, configurationKeys, options} — only the names of the configuration keys, never the secret values |
updateIDP | idp_update | {before, after} with configuration collapsed the same way |
disableIDP | idp_disable | {identityProvider} |
enableIDP | idp_enable | {identityProvider} |
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.
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_code | Meaning |
|---|---|
revalidation_error | The 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_invalid | The stored IdP configuration could not be parsed at re-validation time; the session was kept. |
encryption_disabled | Re-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".