Skip to content

Authentication

Authentication lives under Configuration › Authentication in the admin console. It covers how people sign in (OIDC or SAML) and how an identity provider’s groups map onto project membership (SCIM).

  1. Open Configuration › Authentication

    The list shows any providers already configured, one live per protocol.

  2. Add a provider

    Choose OpenID Connect or SAML 2.0.

    Issuer URL (a .well-known/openid-configuration URL is accepted and reduced to its issuer), Client ID, client secret, redirect URI, scopes, and whether e-mail verification is required.

  3. Decide whether it may sign in SCIM-provisioned users

    Users provisioned by SCIM may sign in through this provider is off by default. Turn it on only for the provider paired with your SCIM client (for example, Entra ID’s sign-in app when Entra also provisions users). While it is off, a first sign-in through this provider cannot take over an account that SCIM created; the sign-in is refused as a conflict. A provider configured through environment variables instead of this screen uses OIDC_ADOPT_SCIM_USERS=true for the same setting.

  4. Enable it for logins

    Turning a provider on retires any other provider of the same protocol — only one live OIDC and one live SAML definition can exist at a time, because each protocol is served on one route.

Authentication section listing configured identity providers

The platform links an account to the OIDC subject (the id_token sub claim), stored as oidc:<sub>. The email claim only selects the account on the first login of a subject. These rules apply to every OIDC sign-in:

id_tokenResult
No email claimRefused: 400 email claim required.
"email_verified": falseRefused on every login: 403 email claim is not verified. This is always so. Neither OIDC_REQUIRE_EMAIL_VERIFIED nor the provider’s e-mail verification setting changes it.
No email_verified claim, subject already linkedAccepted.
No email_verified claim, first login of the subjectAccepted when e-mail verification is not required. Refused with 403 email claim is not verified when it is required.
"email_verified": trueAccepted.

“E-mail verification is required” is the whether e-mail verification is required setting of a provider on this screen. For a provider configured through environment variables, it is OIDC_REQUIRE_EMAIL_VERIFIED=true. The default is off, because many identity providers omit the claim.

The platform requests the scopes openid, profile and email (a provider on this screen can set its own list). Keep the email scope:

  • In Keycloak, keep the email client scope assigned to the client. If you remove it, Keycloak refuses the authorization request with invalid_scope. Without the scope, the id_token also has no email claim, and the login is refused.
  • In Keycloak, a user whose Email verified flag is off gets "email_verified": false, so the login is always refused. Turn the flag on for the user, or let the user verify the address in Keycloak.

A 409 means that the login cannot use the account that the email claim names. Only an operator can resolve it. The body names the cause, and the elitea-main log has the warning OIDC: refused a login that names two accounts with the sub and the cause:

Body starts withcause in the logMeaning and remedy
this identity is already linked to another account. If your identity provider re-created your useroidc_subject_holds_accountAnother OIDC subject holds the account. Usually the identity provider re-created the user. Relink the account.
this identity is already linked to another account: the account with this email address signs in through SAMLsaml_subject_holds_accountA SAML identity holds the account. An OIDC login never takes over an account of the other protocol. The user signs in through SAML. oidc-relink does not apply.
the account with this email address is provisioned by the directory (SCIM)scim_adoption_refusedThe account was created by SCIM, and this provider may not sign in SCIM-provisioned users. If this provider is the one paired with your SCIM client, turn on Users provisioned by SCIM may sign in through this provider (OIDC_ADOPT_SCIM_USERS=true for a provider configured through environment variables). oidc-relink does not apply.
the email address your identity provider now sends belongs to another accountemail_held_by_another_accountThe subject is linked, and its new address is the address of a different account. Change the address of one of the two accounts.
your account changed while you signed inconcurrent_changeThe account or the link changed during the login, for example a concurrent delete. Sign in again.
Section titled “Relink an account after the identity provider re-created a user”

When an identity provider deletes a user and creates it again, the user gets a new sub. The account still holds the old subject, so the login of the new subject is refused with 409:

this identity is already linked to another account. If your identity provider re-created your user, ask an administrator to relink your account (elitea-main oidc-relink).

