Skip to content

feat(portal,chat-frontend): SSO front-door with static per-site config#395

Closed
vjauhari-work wants to merge 1 commit into
mainfrom
claude/gallant-darwin-k0sc1h
Closed

feat(portal,chat-frontend): SSO front-door with static per-site config#395
vjauhari-work wants to merge 1 commit into
mainfrom
claude/gallant-darwin-k0sc1h

Conversation

@vjauhari-work

@vjauhari-work vjauhari-work commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements a confidential OIDC SSO front-door in portal-service and reverts chat-frontend to static per-site configuration. Users now sign in once at portal-service, which redirects them to their home-site chat-frontend already authenticated via Keycloak's SSO cookie. The chat-frontend no longer calls /api/userInfo and instead uses static AUTH_URL, NATS_URL, and SITE_ID environment variables.

Key Changes

portal-service (Go):

  • Added oidclogin.go with loginAuthenticator interface, AuthCodeURL, and ExchangeAndVerify methods for confidential OIDC web-app flow
  • Added /login and /auth/callback endpoints with state/nonce handshake cookie helpers
  • Trimmed /api/userInfo response to 4 fields (account, employeeId, siteId, baseUrl); removed authServiceUrl and natsUrl
  • Added resolveSite helper to map account → employee + site URLs using the in-process directory cache
  • Added OIDC configuration fields (OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URL, OIDC_SCOPES) with conditional prod validation
  • Removed NATSURL field from employee struct and MongoDB projection
  • Updated NewPortalHandler signature to accept loginAuthenticator and cookieSecure parameters
  • Added comprehensive unit tests for OIDC flow, handshake cookies, and site resolution

chat-frontend (JS):

  • Replaced PORTAL_URL with static AUTH_URL, NATS_URL, and SITE_ID in runtimeConfig.js
  • Updated NatsContext.jsx to use static AUTH_URL instead of resolving it from portal /api/userInfo call
  • Removed the portal userInfo fetch at connect time; user.siteId now comes from SITE_ID config
  • Updated environment variable templates and docker-compose configurations
  • Updated tests to reflect static config model

Keycloak:

  • Added confidential portal client to the chatapp realm with appropriate redirect URIs and scopes

Documentation:

  • Added design spec (docs/superpowers/specs/2026-06-24-portal-sso-front-door-design.md)
  • Added implementation plan (docs/superpowers/plans/2026-06-24-portal-sso-front-door.md)
  • Updated docs/client-api.md to reflect the new login flow

Implementation Details

  • Stateless design: No session/token persisted after redirect; only transient handshake cookie (~5 min TTL) and in-process cache
  • Silent re-auth: Browser redirects to site root (/), which triggers signinRedirect() that finds the Keycloak SSO cookie and re-authenticates without user interaction
  • Dev mode: Skips OIDC validation and redirects directly to dev-fallback site
  • Nonce verification: Explicit check in ExchangeAndVerify since go-oidc does not validate nonce automatically
  • Cookie security: HttpOnly, Secure, SameSite=Lax with configurable PORTAL_COOKIE_SECURE flag

https://claude.ai/code/session_01WkCRxoR82h2iLS3EXGJ5c3

Summary by CodeRabbit

  • New Features
    • Added browser redirect-based portal login with an OIDC callback and redirect back to the user’s home site.
    • Updated the chat frontend to connect using static site settings (auth URL, NATS URL, site ID).
  • Bug Fixes
    • Removed portal lookup during connect; authentication and connection now target the configured auth/NATS endpoints directly.
    • Improved handling and validation of login/callback failures with clearer outcomes.
  • Documentation
    • Updated client API and deployment/config docs to reflect the new login/connect flow.
  • Tests
    • Expanded unit/integration coverage for the new login/callback and config behavior.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ff162d49-7c80-472e-bbf5-70626ffcc3ca

📥 Commits

Reviewing files that changed from the base of the PR and between 31b3098 and 1619e4c.

📒 Files selected for processing (31)
  • auth-service/deploy/keycloak/realm-export.json
  • chat-frontend/deploy/30-render-config.sh
  • chat-frontend/deploy/config.js.template
  • chat-frontend/deploy/docker-compose.yml
  • chat-frontend/src/context/NatsContext/NatsContext.jsx
  • chat-frontend/src/context/NatsContext/NatsContext.test.jsx
  • chat-frontend/src/context/NatsContext/useJwtRefresh.js
  • chat-frontend/src/context/NatsContext/useJwtRefresh.test.js
  • chat-frontend/src/lib/runtimeConfig.js
  • chat-frontend/src/lib/runtimeConfig.test.js
  • chat-frontend/src/pages/LoginPage/LoginPage.jsx
  • docker-local/setup.sh
  • docs/client-api.md
  • docs/superpowers/plans/2026-06-24-portal-sso-front-door.md
  • docs/superpowers/specs/2026-06-24-portal-sso-front-door-design.md
  • go.mod
  • pkg/oidc/oidc.go
  • pkg/oidc/oidc_test.go
  • portal-service/cache_test.go
  • portal-service/deploy/docker-compose.yml
  • portal-service/handler.go
  • portal-service/handler_test.go
  • portal-service/integration_test.go
  • portal-service/main.go
  • portal-service/main_test.go
  • portal-service/mock_oidclogin_test.go
  • portal-service/oidclogin.go
  • portal-service/oidclogin_test.go
  • portal-service/routes.go
  • portal-service/store.go
  • portal-service/store_mongo.go
