Kumoss

Enable authentication

Connect an OIDC identity provider, register the app for each supported provider, and grant the first administrator access.

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, and exp.

  • The ability to rebuild and redeploy the core image, since config.yaml changes take effect only after docker compose build core.

Steps

1. Check that your provider is supported

Provider Support Config

Microsoft Entra ID

Supported

issuer_url + client_id (scope auto-derived)

Keycloak

Supported

issuer_url + client_id (audience mapper in the realm)

Auth0

Supported

issuer_url + client_id + audience (API identifier)

Okta

Supported

issuer_url + client_id + audience (the custom authorization server’s audience, e.g. api://default)

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/callback for 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.

  1. App registrations → New registration. Add a Single-page application platform with redirect URI <origin>/auth/callback.

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

  3. Configure:

    oidc:
      issuer_url: "https://login.microsoftonline.com/<tenant-id>/v2.0"
      client_id: "<application-client-id>"

    That is all: for login.microsoftonline.com issuers the core automatically appends api://{client_id}/.default to the login scopes, which requests the exposed scope(s) without needing their names. Leave audience blank — both the bare client id and api://<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.

  1. Create a public client (Standard flow on, Direct access grants off) with valid redirect URI <origin>/auth/callback.

  2. Keycloak does not put the client_id in aud by default: add an Audience mapper (client scopes → dedicated scope → add mapper → Audience) targeting your client.

  3. Configure:

    oidc:
      issuer_url: "https://<keycloak-host>/realms/<realm>"
      client_id: "<client-id>"

    The default scope (openid profile email) suffices; leave audience blank. Keycloak emits both email and email_verified in access tokens, so admin.default_root_email works once the user’s email is marked verified in Keycloak.

  1. Create a Single Page Application with callback URL <origin>/auth/callback and logout URL <origin>.

  2. Create an API (its identifier becomes the token audience) — Auth0 only issues JWT access tokens when an audience is requested.

  3. Configure:

    oidc:
      issuer_url: "https://<tenant>.auth0.com"
      client_id: "<client-id>"
      audience: "<api-identifier>"

    The frontend forwards audience on the authorize request — Auth0 requires it there to issue a JWT access token. Auth0 emits iss with a trailing slash; the core accepts the issuer with or without it, so the issuer_url above 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.

  1. Applications → Create App Integration → OIDC, Single-Page Application, with sign-in redirect URI <origin>/auth/callback and sign-out redirect URI <origin>. Assign the app to your users/groups.

  2. 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/default and audience api://default.

  3. Configure:

    oidc:
      issuer_url: "https://<okta-domain>/oauth2/default"
      client_id: "<client-id>"
      audience: "api://default"

    audience must match the authorization server’s audience setting verbatim (it is not derived from the client). The default scope suffices. Okta access tokens carry no email claim by default; add one on that authorization server (Security → API → Authorization Servers → Claims, value user.email) so the panel shows emails. Okta emits no email_verified, so admin.default_root_email cannot 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

  1. Check what the core serves to the SPA:

    curl http://localhost/api/v1/auth/config

    The response carries your issuer_url and client_id (and scope with {client_id} already expanded) — the exact values the frontend receives. An empty issuer_url means the running image predates the change.

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

  3. GET /api/v1/users/me — which the SPA calls on load, with the bearer token it obtained — returns your operation_role and panel_role: devops and admin for the bootstrap administrator, developer and null for anyone else.

Troubleshooting

  • 401 "Token validation failed … audience". The access token’s aud does not match audience/client_id. Entra: the app does not expose an API scope, or a sovereign-cloud issuer skipped the auto-scope (set scope explicitly). Keycloak: missing audience mapper. Auth0: audience not set in config.yaml. Okta: audience does not match the authorization server’s audience setting.

  • Login redirect rejected by the IdP. The redirect URI <origin>/auth/callback is 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_data volume predates authentication. A volume created before OIDC support was added keyed sessions by username instead of the current users table; migrate it by hand or drop it before pointing a current core at it.