Concepts

Compatibility profiles

Make an auth-service instance behave like the identity provider it replaces — Okta, Auth0 or Keycloak — so that applications change as little as possible.

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
Settingdefaultoktaauth0keycloak
Client authentication (TOKEN_ENDPOINT_AUTH_METHOD)basicbasicpostbasic
Token signature (JWT_PRIMARY_ALG)EdDSARS256RS256RS256
JWT without resource (DEFAULT_AUDIENCE)noyesnoyes
audience accepted as resource (AUDIENCE_PARAM)nonoyesno
Added claims (CLAIM_MAPPINGS)nonegroups, scp, preferred_username, cidnonegroups, preferred_username
Endpoint aliases (COMPAT_ALIASES)noneOktaAuth0none

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.

The Keycloak profile covers tokens and claims only. Keycloak's own endpoint paths (/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_id and client_secret in 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 without resource receives the client's own application url (when the application is active and has one), at the start of an authorization-code flow and for client_credentials. The code exchange and the refresh inherit what was authorised. This is how Okta behaves.
  • AUDIENCE_PARAM=true: audience is read as resource. 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" }
]
sourceValue
rolesthe user's role names in the application
permissionsthe permission strings ("documents" for read, "documents.write" for write)
scopethe granted scopes, as an array
emailthe e-mail address
clientIdthe 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:

AliasPaths
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.

Copyright © 2026