Skip to Content
GuidesAuthentication & OIDCSingle-Tenant Setup

Single-Tenant OIDC Setup

Set up OIDC authentication when all your users authenticate against a single identity provider.


Choose Your Configuration Method

Not sure which to choose? Start with Environment Variables - it’s simpler. Switch to the API later if you need runtime updates.

Both methods use the same login flow: the login page redirects straight to your IdP, and the IdP sends the user back to {PUBLIC_ORIGIN}/login/callback.


Environment Variable Configuration

Use environment variables to configure OIDC. This is the default mode (FEDERATION_ENABLED=false) and there is no separate enable flag: OIDC is active as soon as OIDC_DISCOVERY_ENDPOINT and OIDC_CLIENT_SECRET are set.

Prerequisites

  • Admin access to your Identity Provider
  • Access to your Eneo backend .env file
  • HTTPS enabled on your Eneo domain (PUBLIC_ORIGIN must be https://, except http://localhost for development)

Register your application in the IdP

Register Eneo as a confidential web application and set the redirect URI to your Eneo origin plus /login/callback:

https://your-domain.com/login/callback
  1. Go to Azure Portal  → Azure Active Directory → App registrations
  2. Click New registration
  3. Configure your application:
    • Name: Eneo
    • Supported account types: Accounts in this organizational directory only
    • Redirect URI: Web → https://your-domain.com/login/callback
  4. Click Register and note the Application (client) ID and Directory (tenant) ID
  5. Go to Certificates & secrets → New client secret and copy the value immediately (you won’t see it again)
  6. Go to API permissions → Add a permission → Microsoft Graph → Delegated permissions and add openid, profile, email, User.Read
  7. Click Grant admin consent

Discovery URL:

https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration

Replace {tenant-id} with your Directory (tenant) ID.

Configure backend environment variables

Add to your backend .env file:

# Your Eneo origin - used to compute the redirect URI as {PUBLIC_ORIGIN}/login/callback # https:// only (http://localhost allowed for development), no path, no trailing slash PUBLIC_ORIGIN=https://your-domain.com # Identity provider (OIDC discovery auto-configures the authorization/token/JWKS endpoints) OIDC_DISCOVERY_ENDPOINT=https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration OIDC_CLIENT_ID=your-client-id OIDC_CLIENT_SECRET=your-client-secret # Eneo tenant ID (not the IdP's tenant ID), required by the global login endpoint # Find it with GET /api/v1/sysadmin/tenants/ using the super admin API key OIDC_TENANT_ID=your-eneo-tenant-uuid # Keep the default FEDERATION_ENABLED=false

The frontend environment template (env_frontend.template) carries PUBLIC_ORIGIN as well - set it to the same value.

PUBLIC_ORIGIN must match what you registered in your IdP exactly (including https://). The backend rejects a PUBLIC_ORIGIN that is not https:// (or http://localhost) or that contains a path, query or fragment.

Restart and test

docker compose restart backend

Visit your Eneo instance. The login page detects the OIDC configuration and redirects you to your IdP automatically; after signing in you land back in Eneo. To reach the username/password form instead, open /login?showUsernameAndPassword=true.

Changing environment-variable configuration

To update your OIDC settings:

  1. Edit the OIDC_* variables in your .env file
  2. Restart the backend: docker compose restart backend

If you need to change settings without restarting, use the API configuration instead.

Set OIDC_ALLOWED_DOMAINS to a JSON array such as ["your-company.com"] to restrict global OIDC logins by email domain. The default [] applies no domain filter to existing active members of OIDC_TENANT_ID, but prevents creation of new accounts through JIT.


API Configuration

Configure OIDC via the sysadmin API for runtime updates without restarting the backend. This is the same mechanism as multi-tenant federation, used with a single tenant.

When to use the API

  • You want to update OIDC settings or rotate client secrets without a restart
  • You manage configuration through automation
  • You want to restrict logins to specific email domains (allowed_domains)

Prerequisites

  • All prerequisites from the environment-variable method
  • Super Admin API key (ENEO_SUPER_API_KEY set in the backend .env)
  • Your tenant ID (GET /api/v1/sysadmin/tenants/ with the super admin key)

Enable federation mode

Add to your backend .env file:

# Enable API-based federation config FEDERATION_ENABLED=true # Required for encrypting client secrets in the database (backend refuses to start without it) ENCRYPTION_KEY=your-fernet-encryption-key # Super admin API key for management endpoints ENEO_SUPER_API_KEY=your-super-admin-api-key # Still used as the redirect origin unless the tenant sets canonical_public_origin PUBLIC_ORIGIN=https://your-domain.com

Generate an encryption key:

uv run python -m eneo.cli.generate_encryption_key

Restart the backend after setting these variables. With FEDERATION_ENABLED=true the OIDC_* variables are ignored.

Register your application in the IdP

Follow the same steps as in Register your application in the IdP.

Configure via API

Use the federation API endpoint to set your OIDC configuration. The backend fetches the discovery document immediately and rejects the request with 400 if it is unreachable or incomplete:

curl -X PUT "https://your-domain.com/api/v1/sysadmin/tenants/{tenant_id}/federation" \ -H "X-API-Key: your-super-admin-api-key" \ -H "Content-Type: application/json" \ -d '{ "provider": "entra_id", "canonical_public_origin": "https://your-domain.com", "discovery_endpoint": "https://login.microsoftonline.com/{azure-tenant-id}/v2.0/.well-known/openid-configuration", "client_id": "your-application-client-id", "client_secret": "your-client-secret", "allowed_domains": ["your-company.com"] }'

Test the configuration

Verify your setup works before users try to log in:

curl -X POST "https://your-domain.com/api/v1/sysadmin/tenants/{tenant_id}/federation/test" \ -H "X-API-Key: your-super-admin-api-key"

This fetches the discovery document and checks that issuer, authorization_endpoint, token_endpoint and jwks_uri are present. It does not fetch the JWKS or validate the client credentials.

Test login

Visit your Eneo instance. With exactly one active tenant the login page redirects to your IdP automatically, exactly as in the environment-variable setup. If you later add a second active tenant, Eneo switches to the multi-tenant login flow (tenant selector or ?tenant={slug}).

Updating the API configuration

Use PATCH to change individual fields; omitted fields stay unchanged and changes take effect immediately - no restart needed.

# Example: Update allowed domains curl -X PATCH "https://your-domain.com/api/v1/sysadmin/tenants/{tenant_id}/federation" \ -H "X-API-Key: your-super-admin-api-key" \ -H "Content-Type: application/json" \ -d '{ "allowed_domains": ["company.com", "subsidiary.com"] }'

PUT replaces the whole configuration and requires all mandatory fields again.

View current configuration

curl "https://your-domain.com/api/v1/sysadmin/tenants/{tenant_id}/federation" \ -H "X-API-Key: your-super-admin-api-key"

Client secrets are never returned in GET responses - they’re masked for security.


User Provisioning

Eneo matches the signing-in user to an existing Eneo user by the email claim of the ID token. By default a user who does not exist is rejected with 403 User not found. Provision the account through SCIM or contact your administrator for access.

For deployments using SCIM, keep tenant provisioning set to false (the default). SCIM creates and manages accounts; OIDC signs users in to their existing active account. A user whose account has not arrived through SCIM must wait for provisioning. Enabling SCIM does not automatically change the tenant’s JIT setting.

For deployments without SCIM, JIT can create a user on first sign-in only when all of these conditions hold:

  • Tenant provisioning is explicitly true.
  • A non-empty domain list admits the user’s email domain: OIDC_ALLOWED_DOMAINS for environment-variable configuration, or allowed_domains for API configuration.
  • The validated ID token carries the boolean claim email_verified: true for its email address. Missing, false, numeric or string values do not satisfy this check.

Existing active accounts do not require email_verified to sign in. Inactive or removed accounts cannot be reactivated or recreated through JIT; SCIM or an administrator must restore them. If the IdP does not supply verified-email claims, provision accounts through SCIM or administration.

For environment-variable configuration, set for example:

OIDC_ALLOWED_DOMAINS=["your-company.com"]

Restart the backend after changing this variable. Then enable provisioning on the tenant and set a default role:

curl -X POST "https://your-domain.com/api/v1/sysadmin/tenants/{tenant_id}/" \ -H "X-API-Key: your-super-admin-api-key" \ -H "Content-Type: application/json" \ -d '{"provisioning": true, "default_role_id": "{role-uuid}"}'

Without a default_role_id, auto-provisioned users are created with no roles and cannot use assistants, spaces or any other permission-gated feature until an admin assigns one.

For push-based provisioning from your directory (deactivation, groups), see SCIM Provisioning.


Configuration Reference

Environment Variables

VariableRequiredDescription
PUBLIC_ORIGINYesYour Eneo origin; the redirect URI is {PUBLIC_ORIGIN}/login/callback
OIDC_DISCOVERY_ENDPOINTYes (env method)IdP’s OIDC discovery URL
OIDC_CLIENT_IDYes (env method)OAuth client ID
OIDC_CLIENT_SECRETYes (env method)OAuth client secret
FEDERATION_ENABLED-false (default) uses the OIDC_* variables; true uses the API configuration. FEDERATION_PER_TENANT_ENABLED is a deprecated alias
ENCRYPTION_KEYYes (API method)Fernet key for encrypting secrets
ENEO_SUPER_API_KEYYes (API method)API key for the sysadmin endpoints, sent as X-API-Key

API Configuration Fields

FieldRequiredDescription
providerYesLabel for the IdP (e.g., entra_id, mobilityguard, okta, auth0)
discovery_endpointYesIdP’s OIDC discovery URL
client_idYesOAuth client ID
client_secretYesOAuth client secret (at least 8 characters)
allowed_domainsNoEmail domains allowed to authenticate
canonical_public_originNoOrigin used for the redirect URI; falls back to PUBLIC_ORIGIN
redirect_pathNoCallback path (default: /login/callback)
additional_redirect_urisNoExtra fully-qualified callback URLs, each also registered in the IdP

Scopes are fixed to openid email profile.


Troubleshooting

Redirect URI mismatch

The IdP reports that the redirect URI is not registered, or Eneo rejects the callback with 400 Redirect URI mismatch - authentication flow invalid.

Fix: Ensure both match exactly:

  • In IdP: https://your-domain.com/login/callback
  • In Eneo: PUBLIC_ORIGIN=https://your-domain.com (or the tenant’s canonical_public_origin) - no trailing slash, same scheme, host and port

”Email domain … is not allowed for this organization”

The user’s email domain isn’t in allowed_domains (API method only, 403).

Fix: Add the domain:

curl -X PATCH "https://your-domain.com/api/v1/sysadmin/tenants/{tenant_id}/federation" \ -H "X-API-Key: your-super-admin-api-key" \ -H "Content-Type: application/json" \ -d '{"allowed_domains": ["domain1.com", "domain2.com"]}'

“User not found. Provision the account through SCIM or contact your administrator for access.”

The user authenticated successfully but doesn’t exist in Eneo (403).

Fix: Provision the account through SCIM, create or invite the user, or configure all JIT requirements. Do not enable JIT as a workaround for a SCIM sync failure.

”Email claim not found in ID token”

The ID token has no email claim (401). On Azure Entra ID this happens when the user has no mail attribute or the app registration does not emit the optional email claim - see the checklist in SCIM Provisioning.

Invalid client

Token exchange fails with 401 Failed to exchange authorization code for tokens.

Fix: Verify that the client ID and secret match the IdP registration and that the secret has not expired. The IdP’s own error response is written to the backend log.

Debug mode

For complex issues, enable OIDC debug logging:

curl -X POST "https://your-domain.com/api/v1/sysadmin/observability/oidc-debug/" \ -H "X-API-Key: your-super-admin-api-key" \ -H "Content-Type: application/json" \ -d '{"enabled": true, "duration_minutes": 10, "reason": "Debugging login issue"}'

Then check the backend logs for entries with [OIDC DEBUG] and the correlation ID shown on the error screen:

docker compose logs backend | grep "correlation_id.*a1b2c3d4e5f67890"

Security Best Practices

  1. Use HTTPS: PUBLIC_ORIGIN and the registered redirect URI must be https:// in production
  2. Rotate secrets: Regularly rotate client secrets (with the API method this needs no restart)
  3. Restrict domains: Configure allowed_domains when using the API method
  4. Monitor access: Review authentication logs and the audit log regularly
  5. Back up ENCRYPTION_KEY: Losing it means re-entering every client secret

Next Steps