← All docs

Console sign-in with Keycloak

From docs/integrations/oidc-keycloak.md at v0.1.0 ·View the source on GitHub

This guide connects the Yellow Jack console to Keycloak, so that people sign in with their own Keycloak account, every override they make is recorded under their name, and each person is either an Admin or Read-only.

Tested. e2e/keycloak_oidc.sh runs the files this guide uses (docs/integrations/keycloak/) against a real Keycloak container. It signs in through Keycloak’s own login form, checks that Keycloak’s token endpoint issued the tokens, and checks that an override names the person who made it. It also checks that a wrong client secret makes sign-in fail, that a Read-only user’s change is refused, and that the same change succeeds once an Admin promotes them. Last run: 2026-10-05.

Keycloak image quay.io/keycloak/keycloak:26.8.0@sha256:b0f60d489d51c5d113390bdf5461d4c06e6051be026c05549f2e1e10ec352bcc
Realm file docs/integrations/keycloak/import/yellowjack-realm.json
Trial stack docs/integrations/keycloak/docker-compose.yml

The console settings themselves are described in CONFIGURATION.md.

How the console uses Keycloak

  • The console is a confidential client using the authorization code flow with PKCE (S256). It authenticates to the token endpoint with its client secret (client_secret_basic).
  • At startup it fetches <issuer>/.well-known/openid-configuration once. If that fails, or if the document’s issuer is not the exact string you configured, the console stops. It does not fall back to basic auth.
  • The signed-in identity is the ID token’s email claim. If there is none, it uses preferred_username, then name, then sub.
  • A session lasts CONSOLE_OIDC_SESSION_TTL (default 12h). The console holds no refresh token, so a user disabled in Keycloak is locked out at their next sign-in, not sooner.

1. Start the trial stack

The trial stack runs Keycloak, the approval service and the console. Run these from the repository root. Each value is generated, so nothing in the files is a shared password.

export YJ_KC_ADMIN_PASSWORD="$(openssl rand -hex 16)"   # Keycloak admin console
export YJ_KC_CLIENT_SECRET="$(openssl rand -hex 24)"    # the console's client secret
export YJ_KC_ALICE_PASSWORD="$(openssl rand -hex 12)"   # alice, the trial Admin
export YJ_KC_BOB_PASSWORD="$(openssl rand -hex 12)"     # bob, a trial Read-only user
echo "alice's password: $YJ_KC_ALICE_PASSWORD"
echo "bob's password: $YJ_KC_BOB_PASSWORD"
docker compose -f docs/integrations/keycloak/docker-compose.yml up -d --build

Keycloak takes one to three minutes to start and import the realm. The console waits for it. When docker compose -f docs/integrations/keycloak/docker-compose.yml ps shows the console running, continue.

Ports, all on 127.0.0.1:

