Skip to content

feat(frontend): runtime config + real OIDC sign-in for the FrontX portal - #10

Merged
SysoevAndrey merged 6 commits into
mainfrom
frontend-auth
Aug 15, 2026
Merged

feat(frontend): runtime config + real OIDC sign-in for the FrontX portal#10
SysoevAndrey merged 6 commits into
mainfrom
frontend-auth

Conversation

@SysoevAndrey

Copy link
Copy Markdown
Contributor

What

The first post-foundation slice for the FrontX portal (#9): a runtime config layer and a real Keycloak sign-in on the FrontX auth contract.

  • Runtime configwindow.__STUDIO_ENV__ (injected by the already-shipped docker/10-runtime-env.sh) wins over VITE_* dev fallbacks; index.html loads /env.js before the bundle; the Vite dev server gains the same-origin /cf proxy mirroring nginx.conf.template.
  • KeycloakOidcProvider — the prototype's dependency-free Authorization Code + PKCE S256 client, reshaped onto @gears-frontx/auth's AuthProvider (login/handleCallback/refresh/logout/getIdentity/subscribe). Access token stays in memory; refresh + id tokens under the prototype's studio.oidc.* sessionStorage keys. Over the prototype: a state parameter on the redirect (CSRF), in-provider refresh dedup, and a static-token developer login for the backend's static-auth profiles.
  • TransportcreateFrontXApp({ auth: { provider } }): the default frontxApiTransport() attaches Bearer to every REST call of the host and all MFEs and does one deduplicated refresh-and-retry after a 401.
  • Real /meAccountsApiService now targets /cf/account-management/v1; the header user is assembled from token claims with the /me subject id as the opaque-token fallback. The scaffold's fake /api/accounts pipeline and its test helpers are gone.
  • AuthGate + LoginScreen + Header — callback completion (StrictMode-safe), URL scrubbing, session restore from the refresh token, sign-in screen (SSO + IdP hints + collapsed static-token dev path validated against /me before establishing a session), and the header returns with the current user and RP-initiated sign-out.

Verified live against the compose stack

Full checklist on docker compose (postgres + keycloak + backend) + npm run dev:all, Mock API off:

  1. SSO login (admin/studio) → Keycloak → callback → app mounts, URL scrubbed clean.
  2. Header shows the signed-in user (claims), /me served by the real gear through the dev proxy.
  3. Reload restores the session from the refresh token — no re-login.
  4. Sign out → RP-initiated Keycloak logout → back to the sign-in screen.
  5. Static-token 401 in the docker profile is by design (the authn resolver picks OIDC over static); the dev path works with config/dev.yaml.

Zero console errors/warnings throughout. Static: 61 tests, type-check, arch:deps (0 violations), npm ci-clean build.

Out of scope (stated in code)

SSE auth (the framework transport is REST-only — matters for mini-chat later), OIDC discovery (fixed Keycloak endpoints, as in the prototype), deep links across login (redirect_uri is always /).

🤖 Generated with Claude Code

Andrey Sysoev and others added 4 commits August 15, 2026 10:12
Ports the prototype's env mechanism: window.__STUDIO_ENV__ (written by
the already-present docker/10-runtime-env.sh at container start) wins
over VITE_* build-time fallbacks; index.html loads /env.js before the
bundle; public/env.js is the dev placeholder. Vite dev server gains the
same-origin /cf proxy that nginx.conf.template provides in containers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
…ract

Port of the prototype's dependency-free Authorization Code + PKCE S256
client onto @gears-frontx/auth AuthProvider: login/handleCallback/
refresh/logout/getIdentity/subscribe. Access token stays in memory;
sessionStorage keeps the refresh + id tokens under the prototype's
studio.oidc.* keys. Additions over the prototype: a state parameter on
the redirect (CSRF), in-provider refresh dedup, and a static-token dev
login path for the backend's static profiles. Fixed Keycloak endpoints,
no discovery — deliberate, as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
createFrontXApp gains auth: { provider: keycloakOidcProvider } — the
default frontxApiTransport() attaches Bearer to every REST call of the
host and all MFEs and performs one deduplicated refresh-and-retry after
a 401. AccountsApiService now targets /cf/account-management/v1 with a
single /me identity check; the header user is assembled from token
claims (name/preferred_username/email) with the /me subject id as the
fallback for opaque dev tokens. The scaffold's fake /api/accounts user
pipeline and its test helpers are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
AuthGate wraps the app: completes the OIDC redirect callback when
?code is present (re-entrant-safe against StrictMode), scrubs the OIDC
params from the URL, restores the session from the refresh token, and
mounts the authenticated app only once a session exists — so Layout's
fetchCurrentUser always runs with a token. LoginScreen offers SSO
(+ google/github/microsoft IdP hints) and the collapsed static-token
developer path, validated against /me before the session is
established. Header returns to Layout with the current user and a
sign-out that clears the header state and performs RP-initiated logout.
Styled with the shell's Tailwind + aligned Studio palette (light DOM —
ui-kit component CSS reads complete-color tokens the shell defines as
HSL triplets).

