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 /userand/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 |
|
Redirect URI |
|
Post-logout redirect URI |
|
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 |
|---|---|---|
|
Yes |
The stable identity of the user. |
|
Yes |
The XOE user name. The login is rejected if this claim is missing. |
|
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 |
|
|---|---|---|
Keycloak |
|
|
Microsoft Entra ID |
|
|
Okta |
|
|
AWS Cognito |
|
|
The resulting access depends on what the claim contains:
Roles claim of the user |
Result |
|---|---|
Contains |
Administrator |
Contains |
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
adminanduserand 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
adminanduser.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
adminanduser.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
adminanduser.
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
Check that the XOE server can reach the IdP, and that
issuerin the response is exactly the value ofOIDC_ISSUER_URI, including any trailing slash:curl -s "$OIDC_ISSUER_URI/.well-known/openid-configuration"
Open XOE in a private browser window. You should be redirected to the login page of your IdP and then back to XOE.
Log in as a user with the
adminrole and check that the admin functions are available.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 |
|---|---|---|
|
Required |
Issuer URL of your IdP. |
|
Required |
Client ID from step 1. |
|
Required |
Client secret from step 1. |
|
|
Path of the roles claim, see step 2. |
|
|
Also end the IdP session when the user logs out of XOE. |
|
|
Maximum browser sessions per user, |
|
|
Enables the back-channel logout endpoint. |
|
|
Enables the SCIM provisioning endpoint. |
|
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 |
|
Authentication |
Bearer token, using a value from |
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:
externalIdis required and must have the same value as thesubclaim of the user.userNameis also required and must matchpreferred_username. IfuserNamematches butexternalIddiffers fromsub, the user’s login fails withprovisioning_failed.Only the
/Usersresource is available. Disable group provisioning in your IdP, because there is no/Groupsendpoint.PATCHaccepts only the pathactive, or operations without a path. Other paths, such asname.givenName, return HTTP 400. UsePUTfor attribute updates where your IdP allows it.The role is set from
roles[].value(adminoruser), and defaults touser. 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 ( |
Suspends the account. New logins are rejected. The link to the IdP identity is kept. |
Reactivate ( |
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 |
|---|---|
|
Disables the account and removes the link to the IdP identity. Teams and files are kept. |
|
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
subvalue, 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_idinstead ofaud, so they are rejected.