Kumoss authenticates against any OIDC-compliant identity provider. The
minimal configuration is two fields in config.yaml:
oidc:
issuer_url: "https://your-idp.example.com"
client_id: "your-client-id"
Leaving issuer_url blank disables authentication entirely, the
checked-in default: every request resolves to a fixed dev identity and
acts as a local developer with top roles. config.yaml is baked into the
kumoss-core image at build time, so enabling authentication requires
docker compose build core; to load a different file, point the
KUMOSS_CONFIG env var at its path.
Prerequisites
-
A spec-faithful OIDC provider that supports the authorization code flow with PKCE for public SPA clients, publishes a discovery document with a JWKS endpoint, and issues JWT access tokens (RS*/ES*/PS* signed) carrying
iss,sub,aud, andexp. -
The ability to rebuild and redeploy the core image, since
config.yamlchanges take effect only afterdocker compose build core.
Steps
1. Check that your provider is supported
| Provider | Support | Config |
|---|---|---|
Microsoft Entra ID |
Supported |
|
Keycloak |
Supported |
|
Auth0 |
Supported |
|
Okta |
Supported |
|
AWS Cognito |
Not supported out of the box |
see the known limitation below |
Google Identity |
Not supported |
access tokens are opaque strings, not JWTs — nothing to validate against JWKS |
Auth0 and Okta list audience because they issue access tokens for a
registered API resource whose identifier is an arbitrary string chosen
when you set it up at the IdP (an Auth0 API identifier, an Okta
authorization server audience) — it cannot be derived from client_id,
so copy it into audience. Entra ID and Keycloak issue tokens whose
aud is the client id itself, which the core already accepts when
audience is blank.
Known limitation: AWS Cognito. Cognito access tokens are JWTs but carry
no aud claim (a client_id claim instead) and no email claim, so the
core rejects them and user provisioning / admin.default_root_email
matching would have no email to work with. Cognito’s ID tokens would pass
validation unchanged, so support would need an opt-in "send the ID token
as bearer" toggle (plus real-world testing of Cognito’s nonstandard
Hosted UI logout). This is not implemented today.
2. Register the app at your identity provider
For every provider:
-
Register a public SPA client using the authorization code flow with PKCE. No client secret is used anywhere.
-
Redirect URI:
<origin>/auth/callback(e.g.http://localhost/auth/callbackfor the docker-compose stack). -
Post-logout redirect URI:
<origin>.
3. Configure your identity provider
The IdP-specific steps share the same shape: register the app, complete
the one extra step that IdP needs, then set config.yaml.
-
App registrations → New registration. Add a Single-page application platform with redirect URI
<origin>/auth/callback. -
Expose an API → Add a scope. Accept the default Application ID URI (
api://<client-id>) and name the scope anything (e.g.access_as_user). This step is required: without it Entra has no scope to grant for the app itself. -
Configure:
oidc: issuer_url: "https://login.microsoftonline.com/<tenant-id>/v2.0" client_id: "<application-client-id>"That is all: for
login.microsoftonline.comissuers the core automatically appendsapi://{client_id}/.defaultto the login scopes, which requests the exposed scope(s) without needing their names. Leaveaudienceblank — both the bare client id andapi://<client-id>are accepted as the token audience, covering both Entra access-token versions.
Gotchas. The auto-scope only triggers when scope is left at its
default value (openid profile email); set it explicitly to request
specific scopes (scope: "openid profile email
api://{client_id}/access_as_user" — {client_id} is expanded
automatically). Sovereign clouds (login.microsoftonline.us, …) are not
auto-detected: set scope explicitly there too. Without any api://…
scope in the request, Entra issues the access token for Microsoft Graph
and the core rejects it with a 401 audience mismatch. Entra access tokens
omit the email claim unless you add it (Token configuration → Add
optional claim → Access → email); without it the panel shows
preferred_username instead. Entra never emits email_verified, so
admin.default_root_email cannot elevate an Entra user — see
Grant the first administrator access.
-
Create a public client (Standard flow on, Direct access grants off) with valid redirect URI
<origin>/auth/callback. -
Keycloak does not put the client_id in
audby default: add an Audience mapper (client scopes → dedicated scope → add mapper → Audience) targeting your client. -
Configure:
oidc: issuer_url: "https://<keycloak-host>/realms/<realm>" client_id: "<client-id>"The default scope (
openid profile email) suffices; leaveaudienceblank. Keycloak emits bothemailandemail_verifiedin access tokens, soadmin.default_root_emailworks once the user’s email is marked verified in Keycloak.
-
Create a Single Page Application with callback URL
<origin>/auth/callbackand logout URL<origin>. -
Create an API (its identifier becomes the token audience) — Auth0 only issues JWT access tokens when an audience is requested.
-
Configure:
oidc: issuer_url: "https://<tenant>.auth0.com" client_id: "<client-id>" audience: "<api-identifier>"The frontend forwards
audienceon the authorize request — Auth0 requires it there to issue a JWT access token. Auth0 emitsisswith a trailing slash; the core accepts the issuer with or without it, so theissuer_urlabove works as written.
Auth0 access tokens for a custom API carry neither email nor
email_verified, and Auth0 does not allow adding standard OIDC claims to
them: users show without an email and admin.default_root_email cannot
elevate anyone — see Grant the first administrator
access.
-
Applications → Create App Integration → OIDC, Single-Page Application, with sign-in redirect URI
<origin>/auth/callbackand sign-out redirect URI<origin>. Assign the app to your users/groups. -
Access tokens come from an authorization server (Security → API → Authorization Servers): copy its Issuer URI and Audience. The default custom server uses issuer
https://<okta-domain>/oauth2/defaultand audienceapi://default. -
Configure:
oidc: issuer_url: "https://<okta-domain>/oauth2/default" client_id: "<client-id>" audience: "api://default"audiencemust match the authorization server’s audience setting verbatim (it is not derived from the client). The default scope suffices. Okta access tokens carry noemailclaim by default; add one on that authorization server (Security → API → Authorization Servers → Claims, valueuser.email) so the panel shows emails. Okta emits noemail_verified, soadmin.default_root_emailcannot elevate an Okta user — see Grant the first administrator access.
4. Grant the first administrator access
admin.default_root_email elevates a user to the top role of both role
groups only when the access token carries a matching email claim and
email_verified: true. A preferred_username fallback or a missing
email_verified never elevates. Of the providers above only Keycloak
emits both claims in access tokens.
The elevation is re-applied on every authenticated request, not only at
login: demoting that user in the admin panel reverts on their next
request. To demote them, clear admin.default_root_email and rebuild
the core image.
For every other IdP, grant the first admin’s roles directly in the database after their first login (role labels are the enum names):
UPDATE users SET operation_role = 'DEVOPS', panel_role = 'ADMIN'
WHERE email = '<email>';
-- or, when the token carries no email claim:
-- WHERE issuer = '<issuer_url>' AND subject = '<sub>';
Everyone else is then managed from the admin panel’s Users tab.
Verify
-
Check what the core serves to the SPA:
curl http://localhost/api/v1/auth/configThe response carries your
issuer_urlandclient_id(andscopewith{client_id}already expanded) — the exact values the frontend receives. An emptyissuer_urlmeans the running image predates the change. -
Open the web application at
<origin>. It redirects to your identity provider and back to<origin>/auth/callback; after signing in you land in the wizard. -
GET /api/v1/users/me— which the SPA calls on load, with the bearer token it obtained — returns youroperation_roleandpanel_role:devopsandadminfor the bootstrap administrator,developerandnullfor anyone else.
Troubleshooting
-
401 "Token validation failed … audience". The access token’s
auddoes not matchaudience/client_id. Entra: the app does not expose an API scope, or a sovereign-cloud issuer skipped the auto-scope (setscopeexplicitly). Keycloak: missing audience mapper. Auth0:audiencenot set inconfig.yaml. Okta:audiencedoes not match the authorization server’s audience setting. -
Login redirect rejected by the IdP. The redirect URI
<origin>/auth/callbackis not registered exactly (scheme, host, port). -
Config changes have no effect. Rebuild the core image (
docker compose build core); the file is baked in. -
A
core_db_datavolume predates authentication. A volume created before OIDC support was added keyed sessions byusernameinstead of the currentuserstable; migrate it by hand or drop it before pointing a current core at it.