Cory Policht
← All references
REF · US-OUI-1-5A-KEYCLOAK-OIDC-CLIENT · STORY

Us Oui 1 5a Keycloak Oidc Client

source: ADP-Orchestration-UI/Epic-1-Stories/US-OUI-1.5a-Keycloak-OIDC-Client.mdscrubbed extract

Scrubbed extract of ADP-Orchestration-UI/Epic-1-Stories/US-OUI-1.5a-Keycloak-OIDC-Client.md — cited by the Chronicle (03-the-platform-rises). Redactions: phone / service detail / secrets / clearance / personal / internal hosts.


type: implementation-plan category: Work project: ADP-Orchestration-UI priority: High status: done created: 2026-06-15 completed: 2026-06-17 merge_sha: 7355d03 pr: “homelab/ADP-Orchestration-UI#11” epic: Epic-1-Foundation user-story: US-OUI-1.5a effort: 6h review-profile: standard parallel_group: W3-Keycloak-Identity wave: W3 lane: C repo: “homelab/ADP-Orchestration-UI” branch: feat/us-oui-1.5-keycloak-oidc-client tags:

  • implementation-plan
  • w3
  • identity
  • oidc
  • pkce
  • keycloak-js

US-OUI-1.5a: Real Keycloak OIDC public client (keycloak-js, PKCE)

Epic: [[README|Epic 1: Foundation]] Wave: [[../../AI-Dev-Platform/Trackers/Wave-3-Keycloak-Identity|W3]] · Lane C (consumer) Governing ADR: [[../../AI-Dev-Platform/ADR/ADR-018-Identity-Federation-Strategy|ADR-018]] · roles [[../../AI-Dev-Platform/ADR/ADR-013-Permission-And-Role-Model|ADR-013]]

Mission

