Console sign-in with Keycloak
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-configurationonce. If that fails, or if the document’sissueris 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
emailclaim. If there is none, it usespreferred_username, thenname, thensub. - A session lasts
CONSOLE_OIDC_SESSION_TTL(default12h). 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_ADMINSnames who is an Admin, separated by commas. Use the Keycloak user’s ID (thesubclaim, 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. IfCONSOLE_ADMINSis 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.
http and https: cookie security
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, notstart-dev, with TLS and a real host name, for exampleKC_HOSTNAME=https://sso.example.com. The issuer is thenhttps://sso.example.com/realms/<realm>. - Remove
network_mode: service:keycloak. It exists only solocalhostworks on a laptop. - Serve the console over HTTPS and set
CONSOLE_OIDC_REDIRECT_URLto itshttps://callback. Register exactly that URL in Keycloak. - Use a persistent approval database (
APPROVAL_DATABASE_URL). The trial keeps records in memory.
Yellow Jack