This refusal is intentional: only an operator can confirm that the two subjects are the same person. Do not edit auth_core__user_provider by hand. Use the oidc-relink command of the elitea-main image. It reads DATABASE_URL, so run it in an elitea-main pod:

  1. Find the new subject

    Read the new user’s ID in the identity provider (in Keycloak, the user’s ID field). Or read the sub value in the OIDC: refused a login that names two accounts warning in the elitea-main log.

  2. Print the plan

    Terminal window
    kubectl -n <ns> exec deploy/elitea-main -- \
    /elitea-main oidc-relink --user alice@example.com --sub NEW_SUB

    --user takes the numeric user id or the email address. The command prints the account, its current OIDC links and the change. It writes nothing.

  3. Apply the change

    Terminal window
    kubectl -n <ns> exec deploy/elitea-main -- \
    /elitea-main oidc-relink --user alice@example.com --sub NEW_SUB \
    --apply --operator YOUR_USER_ID

    --operator is your own numeric user id. It is recorded in the audit event. The user can then sign in.

Instead of --sub, you can use --clear. It removes the old link, and the next login of the user joins the account by email again. That login must pass the sign-in rules for a first login, and a SCIM-provisioned account is joined only by a provider that may sign in SCIM-provisioned users.

For a SCIM-provisioned account, use --clear: the command refuses --sub. A login links such an account only through a provider that may sign in SCIM-provisioned users. An oidc:<sub> link does not name its provider, so the command cannot check that setting, and a link that it wrote would bypass it. After --clear, the next login applies the provider’s own setting.

The command refuses, with exit code 2, and changes nothing when:

  • the email matches more than one account (the addresses are compared without case). Give the numeric user id.
  • the account has more than one OIDC link. Name the one to change with --old-sub.
  • another account already holds the new subject. The command names that account’s user id. Resolve that account first.
  • --sub names a SCIM-provisioned account. Use --clear.

The command takes the same per-subject lock as a login, so a first login of the new subject and a relink to it do not both link it.

Each applied change writes the security audit event ai.elitea.security.v1.identity.link.changed in the same transaction. The action is identity.oidc_link.replaced, identity.oidc_link.cleared or identity.oidc_link.added (a new subject on an account with no OIDC link). The resource is the user, the principal is user:<operator id>, and the reasons are provider_namespace:oidc and via:oidc-relink. The event holds no subject and no hash of a subject: a subject is often guessable (a numeric id or an email address), so its hash would disclose it. The plan that the command prints shows the subjects to the operator. See Audit trail.

The command changes OIDC links only. A SAML login with a changed NameID gets the 409 this identity is already linked to another account, and has no command yet.

A browser session that never expires, or that stays valid while nobody uses it, extends the time an attacker can use a stolen session or an unattended computer. Your identity provider decides how often people sign in, but the platform’s own session decides how long a signed-in browser keeps working.

Every browser session has an absolute expiry and an idle timeout. Under Configuration › Session Policy, a platform administrator (permission runtime.plugins) sets two maximums:

SettingEffectRange
Maximum Idle Timeout (seconds)A session with no request for this long is ended. A session that had no idle limit gets this limit.0 (no maximum), or 60 to 2592000 (30 days)
Maximum Session Lifetime (seconds)A session is ended this long after sign-in, whatever its activity.0 (no maximum), or 60 to 31536000 (365 days)

Both are 0 by default, so an upgrade keeps the session lifetimes of earlier releases.

A maximum applies to new sessions when they are created, and the cookie lifetime follows it. It also applies to existing sessions: each request checks the session against the earlier of its own deadline and the deadline the maximum gives. When you lower a maximum, sessions that are already past it end at their next request. Raising a maximum never makes an existing session last longer.

Each replica of the main service reads the policy again after at most 15 seconds, and at once on the replica that saved it. If the policy cannot be read, sign-in fails with 500 and requests with a session fail with 503. The platform does not let a session through without its bound.

Each session has a reference number that is not secret (browser_sessions.ref). The platform writes one audit event for each step of a session’s life:

ActionWhen
session.startA browser signed in.
session.expireThe session passed its idle timeout or its absolute lifetime. entity_name gives the reason, idle or absolute.
session.logoutThe user signed out.
session.revokeThe server ended the session: for example, an administrator deactivated the user, or SCIM deprovisioned them.
{
"event_type": "session",
"action": "session.expire",
"user_id": 4,
"entity_type": "browser_session",
"entity_id": 42,
"entity_name": "provider=oidc reason=idle",
"duration_ms": 3600000
}

duration_ms is the age of the session when it ended. A background job writes these events within about a minute, exactly once each, and an expiry carries the time the session actually ended. The events go to your SIEM with the other audit events (see Audit evidence and retention). No cookie or token value is ever written.

Every run that a browser session started records the session’s reference (execution_jobs.browser_session_ref), so you can join a run to the session that started it. Runs started with an API token have no session reference.

  • Set the sign-in frequency and multi-factor policy in your identity provider. The session policy limits how long a browser stays signed in after that.
  • Choose maximums that match your organisation’s policy for unattended devices.