Replace the src/services/auth.ts localStorage mock with a real keycloak-js Authorization-Code + PKCE (S256) public client against the live adp realm (oui-web, https://internal host/realms/adp). The decoded access token’s realm_access.roles flows into AuthSession.roles so the already-built PermissionContext / RoleGate / useHasRole / useHasSurface (US-OUI-1.6 — done on main) gate on real roles instead of stubRolesForIdentity. Real token storage + refresh; src/router/auth.tsx validates real tokens.

Phase-0 reconciliation (do not redo): the permission layer (src/auth/*) and router auth guard (src/router/auth.tsx) already exist + are tested on main. This lane does not rebuild RoleGate/PermissionContext — it swaps the data source feeding them from mock to real OIDC. Keep the AuthSession interface shape (subject, displayName, channel, grant, issuedAt, expiresAt, roles, projectRoles) so downstream consumers are untouched.

Context Bootstrap

  1. [[../../AI-Dev-Platform/ADR/ADR-018-Identity-Federation-Strategy|ADR-018]] §“Grant types per channel” (Auth-Code+PKCE for credentials/SSO) + the live realm facts: issuer https://internal host/realms/adp, public client oui-web, PKCE S256, redirect URIs https://internal host/* + http://localhost:5173/*, web-origins set, token aud includes ais-api.
  2. The current mock + its consumers — read in the worktree before changing:
    • src/services/auth.ts (the mock you replace: loginWithCredentials/Sso/ApiKey/DeviceCode, getToken/getSession/isAuthenticated/logout, stubRolesForIdentity, AuthSession).
    • src/auth/PermissionContext.tsx + src/auth/roles.ts + src/auth/useHasRole.ts / useHasSurface.ts — trace HOW roles reach PermissionContext today (it currently derives from the auth session’s roles); your real token must feed the same path.
    • src/router/auth.tsx (the route guard that calls isAuthenticated()).
    • src/views/LoginView.tsx (the tab strip that calls the login functions).

Gate Policy

Trigger Gate Condition
Changing the AuthSession interface shape or the src/auth/* permission API Trust Gate US-OUI-1.6 (PermissionContext/RoleGate) is done on main and consumed widely. Feed it real roles via the existing shape; do not refactor it.
Implicit flow / non-PKCE, or pkce not S256 Trust Gate oui-web is public + PKCE-only per ADR-018; implicit is forbidden.
Issuer/realm/clientId diverging from https://internal host/realms/adp / oui-web Trust Gate Cross-lane contract with the live Lane A realm.
Adding a client secret to the SPA Trust Gate oui-web is a PUBLIC client — there is no secret in the browser.
npm run build / npm run test / npm run lint failing at Verify Trust Gate Do not mark complete red.

Deliverables

  • Add keycloak-js to package.json.
  • src/services/keycloak.ts (new): a singleton Keycloak instance configured { url: <VITE_KEYCLOAK_URL or https://internal host>, realm: 'adp', clientId: 'oui-web' }, init({ onLoad: 'check-sso', pkceMethod: 'S256', silentCheckSsoRedirectUri }), plus token-refresh wiring (updateToken). Config via import.meta.env (VITE_KEYCLOAK_URL/VITE_KEYCLOAK_REALM/VITE_KEYCLOAK_CLIENT_ID) with the home-lab values as defaults; .env.development/.env.production templates updated.
  • Rewrite src/services/auth.ts to drive real flows off keycloak.ts:
    • loginWithCredentials / loginWithSso → keycloak.login({ … }) (Auth Code + PKCE redirect; SSO via idpHint — see deferral). The page redirects to Keycloak’s login form and back.
    • Build AuthSession from the decoded real token: subject = token.sub, displayName = token.preferred_username ?? token.name, roles = token.realm_access?.roles ?? [], expiresAt from token.exp, issuedAt from token.iat. Keep projectRoles: [] (server-side, forward-compat).
    • getToken() → keycloak.token; getSession() → built from keycloak.tokenParsed; isAuthenticated() → keycloak.authenticated && !keycloak.isTokenExpired(); logout() → keycloak.logout().
    • Preserve the exported types (AuthSession, AuthChannel, AuthGrant, AuthError, etc.) so imports don’t break. Remove stubRolesForIdentity / generateToken / the localStorage token mock.
  • src/router/auth.tsx: the guard validates real Keycloak auth (and triggers keycloak.login() / redirect on unauthenticated protected routes).
  • LoginView.tsx: the Credentials tab and SSO tab initiate the real keycloak.login() redirect. Discord/GitHub SSO buttons are DEFERRED — render them disabled with a “configured when IdP brokers land” affordance (do NOT call idpHint: 'discord'|'github' — those brokers don’t exist yet and would error). Device-code + API-key tabs may keep their current stubbed behavior for v0.1.0 (note it); the credentials/SSO PKCE path is the gate.
  • Tests (Vitest): mock keycloak-js; assert loginWithCredentials/getSession build an AuthSession whose roles come from a fake token’s realm_access.roles; isAuthenticated reflects the mocked keycloak state; logout delegates to keycloak.logout. A test proving PermissionContext/RoleGate shows/hides correctly when fed real-token roles (extend or mirror the existing src/auth/__tests__/permission-context.test.tsx pattern). Update any existing auth.ts mock-based tests to the new surface.

Execution Phases (TDD-ordered)

  1. Branch — work only in the assigned worktree on feat/us-oui-1.5-keycloak-oidc-client off origin/main.
  2. Load Context — read the bootstrap files; trace the roles→PermissionContext path. (Plan checkpoint.)
  3. RED — author/adjust the Vitest tests for the real-OIDC auth.ts surface so they fail. Separate test(auth): real keycloak-js oidc client (red) commit. (Test checkpoint.)
  4. GREEN — add keycloak-js + keycloak.ts, rewrite auth.ts, wire router/auth.tsx + LoginView.tsx. Minimum to green.
  5. Verify & Refactor — npm run build, npm run test, npm run lint all green (zero warnings). Update status to review.
  6. PR — push; one PR to main on homelab/ADP-Orchestration-UI. Hand-author the PR body.

Definition of Done

  • keycloak-js PKCE (S256) Auth-Code flow against oui-web replaces the mock; no client secret in the SPA.
  • AuthSession.roles populated from the real token’s realm_access.roles; existing RoleGate/PermissionContext consume them unchanged (shape preserved).
  • Discord/GitHub SSO buttons disabled/deferred; credentials + SSO-redirect path works.
  • npm run build/test/lint green.
  • PR merged to main.
  • (W3 gate) adp-owner credentials round-trip through Keycloak PKCE and the dashboard renders role-gated surfaces — verified at W3 close against the deployed OUI + live Keycloak.
  • [[../../AI-Dev-Platform/Trackers/Wave-3-Keycloak-Identity|W3 tracker]] · [[../../HomeLab-Infrastructure/Epic-Identity-Provider/IP-IDP-1-Keycloak-Postgres-Stack|Lane A]] (issuer, live) · [[../../AI-Service-Implementation/Epic-Identity/IP-AIS-IDENTITY-1-JWT-Resource-Server|Lane B]] (resource server, merged)
  • [[../../AI-Dev-Platform/ADR/ADR-018-Identity-Federation-Strategy|ADR-018]] · [[../../AI-Dev-Platform/ADR/ADR-013-Permission-And-Role-Model|ADR-013]]