💤 Files with no reviewable changes (2)
  • portal-service/cache_test.go
  • portal-service/store_mongo.go
✅ Files skipped from review due to trivial changes (5)
  • chat-frontend/src/context/NatsContext/useJwtRefresh.js
  • chat-frontend/src/pages/LoginPage/LoginPage.jsx
  • chat-frontend/src/context/NatsContext/useJwtRefresh.test.js
  • portal-service/mock_oidclogin_test.go
  • docs/superpowers/plans/2026-06-24-portal-sso-front-door.md
🚧 Files skipped from review as they are similar to previous changes (19)
  • portal-service/main_test.go
  • chat-frontend/deploy/config.js.template
  • portal-service/deploy/docker-compose.yml
  • chat-frontend/deploy/docker-compose.yml
  • portal-service/store.go
  • chat-frontend/src/lib/runtimeConfig.js
  • chat-frontend/src/lib/runtimeConfig.test.js
  • portal-service/oidclogin.go
  • portal-service/oidclogin_test.go
  • portal-service/routes.go
  • docker-local/setup.sh
  • portal-service/main.go
  • auth-service/deploy/keycloak/realm-export.json
  • chat-frontend/src/context/NatsContext/NatsContext.jsx
  • chat-frontend/src/context/NatsContext/NatsContext.test.jsx
  • go.mod
  • docs/client-api.md
  • portal-service/handler.go
  • portal-service/handler_test.go

📝 Walkthrough

Walkthrough

The PR adds portal-service OIDC login and callback handling, trims directory and user-info payloads to base URLs and IDs, registers a new Keycloak portal client, and updates chat-frontend to use static AUTH_URL, NATS_URL, and SITE_ID configuration.

Changes

Portal SSO front-door rollout

Layer / File(s) Summary
OIDC helpers and client registration
auth-service/deploy/keycloak/realm-export.json, go.mod, pkg/oidc/*, portal-service/oidclogin*, portal-service/mock_oidclogin_test.go
OIDC discovery helpers, the login-authenticator implementation, and the Keycloak portal client are added.
Portal config and directory shape
portal-service/main.go, portal-service/main_test.go, portal-service/deploy/docker-compose.yml, portal-service/store*, portal-service/cache_test.go, portal-service/integration_test.go, portal-service/handler.go, portal-service/handler_test.go
Portal-service startup validates OIDC config, updates the site registry, and narrows directory responses to base URLs and IDs.
Portal login and callback flow
portal-service/routes.go, portal-service/handler.go, portal-service/handler_test.go
Portal-service adds /login and /auth/callback, handshake cookies, code exchange, and callback redirect or error handling.
Chat-frontend static connection config
chat-frontend/deploy/*, chat-frontend/src/lib/runtimeConfig*, chat-frontend/src/context/NatsContext/*, chat-frontend/src/pages/LoginPage/LoginPage.jsx, docker-local/setup.sh
chat-frontend switches to static auth, NATS, and site config in its runtime config, connect flow, and deploy scripts.
Design notes and client API docs
docs/superpowers/plans/*, docs/superpowers/specs/*, docs/client-api.md
The design spec, implementation plan, and client API docs describe the SSO front-door flow, reduced directory payloads, and static frontend configuration.

Sequence Diagram(s)

Portal-service login callback flow

sequenceDiagram
  participant Browser
  participant portal_service as portal-service
  participant Keycloak
  Browser->>portal_service: GET /login
  portal_service-->>Browser: redirect to Keycloak authorize URL
  Browser->>Keycloak: authorize request
  Keycloak-->>Browser: redirect with code and state
  Browser->>portal_service: GET /auth/callback?code&state
  portal_service->>Keycloak: ExchangeAndVerify(code, nonce)
  portal_service->>portal_service: resolveSite(account)
  portal_service-->>Browser: redirect to resolved BaseURL
Loading

chat-frontend connect flow

sequenceDiagram
  participant Browser
  participant chat_frontend as chat-frontend
  participant auth_service as auth-service
  participant NATS
  Browser->>chat_frontend: load app
  chat_frontend->>auth_service: POST /auth
  auth_service-->>chat_frontend: JWT and user payload
  chat_frontend->>NATS: connect(NATS_URL, SITE_ID, JWT)
  NATS-->>chat_frontend: connected
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • hmchangw/chat#33: Adds the auth-service OIDC /auth flow and Keycloak realm changes that this PR’s portal-service and chat-frontend updates build on.
  • hmchangw/chat#295: Uses the portal-based dynamic site discovery flow that this PR removes from NatsContext.connect() and /api/userInfo.

Suggested reviewers

  • mliu33

Poem

Hop, hop — the portal found its key 🐰
Static paths now guide the NATS spree.
Login bounces through the OIDC gate,
Then home-site hops arrive right on time and date.
A cozy carrot cheer for config set free!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a portal SSO front-door and static per-site chat-frontend configuration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/gallant-darwin-k0sc1h

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@chat-frontend/src/context/NatsContext/NatsContext.jsx`:
- Around line 77-104: The connect flow in connectToNats needs a stale-request
guard before any state writes, not just in nc.closed(). After await
authResp.json() and again immediately after natsConnect, compare myGen to
connectGenRef.current and bail out if they differ so an older connect() cannot
overwrite setCredentials, ncRef.current, setUser, or setConnected after a newer
attempt has already started. Use connectToNats and connectGenRef as the key
points to place the guard.

In `@chat-frontend/src/lib/runtimeConfig.js`:
- Around line 6-13: The runtime config in runtimeConfig.js is still falling back
to localhost values, which should not happen outside development. Update the
AUTH_URL, NATS_URL, and SITE_ID resolution to use dev-only defaults and fail
fast when runtime config is missing in production, so missing or broken
config.js does not silently connect to localhost. Use the existing
runtimeConfig.js exports and the related deploy-time guard behavior to gate the
fallbacks by environment and surface a startup error when production values are
absent.

In `@docker-local/setup.sh`:
- Around line 117-118: The frontend env bootstrap in setup.sh currently uses
VITE_AUTH_URL as the only gate but appends defaults for VITE_AUTH_URL,
VITE_NATS_URL, and VITE_SITE_ID together, which can duplicate and overwrite
existing custom values. Update the logic around the FRONTEND_ENV_FILE checks so
each key is tested independently and only the missing key is appended. Keep the
fix localized to the env initialization block that writes the VITE_* variables.

In `@docs/superpowers/plans/2026-06-24-portal-sso-front-door.md`:
- Around line 820-825: The writeHTMLError example currently concatenates msg
directly into the HTML response, which leaves an XSS sink in the plan. Update
the example so the writeHTMLError helper escapes msg before embedding it in the
page, matching the safer approach already used in portal-service/handler.go via
template.HTMLEscapeString(msg). Keep the helper name and browser-facing error
page behavior the same, but ensure the rendered body cannot interpret raw HTML
from msg.
- Around line 126-145: `newOIDCLogin` only applies `oidc.ClientContext` during
discovery, so the custom HTTP client is not used by the later token exchange and
ID token verification flow in `oidcLogin.ExchangeAndVerify`. Store the provided
`httpClient` on `oidcLogin` and re-bind the context before calling
`o.oauth.Exchange` and `o.verifier.Verify`, or otherwise route both steps
through the same client-aware OIDC context so the custom transport/TLS settings
are preserved end to end.

In `@docs/superpowers/specs/2026-06-24-portal-sso-front-door-design.md`:
- Around line 203-205: Update the testing note in the design doc to match the
current suite: the statement that newOIDCLogin stays uncovered is stale because
portal-service/oidclogin_test.go now includes TestNewOIDCLogin. Adjust the
wording around oidclogin_test.go and newOIDCLogin so the coverage description
reflects the actual tests currently present.

In `@pkg/oidc/oidc.go`:
- Around line 65-68: The shared OIDC client helper is using a
certificate-validation bypass via InsecureSkipVerify, which should not live in
pkg/oidc or be suppressed with G402. Update the client creation logic in the
OIDC helper to use normal TLS verification by default, and move any
TLS_SKIP_VERIFY behavior into non-production-only local wiring or a separate
opt-in path that is clearly isolated from shared discovery/JWKS fetching. Keep
the http.Client and issuer discovery setup intact, but remove the insecure
transport from the reusable helper and avoid the `#nosec` suppression unless it is
a genuine false positive.

In `@portal-service/handler.go`:
- Around line 199-211: Short-circuit PortalHandler.HandleAuthCallback when auth
is unavailable in dev mode so the nil h.auth call is never reached. Add an early
guard before ExchangeAndVerify that detects a nil/unset auth client and returns
a deterministic redirect or not-found response instead of proceeding with the
callback flow. Keep the existing handshake/state checks intact, and make sure
the fallback happens in HandleAuthCallback before any h.auth usage.

In `@portal-service/integration_test.go`:
- Around line 53-56: The integration test fixtures still seed the legacy natsUrl
field even though the assertions in integration_test.go now expect the trimmed
employee schema. Update the hr_employee seed data used by the test so it matches
the new contract by removing natsUrl from the fixture rows, and keep the
existing assertions against byAccount and employee values aligned with the
updated data shape.

In `@portal-service/oidclogin.go`:
- Around line 23-26: The OIDC login flow is dropping the configured HTTP client
after discovery, so the callback path uses the default transport instead of the
intended client. Update oidcLogin and the newOIDCLogin/ExchangeAndVerify flow to
retain and reuse the same httpClient for both the OAuth code exchange and the
verifier/JWKS fetches, and make sure the verifier is created with
VerifierContext so all OIDC requests use the configured client consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d1d8450-f94e-4ab8-800e-53da2fe1d31c

📥 Commits

Reviewing files that changed from the base of the PR and between a27f2b0 and 31b3098.

📒 Files selected for processing (31)
  • auth-service/deploy/keycloak/realm-export.json
  • chat-frontend/deploy/30-render-config.sh
  • chat-frontend/deploy/config.js.template
  • chat-frontend/deploy/docker-compose.yml
  • chat-frontend/src/context/NatsContext/NatsContext.jsx
  • chat-frontend/src/context/NatsContext/NatsContext.test.jsx
  • chat-frontend/src/context/NatsContext/useJwtRefresh.js
  • chat-frontend/src/context/NatsContext/useJwtRefresh.test.js
  • chat-frontend/src/lib/runtimeConfig.js
  • chat-frontend/src/lib/runtimeConfig.test.js
  • chat-frontend/src/pages/LoginPage/LoginPage.jsx
  • docker-local/setup.sh
  • docs/client-api.md
  • docs/superpowers/plans/2026-06-24-portal-sso-front-door.md
  • docs/superpowers/specs/2026-06-24-portal-sso-front-door-design.md
  • go.mod
  • pkg/oidc/oidc.go
  • pkg/oidc/oidc_test.go
  • portal-service/cache_test.go
  • portal-service/deploy/docker-compose.yml
  • portal-service/handler.go
  • portal-service/handler_test.go
  • portal-service/integration_test.go
  • portal-service/main.go
  • portal-service/main_test.go
  • portal-service/mock_oidclogin_test.go
  • portal-service/oidclogin.go
  • portal-service/oidclogin_test.go
  • portal-service/routes.go
  • portal-service/store.go
  • portal-service/store_mongo.go
💤 Files with no reviewable changes (2)
  • portal-service/store_mongo.go
  • portal-service/cache_test.go

Comment thread chat-frontend/src/context/NatsContext/NatsContext.jsx
Comment thread chat-frontend/src/lib/runtimeConfig.js
Comment thread docker-local/setup.sh Outdated
Comment thread docs/superpowers/plans/2026-06-24-portal-sso-front-door.md
Comment thread docs/superpowers/plans/2026-06-24-portal-sso-front-door.md
Comment thread docs/superpowers/specs/2026-06-24-portal-sso-front-door-design.md Outdated
Comment thread pkg/oidc/oidc.go
Comment thread portal-service/handler.go
Comment thread portal-service/integration_test.go
Comment thread portal-service/oidclogin.go
…onfig

portal-service gains a confidential-OIDC SSO front-door: GET /login signs the
user in at Keycloak; GET /auth/callback verifies the code (state CSRF guard +
explicit nonce check), resolves the account's home-site baseUrl from the
in-process directory, and 302-redirects the browser there. /api/userInfo is
trimmed to {account, employeeId, siteId, baseUrl} and is no longer on the
connect path (retained as a directory endpoint).

chat-frontend reverts to static per-site config (AUTH_URL/NATS_URL/SITE_ID),
drops the portal lookup, and silently re-auths after the redirect.

Supporting changes:
- pkg/oidc: shared HTTPClient + DiscoverProvider helpers (also used by
  auth-service and upload-service).
- Keycloak: confidential `portal` client in the chatapp realm.
- docs/client-api.md: two-step connect narrative + trimmed /api/userInfo.

Review fixes incorporated:
- oidclogin: carry the configured httpClient through ExchangeAndVerify so the
  token exchange and JWKS fetch use its TLS/transport, not just discovery.
- portal handler: short-circuit /auth/callback in dev mode (nil-auth guard);
  nosemgrep the false-positive cookie-missing-secure on the handshake cookies.
- NatsContext: guard stale connect() completions before writing state.
- docker-local/setup.sh: append only individually-missing VITE_ keys.
- portal integration test: drop dead natsUrl seed field.
@vjauhari-work vjauhari-work force-pushed the claude/gallant-darwin-k0sc1h branch from 31b3098 to 1619e4c Compare June 26, 2026 04:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants