Compatibility profiles
Applications written for another identity provider rely on details that are not part of OpenID Connect itself: how the client authenticates, which algorithm signs the tokens, whether an access token is a JWT by default, which claims it carries. A compatibility profile sets those details for the whole instance.
The profile is an instance setting, chosen with environment variables when the stack is deployed. The default profile changes nothing.
Choosing a profile
COMPAT_PROFILE=okta # default | okta | auth0 | keycloak | custom
| Setting | default | okta | auth0 | keycloak |
|---|---|---|---|---|
Client authentication (TOKEN_ENDPOINT_AUTH_METHOD) | basic | basic | post | basic |
Token signature (JWT_PRIMARY_ALG) | EdDSA | RS256 | RS256 | RS256 |
JWT without resource (DEFAULT_AUDIENCE) | no | yes | no | yes |
audience accepted as resource (AUDIENCE_PARAM) | no | no | yes | no |
Added claims (CLAIM_MAPPINGS) | none | groups, scp, preferred_username, cid | none | groups, preferred_username |
Endpoint aliases (COMPAT_ALIASES) | none | Okta | Auth0 | none |
Every variable overrides the profile, so COMPAT_PROFILE=okta with TOKEN_ENDPOINT_AUTH_METHOD=post is an Okta profile with POST authentication. custom starts from the plain defaults: only the variables you set apply. The active profile is shown, read only, in the Configuration page of the admin interface, and logged at startup when it is not default.
/realms/<realm>/protocol/openid-connect/…) are not aliased.What each setting does
Client authentication
A confidential application registers for one method, and the token endpoint refuses the other:
basic:Authorization: Basic base64(client_id:client_secret).post:client_idandclient_secretin the form body.
The setting applies to new applications, and at every startup to existing ones. System applications (the LiteLLM gateway clients) always use basic. Public applications use neither.
Token signature
EdDSA (Ed25519) is the default. RS256 is what most Okta, Auth0 and Keycloak clients expect, and it signs access tokens and ID tokens, so SDKs that validate the ID token with RS256 work. The JWKS and the discovery document advertise the configured algorithm.
Choose it before the first start. On an instance that already holds keys of another algorithm, a key of the new one is created at startup and a warning is logged: tokens issued before keep verifying until their key expires (one month of grace after rotation). This is meant for a deliberate switch, not for production routines.
The LiteLLM gateway accepts both algorithms.
Audience
An access token is a JWT only when the request names a resource.
DEFAULT_AUDIENCE=true: a request withoutresourcereceives the client's own applicationurl(when the application is active and has one), at the start of an authorization-code flow and forclient_credentials. The code exchange and the refresh inherit what was authorised. This is how Okta behaves.AUDIENCE_PARAM=true:audienceis read asresource. Auth0 clients send it. Without an audience the token stays opaque, as with Auth0.
Neither setting overrides a resource the client sent, and the resource is still checked against the application's allowedResources.
Claims
CLAIM_MAPPINGS copies values that auth-service already computes into claims another provider emits. There is no expression language:
[
{ "claim": "groups", "source": "roles", "scope": "groups" },
{ "claim": "scp", "source": "scope" },
{ "claim": "https://acme.example/roles", "source": "roles", "scope": "roles" }
]
source | Value |
|---|---|
roles | the user's role names in the application |
permissions | the permission strings ("documents" for read, "documents.write" for write) |
scope | the granted scopes, as an array |
email | the e-mail address |
clientId | the application's client id |
scope is optional: when present, the claim is emitted only if the client requested it. A scope that is not native (Okta's groups) is added to the supported scopes; remember to allow it in the application's allowedScopes. Claims needed only to feed a mapping are not disclosed under their own name unless the client asked for them. Claim names that belong to the token (sub, aud, iss, scope, azp…) are refused. Mappings apply to the access token, the ID token and userinfo of user-bound grants; a client_credentials token has no user and carries no mapped claims.
Endpoint aliases
COMPAT_ALIASES makes the service answer the endpoint paths of another provider, for SDKs that derive them from a domain instead of reading discovery:
| Alias | Paths |
|---|---|
okta | /oauth2/default/v1/… and /oauth2/v1/…: authorize, token, keys, userinfo, introspect, revoke, logout |
auth0 | /authorize, /oauth/token, /oauth/revoke, /userinfo, /.well-known/jwks.json |
An alias is resolved to the canonical /api/auth/… path before routing and before every guard, so CORS, rate limiting and validation are exactly those of the canonical endpoint. The match is exact, never a prefix, and only OAuth and OIDC endpoints are targets.
Limits: the issuer in tokens and in the discovery document stays the root URL, so clients must be configured with it; /v2/logout of Auth0 is not mapped (use end-session); the token endpoint takes form bodies, so auth0-spa-js must keep its default useFormData.
Migrating the users
The profile covers how applications talk to the service. Moving the users is a separate step, with a command per provider: see Migrating from Okta, from Auth0 and from Keycloak.