SCIM lets an identity provider push its directory. It creates accounts when people join and deactivates them when they leave. The SCIM endpoint is /api/v2/scim/v2.

An identity provider authenticates to SCIM with a SCIM client. A SCIM client is a credential that belongs to the integration, not to a person. The audit trail records its changes as scim:<client name>.

  1. Open Configuration › Authentication › SCIM provisioning

    The panel shows the Tenant URL and the Token endpoint of this deployment, each with a copy button. You need the admin.auth.users permission to manage SCIM clients.

  2. Create a SCIM client

    Select New SCIM client. Type a name for the identity provider, and select a method:

    • Secret token (Bearer) — one secret. The identity provider sends it as the bearer token on each request.
    • OAuth2 client credentials — a client identifier and a client secret. The identity provider gets a short-lived access token from the token endpoint (1 hour by default), and sends that access token.

    Optionally, set Expires on. The date must be from tomorrow to two years ahead. After that day the client stops working. The list shows the days left, and it shows a warning in the last 30 days.

  3. Copy the secret

    The secret shows one time only. Copy it into the identity provider before you close the dialog. Elitea keeps only a hash of the secret, so nobody can show it again.

In the Entra ID enterprise application, open Provisioning, set the mode to Automatic, and use these values:

Entra ID fieldBearer authenticationOAuth2 client credentials grant
Authentication methodBearer authenticationOAuth2 client credentials grant
Tenant URLhttps://<host>/api/v2/scim/v2https://<host>/api/v2/scim/v2
Secret tokenthe scim_… secret—
Token endpoint—https://<host>/api/v2/scim/oauth/token
Client identifier—the scimc_… client identifier
Client secret—the scimcs_… client secret

Select Test connection. Entra ID calls /Users with the credential, and a correct credential answers 200.

The token endpoint accepts grant_type=client_credentials with the client identifier and secret in the form body (client_secret_post) or in an HTTP Basic header (client_secret_basic). The operator sets the access token lifetime with ELITEA_SCIM_ACCESS_TOKEN_TTL (between 5m and 24h).

Each OAuth2 client can hold at most 20 live access tokens. A new token removes the oldest one. Failed token requests are limited per client identifier and caller address. After 20 failures in one minute, that address gets 429 until the minute ends, also with the correct secret. Failures from one address never block another address, so an attacker cannot lock out your identity provider. To know the real caller address behind an ingress, set the proxy networks in ELITEA_TRUSTED_PROXY_CIDRS (or trusted_proxy_cidrs in the Form authentication document). Without them the caller address is unknown. The endpoint then checks every secret first, so a correct secret is never refused, and it limits only failures.

  • Rotate makes a new secret. The old secret stops working immediately, and every access token issued with it stops working too. Put the new secret in the identity provider at once.
  • Revoke stops the client from authenticating. The row stays in the list, so the audit trail can still name it.
  • Delete removes the client.

What the admin console does provide is the group-to-role mapping, beneath the identity provider list:

  1. Create a binding

    Name the SCIM group exactly as your identity provider sends it, and choose the project and role it grants.

  2. Understand membership

    A member the group granted the role to loses it when they leave the group. A member who already held that role independently keeps it — the identity provider is authoritative only over what it granted, not over everything it happens to find.

SCIM manages ordinary accounts: it creates them, renames them, changes their display name and name parts, and deactivates them. A SCIM DELETE deactivates (suspends) the account instead of deleting it, so the person’s work stays attached if they return.

Some accounts are off limits to SCIM. Any write to them is refused with 403 (scimType: mutability), the reason is included in the response, and the refused attempt is recorded in the Audit Trail:

  • Accounts with an administration role (any role on the admin console’s Users page). SCIM cannot rename, suspend or deprovision them, including people who became administrators after SCIM created their account. This stops a SCIM credential from taking over or locking out the people who administer the platform.
  • Platform accounts — addresses in the @centry.user domain, such as system@centry.user and the per-project system_user_<n>@centry.user. SCIM also cannot give any account an address in that domain, and a SCIM group cannot grant a project role to a platform account.
  1. Deprovision an administrator

    When your identity provider tries to deprovision someone who holds an administration role, the request is refused with “SCIM deprovisioning refused: account holds an administration role — remove the role, then let the IdP retry”. Until then, the person’s sessions and access tokens keep working.

  2. Remove the role

    On the admin console’s Users page, remove the person’s administration role. You can also suspend them from that page right away.

  3. Let the identity provider retry

    The next provisioning cycle repeats the request, and this time it succeeds.