Verified live without an IdP: the gate renders the login screen, the
SSO button navigates to the correct Keycloak authorize URL (client_id,
redirect_uri, state, S256 challenge — connection refused only because
no local Keycloak), the static path fails gracefully when the backend
is down. The full SSO round-trip needs the compose stack (no container
runtime on this machine).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review available on request

  • 🔍 Trigger review

Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment @coderabbitai review to review the latest changes. For a full review, comment @coderabbitai full review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 61e21b4c-dae1-430d-884a-ab373a3b4ccd


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.

@SysoevAndrey SysoevAndrey self-assigned this Aug 15, 2026
Andrey Sysoev and others added 2 commits August 15, 2026 11:47
Blocking:
- treat a non-JSON 200 from the token endpoint as a rejected refresh and
  land every AuthGate failure on the login screen — a misconfigured issuer
  could previously strand the app on "Restoring session…" forever
- gate the static-token dev sign-in behind import.meta.env.DEV in both the
  login screen and the provider; the panel, the default token string and
  the session-establishing branch are eliminated from production bundles

Correctness:
- decode JWT claims as UTF-8 (non-ASCII display names arrived mangled)
- mark explicit sign-out transitions so the UI stops saying "Session
  expired" after the user clicks Sign out
- surface OIDC ?error callbacks on the login screen and scrub them from
  the URL (the fragment now survives scrubbing too)
- discard refresh results that resolve after logout (session epoch)
- cap token-endpoint fetches with AbortSignal.timeout so a hung IdP can't
  hang every REST call behind the transport's getSession await
- fail closed when a PKCE verifier exists without a stored state
- request 'openid profile email' explicitly instead of relying on
  Keycloak default client scopes
- getIdentity reads the ID token (access token only as dev fallback) and
  never triggers a network refresh
- re-arm the proactive renewal timer after a transient network failure
- route AuthGate/LoginScreen/Header/bootstrapEffects through the framework
  auth runtime (useFrontX().auth) instead of the provider singleton

Cleanups: remove the dead mock-user pipeline (ApiUser/UserRole/UserExtra,
notifyUserLoaded, app/user/loaded), share ACCOUNTS_API_BASE_URL with the
login /me probe, log only error messages on /me failure (an AxiosError
would print the bearer token), warn when the issuer falls back to the
localhost default in prod, guard crypto.subtle with a readable error, fix
the stale env.ts path in docker/10-runtime-env.sh.

Tests: new AuthGate/LoginScreen/Header suites and provider cases for every
fixed path (fake-timer renewal loop, logout-during-refresh, non-JSON
responses, UTF-8 claims, prod gating); UTF-8-safe makeJwt helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
… timeouts

Follow-ups from the re-review:
- AuthGate no longer throws when createFrontXApp() was built without an
  auth plugin (the no-uikit scaffold entry shares App.tsx): it renders the
  app ungated with a loud console warning instead of crashing on mount
- tests assert both token fetches carry an AbortSignal and that an aborted
  (timed out) refresh behaves like a network failure — the refresh token
  survives for the next attempt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
@SysoevAndrey
SysoevAndrey merged commit 9397b4f into main Aug 15, 2026
6 checks passed
@SysoevAndrey
SysoevAndrey deleted the frontend-auth branch August 15, 2026 10:07
AndrejK666 pushed a commit that referenced this pull request Aug 16, 2026
…tal (#10)

* feat(frontend): runtime config layer + /cf dev proxy

Ports the prototype's env mechanism: window.__STUDIO_ENV__ (written by
the already-present docker/10-runtime-env.sh at container start) wins
over VITE_* build-time fallbacks; index.html loads /env.js before the
bundle; public/env.js is the dev placeholder. Vite dev server gains the
same-origin /cf proxy that nginx.conf.template provides in containers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>

* feat(frontend): Keycloak OIDC provider (PKCE) on the FrontX auth contract

