Skip to content

Sign-in with OpenID Connect ​

CodeLotse signs people in with GitHub by default. Instance admins can add OpenID Connect (OIDC) identity providers, for example Microsoft Entra ID, Keycloak, Okta or the bundled Dex broker for LDAP. Design and security decisions: ADR-0013.

Each sign-in button on the login page is one identity provider. What an OIDC user may do is decided only by sign-in groups: tenant admins map groups of a provider to a tenant role and, optionally, to a repository role. A user without a matching group cannot sign in (instance admins excepted).

Requirements at the identity provider ​

  • Authorization code flow with PKCE S256. Configure the provider to require PKCE and, where possible, to refuse the plain method.
  • A confidential client (client id and client secret, client_secret_basic).
  • Redirect URI: <PUBLIC_URL>/auth/oidc/<slug>/callback, for example https://codelotse.example.com/auth/oidc/corp/callback. The instance settings show the exact value with a copy button.
  • An https:// issuer whose discovery document is at <issuer>/.well-known/openid-configuration and whose issuer value is identical to the configured issuer (CodeLotse removes a trailing slash).
  • A groups claim in the ID token or in the userinfo response: a JSON array of strings, at most 200 groups, each at most 256 characters. A login with more or longer groups is refused rather than truncated, so filter the groups at the provider to those relevant for CodeLotse.
  • A username claim (default preferred_username; fallback name, then the subject). CodeLotse does not store e-mail addresses.

Configure a provider in CodeLotse &ZeroWidthSpace;

  1. Instance settings → Identity providers → Add: slug (lowercase, part of the redirect URI, cannot be changed later), display name, issuer, client id, client secret, scopes (must contain openid; default openid profile groups), username and groups claim names, and optionally the tenant it is bound to. In the enterprise edition every provider must be bound to one tenant and its issuer must resolve to a public address.

  2. Click Test connection or run ./deploy/compose/quavon auth oidc test <slug> (add --json for machine-readable output). The test loads the discovery document and key set through the same guarded HTTP client as the login and checks the issuer, https endpoints, a signing key, PKCE S256, the code response type, and whether the configured scopes and claims are advertised. Exit code 0 means no failures (warnings allowed), 1 at least one failure, 2 invalid arguments or unknown slug.

  3. In each tenant: Tenant settings → Sign-in groups → map groups to roles. A mapping for a single repository grants tenant role member and the chosen repository role.

  4. To make an OIDC user an instance admin, let them sign in once (an instance admin or a mapping must permit the first login), then run:

    sh
    ./deploy/compose/quavon grant-instance-admin --provider oidc --issuer https://idp.example.com/realms/acme --login jane

Removing a mapping or disabling the provider takes effect on the next request. Group changes at the identity provider take effect at the user's next sign-in; sessions last at most 8 hours. Deleting a provider ends all its sessions.

Microsoft Entra ID &ZeroWidthSpace;

  • App registration → Authentication → Web platform, redirect URI as above. Certificates & secrets → new client secret.
  • Issuer: https://login.microsoftonline.com/<directory-id>/v2.0.
  • Token configuration → Add groups claim → "Groups assigned to the application" (recommended; keeps the list short) or "Security groups". Entra ID emits group object ids by default; synchronised groups can emit sAMAccountName instead. Map the values CodeLotse receives.
  • Scopes: openid profile (Entra ID does not know a groups scope; the claim comes from the token configuration). Username claim: preferred_username.
  • With more than 200 groups Entra ID replaces the claim with an overage reference; CodeLotse then sees no groups and refuses the login. Assign groups to the application to stay below the limit.

Keycloak &ZeroWidthSpace;

  • Client: OpenID Connect, client authentication on, standard flow only, valid redirect URI as above. Advanced → Proof Key for Code Exchange Code Challenge Method: S256.
  • Issuer: https://<host>/realms/<realm>.
  • Client scopes → create a scope groups with a "Group Membership" mapper (token claim name groups, full group path off, add to ID token and userinfo) and assign it to the client as optional. Keep the default scopes openid profile groups.

Okta &ZeroWidthSpace;

  • Applications → Create OIDC web application, grant type authorization code, sign-in redirect URI as above, require PKCE.
  • Issuer: the authorization server, for example https://<org>.okta.com/oauth2/default.
  • Security → API → Authorization servers → Claims → add groups (value type Groups, a filter such as "Starts with: codelotse-", include in the ID token). If the authorization server has no groups scope, set the scopes to openid profile.

Dex (LDAP and other upstream providers) &ZeroWidthSpace;