Port Service
8180 Keycloak (admin console at http://localhost:8180/admin, user admin)
8185 Yellow Jack console
8190 Approval service

They are not the main stack’s ports, so the trial can run beside it.

Why one URL matters. The issuer must be the same string for your browser and for the console. Keycloak builds its issuer from KC_HOSTNAME (http://localhost:8180). Inside a container, localhost is the container itself, so the trial stack runs the console in Keycloak’s network namespace (network_mode: service:keycloak). Then localhost:8180 means Keycloak to both. In a real deployment, give Keycloak a DNS name that browsers and the console can both resolve, and you do not need this.

2. The realm, client and users

The stack imports import/yellowjack-realm.json at startup. Keycloak replaces the ${YJ_KC_CLIENT_SECRET}, ${YJ_KC_ALICE_PASSWORD} and ${YJ_KC_BOB_PASSWORD} placeholders in that file with the environment values above.

To use an existing Keycloak instead, create the same objects in its admin console. These are the settings the import makes. The import is what was tested; the admin-console labels are Keycloak 26’s names for the same settings.

Realm: yellowjack.

Client (Clients, then Create client):

Setting Value Realm file field
Client type OpenID Connect protocol
Client ID yellowjack-console clientId
Client authentication On (a confidential client) publicClient: false
Standard flow On standardFlowEnabled
Direct access grants Off directAccessGrantsEnabled
Valid redirect URIs http://localhost:8185/auth/callback, exactly redirectUris
PKCE method S256 attributes."pkce.code.challenge.method"
Client secret the Credentials tab secret

The redirect URI must be the full URL, ending /auth/callback. Do not use a wildcard.

Users (Users, then Create new user): username alice, email alice@example.test, email verified, first name and last name set. Then Credentials, Set password, with Temporary off. The realm file also has bob (bob@example.test), set up the same way, for section 6.

Set the first name, last name and email. Keycloak 26’s default user profile requires them. A user without them is sent to an account-information form after the password (the VERIFY_PROFILE required action) instead of back to the console. This was checked by removing the names from the realm file: sign-in stopped at /login-actions/required-action?execution=VERIFY_PROFILE.

If you import users from a file, note that the import ignores "temporary": true on a password. To force a password change, add "requiredActions": ["UPDATE_PASSWORD"] to the user instead.

Events (optional). The realm file turns on login events, so you can see sign-ins and failed token requests under Events in the admin console. The e2e test reads them.

3. The console’s values

The trial stack sets these on the console service. For your own deployment, set the same values (with your hostnames and your administrators):

CONSOLE_OIDC_ISSUER=http://localhost:8180/realms/yellowjack
CONSOLE_OIDC_CLIENT_ID=yellowjack-console
CONSOLE_OIDC_CLIENT_SECRET=<the client secret from Keycloak>
CONSOLE_OIDC_REDIRECT_URL=http://localhost:8185/auth/callback
CONSOLE_ADMINS=alice@example.test
  • The issuer is Keycloak’s base URL plus /realms/<realm>, with no trailing path.
  • The client secret is a credential. Inject it with your orchestrator’s secret mechanism.
  • The four CONSOLE_OIDC_* values must all be set. If any one is empty, OIDC is off (see “Missing client secret” below).
  • CONSOLE_ADMINS names who is an Admin, separated by commas. Use the Keycloak user’s ID (the sub claim, shown as ID on the user’s page in the admin console) or their email address. An email counts only when Keycloak marks it verified, because otherwise a user who can edit their own email could make themselves an Admin. Everyone else who signs in is Read-only until an Admin changes their role. If CONSOLE_ADMINS is empty, nobody is an Admin and nothing can be approved.

When the console starts with OIDC on, its log shows:

AUTH: OIDC sign-in enabled (issuer http://localhost:8180/realms/yellowjack); overrides are attributed to the signed-in user
ROLES: 1 Admin(s) named in CONSOLE_ADMINS; everyone else starts Read-only

and its startup line ends overrides ENABLED for Admins (OIDC sign-in; each override names the signed-in user).

4. Sign in

Open http://localhost:8185. The console sends you to Keycloak. Sign in as alice with $YJ_KC_ALICE_PASSWORD. Keycloak sends you back, and the sidebar shows:

Signed in as alice@example.test · Admin · Sign out

5. Check that an override names you

A new trial stack has nothing in its queue. Add one pending package, the way the gate would when it refuses one:

curl -s -X PUT http://localhost:8190/v1/decisions -H 'Content-Type: application/json' \
  -d '{"package":"yj-signin-check","ecosystem":"npm","version":"1.0.0","verdict":"pending"}'

Open Decisions in the console and select yj-signin-check under Awaiting review. Leave “What the Allow covers” at its default, only 1.0.0, and press Allow yj-signin-check. The settled table now shows alice@example.test under Decided by. The record in the approval service carries the same value:

curl -s "http://localhost:8190/v1/decisions?package=yj-signin-check&ecosystem=npm&version=1.0.0"
# {"package":"yj-signin-check",...,"verdict":"approved","decidedBy":"alice@example.test",...}

and the approval service logs:

decision recorded: npm yj-signin-check@1.0.0 -> approved (by "alice@example.test")

6. Roles: a Read-only user, then a promotion

Sign out, and sign in as bob with $YJ_KC_BOB_PASSWORD. bob is not in CONSOLE_ADMINS, so this first sign-in makes him Read-only. The sidebar shows:

Signed in as bob@example.test · Read-only · Sign out

Add a second pending package, as in section 5, and select it on Decisions. bob sees the package but no Allow button. If the request the button sends is made by hand, the console refuses it (HTTP 403) and the package stays pending:

Your role is Read-only, so this change was refused. Read-only users can see everything and change nothing. An Admin can change your role on the Users page.

Sign in as alice again and open Users. It lists both people. alice’s role is shown as “set by CONSOLE_ADMINS”, with no way to change it here. bob’s row has a role menu: choose Admin and press Change. The change is recorded on the audit log’s Operator actions tab, and in the approval service:

curl -s http://localhost:8190/v1/operator-actions
# [{"id":1,...,"actor":"alice@example.test","kind":"role-changed","subject":"bob@example.test","outcome":"read-only → admin"}]

bob does not need to sign in again. His next request is checked against his new role, so the same Allow now succeeds and names him. A demotion also takes effect on the next request.

To stop the trial stack: docker compose -f docs/integrations/keycloak/docker-compose.yml down -v (the four variables must still be set).

Common failures

Each message below was produced by causing the failure against the trial stack.

Issuer mismatch

The console stops at startup (exit code 1):

OIDC is configured but could not be initialised: OIDC discovery issuer mismatch: configured "http://127.0.0.1:8180/realms/yellowjack", document says "http://localhost:8180/realms/yellowjack"

127.0.0.1 and localhost reach the same Keycloak, but they are different issuers. Copy the issuer value from <issuer>/.well-known/openid-configuration into CONSOLE_OIDC_ISSUER. A trailing / is ignored.

Two related startup failures:

OIDC is configured but could not be initialised: OIDC discovery at http://localhost:8180/realms/yellowjack/.well-known/openid-configuration: Get "http://localhost:8180/realms/yellowjack/.well-known/openid-configuration": dial tcp [::1]:8180: connect: connection refused

The console cannot reach that URL. Here, the console ran in its own container, where localhost is not Keycloak.

OIDC is configured but could not be initialised: OIDC discovery at http://localhost:8180/realms/yellowjak/.well-known/openid-configuration returned 404

The realm name in the issuer is wrong.

Wrong redirect URI

Keycloak refuses before showing the login form. The browser shows a Keycloak error page (HTTP 400):

We are sorry...
Invalid parameter: redirect_uri

The console logs nothing, because the request never comes back to it. Keycloak logs:

WARN  [org.keycloak.events] ... type="LOGIN_ERROR", ... clientId="yellowjack-console", ... error="invalid_redirect_uri", redirect_uri="http://localhost:8185/callback"

CONSOLE_OIDC_REDIRECT_URL must equal one of the client’s Valid redirect URIs, character for character, including the scheme, the port and /auth/callback.

A related failure: the redirect URL is right, but you opened the console under a different host name (here http://127.0.0.1:8185 while the redirect URL says localhost). The browser keeps the console’s sign-in cookie per host name, so the callback arrives without it. The console answers (HTTP 400):

sign-in has no handshake cookie; start again at /auth/login

Open the console at the same host name the redirect URL uses.

Missing client secret

There is no error. With CONSOLE_OIDC_CLIENT_SECRET empty, the console treats OIDC as not configured and starts without it. Its log has no AUTH: OIDC sign-in enabled line, and it serves its pages to anyone, read-only, with no sign-in. If the console opens without sending you to Keycloak, check that all four values are set.

Wrong client secret

Keycloak shows the login form and accepts the password. When the browser comes back, the console answers at /auth/callback (HTTP 502):

could not complete sign-in

The console logs the cause:

OIDC sign-in failed at the token exchange: token endpoint returned 401 (unauthorized_client) -- the usual cause is a wrong CONSOLE_OIDC_CLIENT_SECRET

Keycloak logs it too:

WARN  [org.keycloak.events] ... type="CODE_TO_TOKEN_ERROR", ... clientId="yellowjack-console", ... error="invalid_client_credentials", grant_type="authorization_code"

Copy the secret again from the client’s Credentials tab. If you regenerate it in Keycloak, update the console and restart it.

The scheme of CONSOLE_OIDC_REDIRECT_URL decides whether the console marks its cookies Secure. https:// marks them; http:// does not. Use http:// only for a trial on localhost.

An https:// redirect URL, but the console is served over plain HTTP. The console sets a Secure cookie at /auth/login:

Set-Cookie: yj_oidc_state=...; Path=/; Expires=...; HttpOnly; Secure; SameSite=Lax

After sign-in, Keycloak sends the browser to https://localhost:8185/auth/callback, and a plain-HTTP console cannot answer TLS. The browser shows a connection or TLS error. With curl on Windows the error was:

curl: (35) schannel: next InitializeSecurityContext failed: SEC_E_INVALID_TOKEN (0x80090308) - The token supplied to the function is invalid

The console logs nothing. Serve the console over HTTPS (for example behind a reverse proxy that terminates TLS), or use an http:// redirect URL for a local trial.

An https:// redirect URL, and people open the console at an http:// address on a real host name. The browser does not store a Secure cookie received over plain HTTP, so the sign-in cookie is lost. After Keycloak, the callback answers (HTTP 400):

sign-in has no handshake cookie; start again at /auth/login

This was reproduced with curl and the host name console.test. It does not happen on localhost, because browsers and curl treat http://localhost as secure. Make your proxy redirect http:// to https://.

An http:// redirect URL behind an HTTPS proxy. Sign-in works, but the session cookie is not marked Secure. Change the redirect URL to https://.

Moving from the trial to a deployment

  • Run Keycloak with start, not start-dev, with TLS and a real host name, for example KC_HOSTNAME=https://sso.example.com. The issuer is then https://sso.example.com/realms/<realm>.
  • Remove network_mode: service:keycloak. It exists only so localhost works on a laptop.
  • Serve the console over HTTPS and set CONSOLE_OIDC_REDIRECT_URL to its https:// callback. Register exactly that URL in Keycloak.
  • Use a persistent approval database (APPROVAL_DATABASE_URL). The trial keeps records in memory.