Port of the prototype's dependency-free Authorization Code + PKCE S256
client onto @gears-frontx/auth AuthProvider: login/handleCallback/
refresh/logout/getIdentity/subscribe. Access token stays in memory;
sessionStorage keeps the refresh + id tokens under the prototype's
studio.oidc.* keys. Additions over the prototype: a state parameter on
the redirect (CSRF), in-provider refresh dedup, and a static-token dev
login path for the backend's static profiles. Fixed Keycloak endpoints,
no discovery — deliberate, as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>

* feat(frontend): wire the auth transport; /me against the real gear

createFrontXApp gains auth: { provider: keycloakOidcProvider } — the
default frontxApiTransport() attaches Bearer to every REST call of the
host and all MFEs and performs one deduplicated refresh-and-retry after
a 401. AccountsApiService now targets /cf/account-management/v1 with a
single /me identity check; the header user is assembled from token
claims (name/preferred_username/email) with the /me subject id as the
fallback for opaque dev tokens. The scaffold's fake /api/accounts user
pipeline and its test helpers are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>

* feat(frontend): sign-in gate, login screen, header with sign-out

AuthGate wraps the app: completes the OIDC redirect callback when
?code is present (re-entrant-safe against StrictMode), scrubs the OIDC
params from the URL, restores the session from the refresh token, and
mounts the authenticated app only once a session exists — so Layout's
fetchCurrentUser always runs with a token. LoginScreen offers SSO
(+ google/github/microsoft IdP hints) and the collapsed static-token
developer path, validated against /me before the session is
established. Header returns to Layout with the current user and a
sign-out that clears the header state and performs RP-initiated logout.
Styled with the shell's Tailwind + aligned Studio palette (light DOM —
ui-kit component CSS reads complete-color tokens the shell defines as
HSL triplets).

Verified live without an IdP: the gate renders the login screen, the
SSO button navigates to the correct Keycloak authorize URL (client_id,
redirect_uri, state, S256 challenge — connection refused only because
no local Keycloak), the static path fails gracefully when the backend
is down. The full SSO round-trip needs the compose stack (no container
runtime on this machine).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>

* fix(frontend): harden the auth slice per independent review

Blocking:
- treat a non-JSON 200 from the token endpoint as a rejected refresh and
  land every AuthGate failure on the login screen — a misconfigured issuer
  could previously strand the app on "Restoring session…" forever
- gate the static-token dev sign-in behind import.meta.env.DEV in both the
  login screen and the provider; the panel, the default token string and
  the session-establishing branch are eliminated from production bundles

Correctness:
- decode JWT claims as UTF-8 (non-ASCII display names arrived mangled)
- mark explicit sign-out transitions so the UI stops saying "Session
  expired" after the user clicks Sign out
- surface OIDC ?error callbacks on the login screen and scrub them from
  the URL (the fragment now survives scrubbing too)
- discard refresh results that resolve after logout (session epoch)
- cap token-endpoint fetches with AbortSignal.timeout so a hung IdP can't
  hang every REST call behind the transport's getSession await
- fail closed when a PKCE verifier exists without a stored state
- request 'openid profile email' explicitly instead of relying on
  Keycloak default client scopes
- getIdentity reads the ID token (access token only as dev fallback) and
  never triggers a network refresh
- re-arm the proactive renewal timer after a transient network failure
- route AuthGate/LoginScreen/Header/bootstrapEffects through the framework
  auth runtime (useFrontX().auth) instead of the provider singleton

Cleanups: remove the dead mock-user pipeline (ApiUser/UserRole/UserExtra,
notifyUserLoaded, app/user/loaded), share ACCOUNTS_API_BASE_URL with the
login /me probe, log only error messages on /me failure (an AxiosError
would print the bearer token), warn when the issuer falls back to the
localhost default in prod, guard crypto.subtle with a readable error, fix
the stale env.ts path in docker/10-runtime-env.sh.

Tests: new AuthGate/LoginScreen/Header suites and provider cases for every
fixed path (fake-timer renewal loop, logout-during-refresh, non-JSON
responses, UTF-8 claims, prod gating); UTF-8-safe makeJwt helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>

* fix(frontend): AuthGate tolerates a missing auth runtime; cover fetch timeouts

Follow-ups from the re-review:
- AuthGate no longer throws when createFrontXApp() was built without an
  auth plugin (the no-uikit scaffold entry shares App.tsx): it renders the
  app ungated with a loud console warning instead of crashing on mount
- tests assert both token fetches carry an AbortSignal and that an aborted
  (timed out) refresh behaves like a network failure — the refresh token
  survives for the next attempt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>

---------

Signed-off-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
Co-authored-by: Andrey Sysoev <andrey.sysoev@constructor.tech>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant