4. Single Sign-On with OIDC

How to connect XOE to your corporate identity provider (IdP) using OpenID Connect (OIDC).

This page is for the administrators who install and operate the corporate edition of XOE. It works with any standards-compliant OIDC provider. Setup notes are included for Keycloak, Microsoft Entra ID (Azure AD), Okta and AWS Cognito.

What changes with OIDC

  • Users log in on the login page of your IdP. XOE no longer shows its own login form and local passwords are no longer used.

  • A local user account is created automatically the first time a user logs in. Name, email and role are updated from the IdP at every login.

  • Users are created, changed and removed in your IdP. The XOE endpoints for managing users (/createuser, PUT /user and /deleteuser) are not available in this mode. Teams are still managed in XOE.

  • A role changed locally in XOE is overwritten at the user’s next login, so roles must be assigned in the IdP.

Warning

Once OIDC is active, nobody can log in with a local password, including existing local administrators. Assign the admin role to at least one user in your IdP before you switch over.

Before you start

You need:

  • The public URL of XOE, which must use HTTPS. The session cookie is marked as secure, so a login over plain HTTP does not work.

  • Administrator access to your IdP.

  • Network access from the XOE server to the IdP. XOE reads the IdP’s configuration at startup and does not start if the IdP cannot be reached. If the IdP uses a certificate from a private CA, that CA must be in the truststore of the Java runtime.

If XOE runs behind a reverse proxy, the proxy must send the X-Forwarded-Proto and X-Forwarded-Host headers. XOE uses them to build the redirect URL it sends to the IdP. Without them, the IdP typically rejects the login with a redirect URI mismatch.

Step 1: Register XOE in your IdP

Create a confidential web application (client) with the following settings. Replace xoe.example.com with your own host name.

Setting

Value

Grant type

Authorization Code

Client authentication

Client secret

Scopes

openid, profile, email

Redirect URI

https://xoe.example.com/login/oauth2/code/oidc

Post-logout redirect URI

https://xoe.example.com/ (only needed if you enable OIDC_LOGOUT_TERMINATE_IDP)

Note the client ID, the client secret and the issuer URL for step 3.

Your IdP must provide these claims in the ID token or from its userinfo endpoint:

Claim

Required

Used for

sub

Yes

The stable identity of the user.

preferred_username

Yes

The XOE user name. The login is rejected if this claim is missing.

given_name, family_name, email

No

The user’s profile.

Roles claim (see step 2)

No

The user’s role in XOE.

XOE also uses the user name as the name of the user’s directory. The characters / \ : * ? " < > | are replaced by _. If preferred_username changes for a user, XOE renames the account and its directory at the next login.

Step 2: Assign roles

XOE knows two roles: admin and user. Role names are not case-sensitive. Roles are read from one claim, which you select with XPLAIN_OIDC_ROLES_CLAIM.

IdP

Issuer URL

XPLAIN_OIDC_ROLES_CLAIM

Keycloak

https://<host>/realms/<realm>

realm_access.roles

Microsoft Entra ID

https://login.microsoftonline.com/<tenant-id>/v2.0

roles (default)

Okta

https://<org>.okta.com/oauth2/default

roles (default)

AWS Cognito

https://cognito-idp.<region>.amazonaws.com/<user-pool-id>

cognito:groups

The resulting access depends on what the claim contains:

Roles claim of the user

Result

Contains admin

Administrator

Contains user

Normal user

Two consequences are important:

  • XOE does not restrict who may log in. Every user who can authenticate with this client and has no roles claim becomes a normal user. Restrict access to XOE in your IdP, for example with required user assignment.

The claim path supports one level of nesting. Keycloak client roles (resource_access.<client>.roles) and claim names containing a dot, for example namespaced URLs, cannot be read. Map these values to a flat claim in your IdP instead.

Keycloak

  • Create a client with Client authentication and Standard flow enabled.

  • Create the realm roles admin and user and assign them to your users.

  • Under Client scopes > roles > Mappers > realm roles, enable Add to ID token or Add to userinfo. By default, Keycloak only adds these roles to the access token. XOE would then not see them at login, and every user would become a normal user.

Microsoft Entra ID

  • Create an App registration and add the redirect URI on the Web platform.

  • Under App roles, create two roles with the values admin and user.

  • Under Enterprise applications > Users and groups, assign users or groups to these roles.

  • Under Enterprise applications > Properties, set Assignment required to Yes to limit access to the assigned users.

The user name is normally the user’s UPN, for example jane.doe@example.com.

Okta

  • Create an OIDC Web Application and assign your users or groups to it.

  • Use a custom authorization server such as default. The org authorization server does not support custom claims.

  • Create the groups admin and user.

  • Under Security > API > Authorization Servers > default > Claims, add a claim with the name roles, the token type ID Token (Always), the value type Groups and the filter Matches regex ^(admin|user)$.

AWS Cognito

  • Create an app client with a client secret and enable the Authorization Code grant.

  • Create the user pool groups admin and user.

Step 3: Start XOE with the OIDC profile

Set the connection details as environment variables and activate the oidc profile:

export OIDC_ISSUER_URI=https://login.microsoftonline.com/<tenant-id>/v2.0
export OIDC_CLIENT_ID=<client-id>
export OIDC_CLIENT_SECRET=<client-secret>
export XPLAIN_OIDC_ROLES_CLAIM=roles

java -jar -Xmx20g oe.jar --spring.profiles.active=oidc --spring.datasource.url="jdbc:sqlite:path/to/exampleusers.db"

With Docker Compose:

services:
  xoe:
    image: <your-xoe-image>
    command: >
      java -jar bin/xoe.jar
      --spring.profiles.active=oidc
      --spring.datasource.url=jdbc:sqlite:./exampleusers.db
    environment:
      - OIDC_ISSUER_URI=${OIDC_ISSUER_URI}
      - OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
      - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
      - XPLAIN_OIDC_ROLES_CLAIM=realm_access.roles

Keep the client secret out of scripts and version control, for example by using the secret store of your deployment platform.

Step 4: Test the login

  1. Check that the XOE server can reach the IdP, and that issuer in the response is exactly the value of OIDC_ISSUER_URI, including any trailing slash:

    curl -s "$OIDC_ISSUER_URI/.well-known/openid-configuration"
    
  2. Open XOE in a private browser window. You should be redirected to the login page of your IdP and then back to XOE.

  3. Log in as a user with the admin role and check that the admin functions are available.

  4. Log in as a normal user and check that the admin functions are not available.

Configuration reference

All settings are environment variables and are read at startup, so changes require a restart of XOE.

Variable

Default

Description

OIDC_ISSUER_URI

Required

Issuer URL of your IdP.

OIDC_CLIENT_ID

Required

Client ID from step 1.

OIDC_CLIENT_SECRET

Required

Client secret from step 1.

XPLAIN_OIDC_ROLES_CLAIM

roles

Path of the roles claim, see step 2.

OIDC_LOGOUT_TERMINATE_IDP

false

Also end the IdP session when the user logs out of XOE.

OIDC_CONCURRENT_SESSION_LIMIT

0

Maximum browser sessions per user, 0 means unlimited. When the limit is exceeded, the oldest session expires. Requires OIDC_BACKCHANNEL_LOGOUT_ENABLED=true.

OIDC_BACKCHANNEL_LOGOUT_ENABLED

true

Enables the back-channel logout endpoint.

OIDC_SCIM_ENABLED

false

Enables the SCIM provisioning endpoint.

OIDC_SCIM_TOKENS

Empty

Comma-separated list of valid SCIM bearer tokens. XOE does not start if SCIM is enabled and no token is set.

Sessions and logout

Logging out

By default, Log out only ends the XOE session and the user stays logged in to other applications that use the same IdP. Because the IdP session is still active, opening XOE again logs the user back in without a password prompt.

Set OIDC_LOGOUT_TERMINATE_IDP=true to also end the IdP session. This logs the user out of all applications that share this IdP session.

When access is revoked in the IdP