Dex v2.46.0 runs as an optional Compose overlay and appears to CodeLotse as one provider with the issuer <PUBLIC_URL>/dex. Dex connects to LDAP or to an upstream OIDC provider. Do not use Dex's SAML connector: its maintainers mark it as unmaintained and likely vulnerable to authentication bypasses. For SAML-only identity providers use a maintained broker that speaks OIDC to CodeLotse, or the identity provider's own OIDC endpoint.

The overlay (deploy/compose/docker-compose.dex.yml) runs Dex from a digest-pinned distroless image as a non-root user with a read-only root file system, no capabilities, SQLite storage on the dex-data volume and the telemetry port only inside the Compose network. The configuration template deploy/compose/dex/config.yaml.tmpl enforces PKCE S256, skips the approval screen, disables the local password database and registers the client codelotse with the redirect URI <PUBLIC_URL>/auth/oidc/dex/callback. Secrets are read from Compose secrets, never from environment variables.

  1. Secrets in deploy/compose/secrets/ (mode 644 inside the 700 directory, like the other secrets):

    • dex_client_secret: a random value, for example openssl rand -hex 32.
    • dex_ldap_bind_password: the LDAP bind password, or empty when LDAP is not used.
    • dex_oidc_client_secret: the upstream client secret, or empty when no upstream OIDC provider is used.
  2. Enable at least one connector in deploy/compose/.env (Dex refuses to start without one):

    sh
    DEX_LDAP_HOST=ldap.example.com:636
    DEX_LDAP_BIND_DN=cn=codelotse,ou=services,dc=example,dc=com
    DEX_LDAP_USER_BASE_DN=ou=people,dc=example,dc=com
    DEX_LDAP_GROUP_BASE_DN=ou=groups,dc=example,dc=com

    Optional LDAP settings with their defaults: DEX_LDAP_NAME (LDAP), DEX_LDAP_START_TLS (false; the default is LDAPS), DEX_LDAP_ROOT_CA_DATA (base64 PEM of a private CA), DEX_LDAP_USERNAME_PROMPT (Username), DEX_LDAP_USER_FILTER ((objectClass=person)), DEX_LDAP_USERNAME_ATTR (uid), DEX_LDAP_ID_ATTR (entryUUID), DEX_LDAP_EMAIL_ATTR (mail), DEX_LDAP_NAME_ATTR (cn), DEX_LDAP_GROUP_FILTER ((objectClass=groupOfNames)), DEX_LDAP_GROUP_MEMBER_ATTR (member, matched against the user's DN), DEX_LDAP_GROUP_NAME_ATTR (cn). For Active Directory use DEX_LDAP_USERNAME_ATTR=sAMAccountName, DEX_LDAP_ID_ATTR=objectGUID, DEX_LDAP_USER_FILTER=(objectClass=user) and DEX_LDAP_GROUP_FILTER=(objectClass=group).

    Upstream OIDC: DEX_OIDC_ISSUER, DEX_OIDC_CLIENT_ID, optional DEX_OIDC_NAME (Single sign-on), DEX_OIDC_GROUPS_CLAIM (groups), DEX_OIDC_USERNAME_CLAIM (preferred_username). Register <PUBLIC_URL>/dex/callback as redirect URI at the upstream provider.

  3. Add dex to COMPOSE_PROFILES in deploy/compose/.env (for example COMPOSE_PROFILES=dex or COMPOSE_PROFILES=sandbox,dex) and run ./deploy/compose/quavon install. The installer and the quavon commands then include the overlay docker-compose.dex.yml.

    Caddy forwards /dex/* to the dex service.

  4. Add the provider in CodeLotse: slug dex, issuer <PUBLIC_URL>/dex, client id codelotse, client secret = content of secrets/dex_client_secret, scopes openid profile groups. Run quavon auth oidc test dex.

The API reaches Dex through the public URL. If the host cannot reach its own public name, add a DNS entry or an extra_hosts entry for the API container that points to the Caddy address.

Residual risks &ZeroWidthSpace;

  • Group membership is read at sign-in. A user removed from a group at the identity provider keeps the corresponding roles until the session ends (at most 8 hours) or until the mapping is removed or the provider disabled in CodeLotse.
  • The identity provider is trusted for the subject and the groups it asserts. Anyone who can add users to a mapped group at the identity provider can grant access to CodeLotse; restrict group administration accordingly.
  • In the self-hosted edition, identity providers on private networks are allowed so that internal brokers such as Dex work. An instance admin can therefore make the API connect to internal https endpoints (discovery, key set, token, userinfo; redirects are refused). The enterprise edition refuses private addresses.
  • OIDC users have no link to GitHub accounts: avatars, GitHub profile links and assignment pages for "me" are not available to them.

The open core is released under the Apache License 2.0.