XOE keeps its own session after login. Whether a revocation in the IdP reaches an active session depends on refresh tokens:

  • With a refresh token, XOE renews the token when it expires. If the IdP refuses the renewal, for example because the account was disabled, the session ends and the user is sent to the login page.

  • Without a refresh token, XOE cannot detect a revocation, and the session stays active until it ends in XOE.

Some providers, including Entra ID and Okta, only issue refresh tokens for the offline_access scope, which XOE does not request by default. To request it, set the following variable and allow the scope in your IdP. In Okta, also enable the Refresh Token grant for the application.

export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_SCOPE=openid,profile,email,offline_access

If the IdP is temporarily unavailable, active sessions continue, but new logins fail until it is back.

Back-channel logout

With OpenID Connect Back-Channel Logout, the IdP notifies XOE directly when an IdP session ends, so that the matching XOE session ends immediately. In your IdP, set the back-channel logout URL to:

https://xoe.example.com/logout/back-channel

The IdP must be able to reach this URL. In Keycloak, the setting is Backchannel logout URL in the client’s logout settings. Check the documentation of other providers to see whether they send back-channel logout tokens. If yours does not, this feature has no effect.

Provisioning users with SCIM (optional)

Without SCIM, a user account is only created at first login, and XOE does not learn that a user was removed in the IdP. With SCIM 2.0, the IdP can create, update, suspend and remove accounts directly.

Generate a token and enable SCIM:

export OIDC_SCIM_ENABLED=true
export OIDC_SCIM_TOKENS=$(openssl rand -hex 32)

In the provisioning settings of your IdP, use:

Setting

Value

Base URL

https://xoe.example.com/scim/v2

Authentication

Bearer token, using a value from OIDC_SCIM_TOKENS

To change the token without an interruption, add the new token to the list and restart XOE, change the token in your IdP, then remove the old one and restart again. After 10 failed authentications within 15 minutes, further requests from that IP address receive HTTP 429 until the 15 minutes have passed.

Requirements for the IdP

Check these points before you enable provisioning, because the default mappings of your IdP may not meet them:

  • externalId is required and must have the same value as the sub claim of the user. userName is also required and must match preferred_username. If userName matches but externalId differs from sub, the user’s login fails with provisioning_failed.

  • Only the /Users resource is available. Disable group provisioning in your IdP, because there is no /Groups endpoint.

  • PATCH accepts only the path active, or operations without a path. Other paths, such as name.givenName, return HTTP 400. Use PUT for attribute updates where your IdP allows it.

  • The role is set from roles[].value (admin or user), and defaults to user. The role from the login token overwrites it at the next login.

What the SCIM operations do

Operation in the IdP

Effect in XOE

Assign or create user

Creates the account and the user directory.

Update user

Updates name, email and role.

Deactivate (active=false)

Suspends the account. New logins are rejected. The link to the IdP identity is kept.

Reactivate (active=true)

Enables the account again.

Delete

Disables the account and removes the link to the IdP identity. Teams and files are kept.

Suspending or deleting a user blocks new logins but does not end a session that is already active. See When access is revoked in the IdP.

Removing and restoring users

Removing a user in the IdP prevents new logins, but the local account remains in XOE. Without SCIM, an administrator can clean it up with these REST endpoints. Both require an administrator login.

Endpoint

Effect

POST /deprovision-oidc-user/<username>

Disables the account and removes the link to the IdP identity. Teams and files are kept.

POST /re-enable-oidc-user/<username>

Enables a deprovisioned account again. It is linked at the next login with this user name.

Warning

A re-enabled account is linked to whichever IdP identity next logs in with this user name, and that user takes over its teams and files. If the name now belongs to a different person, remove the old team memberships first.

Calling the API with bearer tokens

Besides browser sessions, XOE accepts OIDC access tokens in the Authorization: Bearer header. A token must be signed by the IdP, come from the configured issuer, and contain the client ID in its aud claim. Note the following limits:

  • The request runs under the sub value, not the user name, so it does not use the user’s account and directory.

  • No account is created, and the suspended status of an account is not checked.

  • Keycloak needs an Audience mapper to add the client ID to aud.

  • Cognito access tokens carry the client ID in client_id instead of aud, so they are rejected.