Skip to content

Migrate the SPA examples: React, Vue, Nuxt, Angular and multitenant - #44

Merged
pnzrr merged 20 commits into
p2-inc:mainfrom
Wictorgirardi:feat/spa-examples
Oct 5, 2026
Merged

pnzrr merged 20 commits into
p2-inc:mainfrom
Wictorgirardi:feat/spa-examples

Conversation

@Wictorgirardi

@Wictorgirardi Wictorgirardi commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #28: the branch includes its commit.

This PR replaces #29, #30, #31, #32, #33, #34, #39 and #40: the examples that log in from the browser with a public client and PKCE. The Nuxt examples run as SPAs too (ssr: false).

Each example has its own commits and touches only its own folder and workflow. The commits follow the order of the table, so the PR is easiest to review commit by commit. The section for each example is the description of the PR it replaces.

Example Folder Commits Replaces
React (oidc-client-ts) frameworks/reactjs/oidc-client-ts 1 #29
React (oidc-spa) frameworks/reactjs/oidc-spa 1 #30
React (oidc-spa tutorial starter) frameworks/reactjs/oidc-spa-starter 1 #31
Vue (oidc-client-ts) frameworks/vue 1 #32
Nuxt (keycloak-js) frameworks/nuxt/keycloak-js 1 #33
Nuxt (oidc-client-ts) frameworks/nuxt/oidc-client-ts 1 #34
Multi-tenant (Nx) multitenant 13 #39
Angular (angular-oauth2-oidc) frameworks/angular 1 #40

React (oidc-client-ts)

Replaces #29.

Summary

  • Toolchain: Create React App → Vite 8, React 19.3, TypeScript 6.0, Tailwind CSS 4 (configured in CSS), ESLint 10 flat config and Prettier.
  • Auth libraries: react-oidc-context 2 → 3.3 and oidc-client-ts 2 → 3.5.
  • Config:
  • Auth fixes:
    • Redirects back to the current origin instead of the hardcoded production URL.
    • Logs out with signoutRedirect, so the Keycloak session ends too. It used to call removeUser, which only cleared local state.
    • Auth errors are now shown; before, that branch was unreachable.
  • UI:
    • Shows decoded access and ID tokens and drops the refresh-token panel.
    • Fixes the broken background fallback image and the GitHub link, which now points to this example.
    • Icons are inline SVG, so @iconify/react is no longer needed.
  • Tooling:
    • pnpm 10.34 via packageManager, Node 24 via .nvmrc and engines.
    • vercel.json pins the Vite preset, since the Vercel project was set up for CRA.
  • Workflow: now calls the shared CI and Vercel deploy workflows.

Test plan

  • pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm build pass on Node 24.
  • tools/e2e-smoke passes against the local Keycloak: log in, decoded tokens shown, log out.
  • Vercel preview deploy. It's skipped for PRs from forks, so it first runs after merge.

React (oidc-spa)

Replaces #30.

Summary

  • Toolchain: Create React App → Vite 8, React 19.3, TypeScript 6.0, Tailwind CSS 4, ESLint 10 flat config and Prettier. Same scaffold as the oidc-client-ts example above.
  • Auth library: oidc-spa 5 → 10.2, following the pattern used in Phase Two's dashboard and idp-wizard.
  • src/oidc.ts:
    • oidcSpa from oidc-spa/react-spa, with a zod looseObject schema so decodedIdToken is typed and still shows every claim.
    • bootstrapOidc reads VITE_OIDC_* env vars, including a mock mode (VITE_OIDC_USE_MOCK=true) and debug logs.
    • fetchWithAuth for calling APIs with the access token.
  • Vite: the oidcSpa() plugin with browserRuntimeFreeze.
  • UI:
    • OidcInitializationGate around the status area.
    • Logout through Keycloak with logout({ redirectTo: "home" }).
    • A "Manage account" link to the Keycloak account console.
    • Decoded access token (oidc-spa/decode-jwt, refreshed on rotation) and ID token panels.
    • An auto-logout warning overlay.
  • Removed:
    • The v5 createReactOidc / publicUrl setup. process.env.BASE_URL was always undefined under CRA.
    • The token console.log and the circular import from the entry file.
  • Deploy: vercel.json pins the Vite preset. The workflow calls the shared CI and deploy workflows.

Test plan

  • pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm build pass on Node 24.
  • tools/e2e-smoke passes against the local Keycloak: log in, decoded tokens shown, log out.
  • Mock mode: the mock user logs in and out with no page errors.
  • Vercel preview deploy. It's skipped for PRs from forks, so it first runs after merge.

React (oidc-spa tutorial starter)

Replaces #31.

Summary

The blog post Securing Keycloak with OIDC SPA and Phase Two links here as its starting point.

  • What it is now: exactly the oidc-spa example above minus the authentication code. Same Vite 8 / React 19 / TypeScript 6 / Tailwind 4 scaffold, same layout and assets, with .env already pointing at the demo realm. It used to be a CRA copy.
  • src/Auth.tsx: a clear placeholder. Before, it was a stub that wasn't even exported.
  • README: replaced the text copied from the oidc-client-ts example with the tutorial steps for oidc-spa 10:
    • pnpm add oidc-spa zod, then src/oidc.ts;
    • the Vite plugin;
    • the OidcInitializationGate;
    • the Auth.tsx component.
  • Workflow: new CI-only workflow. The starter has no deployment.

Test plan

  • pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm build pass on Node 24.
  • diff -r against the oidc-spa example shows only the auth files, vercel.json, dependencies and titles.
  • Applied the README's tutorial steps to a copy of the starter: it typechecks, builds, and logs in and out against the local Keycloak.

Vue (oidc-client-ts)

Replaces #32.

Summary

  • Toolchain: re-scaffolded from the current create-vue (3.24): Vite 4 → 8, vue-router 4 → 5, vue-tsc 1 → 3, TypeScript 5.1 → 6.0, ESLint 8 → 10 flat config (@vue/eslint-config-typescript), Tailwind CSS 3 → 4 (configured in CSS). The devtools plugin, oxlint and npm-run-all are left out to keep the example small.
  • Auth library: oidc-client-ts 2 → 3.5.
  • src/auth.ts: a single UserManager, configured from .env (VITE_OIDC_ISSUER_URI, VITE_OIDC_CLIENT_ID). It now uses the library's default session storage; the old explicit WebStorageStateStore stored tokens in localStorage.
  • src/composables/useAuth.ts: reactive user state driven by oidc-client-ts events, with signIn / signOut.
  • Components: <script setup lang="ts"> everywhere. This replaces the Options API and the untyped $auth global, which was assigned after mount().
  • Bug fixes:
    • /silent-refresh now calls signinSilentCallback(). It used to call a non-existent renewToken().
    • /auth returns to the page the login started from.
    • The router guard is removed; it redirected to a missing /unauthenticated route.
  • UI:
    • Shared layout with inline SVG icons (drops the iconify-icon web component).
    • Fixes the className / htmlFor React-isms, the "(keycloak-js)" header label and the README titled "React".
    • Components renamed to multi-word names (AppHeader, AppFooter, UserStatus, TokenPanels), as eslint-plugin-vue requires.
  • Tooling:
    • pnpm 10.34 via packageManager, Node 24 via .nvmrc / engines.
    • vercel.json pins the Vite preset and keeps the SPA rewrite.
    • The workflow calls the shared CI and deploy workflows.

Test plan

  • pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm build pass on Node 24.
  • tools/e2e-smoke passes against the local Keycloak: log in, decoded tokens shown, log out.
  • Vercel preview deploy. It's skipped for PRs from forks, so it first runs after merge.

Nuxt (keycloak-js)

Replaces #33.

Summary

  • Stack: Nuxt 3.20 → 4.5 with the Nuxt 4 app/ directory layout, TypeScript 6.0 (nuxt typecheck), @nuxt/eslint with ESLint 10, and Tailwind CSS 4 through @tailwindcss/vite. The PostCSS setup, tailwind.config.js and the pnpm shamefully-hoist .npmrc are gone, and @nuxt/devtools: "latest" is no longer pinned.
  • Auth library: keycloak-js 25 → 26.2.
  • app/plugins/keycloak.client.ts:
    • Now awaits init before the app renders, so the logged-out UI no longer flashes.
    • Uses check-sso with a silent check page (public/silent-check-sso.html) instead of a full-page round trip on every load, plus PKCE S256 and checkLoginIframe: false.
    • Refreshes the token on onTokenExpired; it was never refreshed before.
    • Exposes the adapter through provide.
  • Config: comes from runtimeConfig.public (NUXT_PUBLIC_KEYCLOAK_URL, _REALM, _CLIENT_ID) instead of hardcoded values. .env.example targets the local Keycloak from Add shared CI workflows, local Keycloak and contributing guide #28.
  • Reactive state: useKeycloak() holds it in useState, and logout redirects back to the app.
  • Rendering: app.vue renders <NuxtLayout><NuxtPage/>. Before, the pages and layout were dead code.
  • UI:
    • Shared layout with inline SVG icons (drops iconify-icon and jwt-decode, since keycloak-js already provides parsed tokens).
    • <script setup> everywhere, and the htmlFor React-isms are fixed.
  • Tooling: pnpm 10.34 via packageManager (with onlyBuiltDependencies), Node 24. The workflow calls the shared CI and deploy workflows.

Test plan

  • pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm build pass on Node 24; the build produces .vercel/output.
  • tools/e2e-smoke passes against the local Keycloak: log in, decoded tokens shown, log out.
  • Reloading while logged in keeps the session through the silent check-sso.
  • Vercel preview deploy. It's skipped for PRs from forks, so it first runs after merge.

Nuxt (oidc-client-ts)

Replaces #34.

Summary

  • Stack: Nuxt 3.20 → 4.5 (app/ layout), TypeScript 6.0, @nuxt/eslint with ESLint 10, and Tailwind CSS 4 through @tailwindcss/vite. Same scaffold as the Nuxt keycloak-js example above.
  • Libraries: oidc-client-ts 2 → 3.5, pinia 2 → 4, @pinia/nuxt 0.4 → 1.0.
  • Broken build fixed: services/application-service.ts imported a ./keycloak-config file that was deleted in 2024.
  • app/plugins/oidc.client.ts: a single UserManager. Before, every useServices() call created a new one, each with its own silent-renew timer.
  • app/stores/auth.ts: a Pinia setup store driven by oidc-client-ts events, with signIn, signOut and the callback handlers. Components read it with storeToRefs; the old component copied authStore.user and lost reactivity.
  • app/middleware/auth.global.ts: loads the stored user once, before any page renders.
  • Config:
    • Comes from runtimeConfig.public (NUXT_PUBLIC_OIDC_ISSUER_URI, NUXT_PUBLIC_OIDC_CLIENT_ID), replacing the require("dotenv") call (dotenv wasn't a declared dependency).
    • The issuer is now a full realm URL instead of a domain with /auth/realms/ appended.
    • The unused client secret is removed; this is a public SPA.
  • Storage: tokens use the library's default session storage. The old explicit WebStorageStateStore used localStorage.
  • Removed: the /logout page (logout is signOutRedirect from the store), overrides: { vue: "latest" }, and @nuxt/devtools: "latest".
  • .env.example: targets the local Keycloak from Add shared CI workflows, local Keycloak and contributing guide #28. It used to contain the retired usw2.auth.ac host and the wrong client (remix-example).
  • UI: shared layout with inline SVG icons, <script setup> everywhere, and the fixed "[oidc-client-js]" label.

Test plan

  • pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm build pass on Node 24.
  • tools/e2e-smoke passes against the local Keycloak: log in, decoded tokens shown, log out.
  • Reloading while logged in keeps the session.
  • Vercel preview deploy. It's skipped for PRs from forks, so it first runs after merge. The old KEYCLOAK_* env vars on the Vercel project become unused and can be removed.

Multi-tenant (Nx)

Replaces #39.

Summary

  • Nx 22.4 → 23.2:
    • First applied the 28 migrations that were never run, then ran nx migrate 23.2.1 and applied its migrations.
    • migrations.json is removed, analytics are off, and Nx's deprecated Vite plugins are replaced with Vite 8's own resolve.tsconfigPaths.
  • Tooling:
    • pnpm only: the stale root and nested package-lock.json files are gone. Node 24.
    • React 18 → 19.3, Vite 7 → 8, TypeScript 6.0 (no baseUrl, no ignoreDeprecations), Tailwind CSS 4 configured in CSS. The old Tailwind themes were empty, so the p2blue classes never rendered.
    • ESLint 9.39 with Nx's flat configs (eslint-plugin-react, used by Nx's React config, doesn't support ESLint 10), and Prettier 3.
  • Tests: Jest is removed, and every project uses Vitest 4 through @nx/vitest. Nx 23's Vitest plugin doesn't support Vitest 5 yet. The old specs couldn't pass; the new ones cover:
    • the apps under oidc-spa mock mode;
    • the API client's Authorization header;
    • the Organizations component.
  • Auth:
    • oidc-spa 5 → 10: src/oidc.ts in each app, which also ends the circular imports into main.tsx.
    • The Vite plugin and OidcInitializationGate.
    • Config comes from .env (VITE_OIDC_ISSUER_URI, VITE_OIDC_CLIENT_ID) instead of the <CLIENT> / <REALM> placeholders in code.
  • API client:
    • libs/api-manager is now createOrgsApi({ issuerUri, getAccessToken }). It derives the base URL and realm from the issuer and asks oidc-spa for a fresh token on every request.
    • Before, the token was captured once into static headers and expired. The old await oidc.getTokens().accessToken would also have sent Bearer undefined with oidc-spa 10.
  • Generated client:
    • Regenerated with OpenAPI Generator 7.25 from the current spec. The old script pointed at the phasetwo-docs master branch, which now returns 404.
    • Generator options live in openapitools.json, and generated code isn't hand-edited.
  • UI:
    • The shared layout from the other examples.
    • One card per organization with the user's roles and whether they grant access to the current app.
    • Separate ports: zoo 4200, aquarium 4201 (both used 4200 before).
    • The GitHub link now points to multitenant.
  • CI: new CI-only workflow, multitenant.yml.

Test plan

  • From a clean install on Node 24: pnpm install --frozen-lockfile && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build all pass: 5 projects, 5 tests, 2 app builds.

  • End to end against the local Keycloak with docker compose -f keycloak/docker-compose.yml --profile orgs up -d --wait, in dev and in production builds. /orgs/me returns 200:

    App User California New York
    zoo jane zoo: access zoo, aquarium: access
    aquarium jacques aquarium: access aquarium: access
    zoo jacques aquarium: no access aquarium: no access
    aquarium jane zoo: no access zoo, aquarium: access
  • actionlint passes on the workflow.

Docs drift

blog/2024-11-11-multi-tenancy-with-organizations.md:

  • L94–101: hosted-only setup with * redirect URIs. The example defaults to the local Keycloak, where clients, users (jane / jacques) and organizations are preconfigured. Clients need PKCE and http://localhost:4200/* or http://localhost:4201/*.
  • L102 vs L104: the org is called "new york" in one place and "newyork" in the other; the example uses newyork with display name "New York".
  • L110–112: "change the realm/client in code" → per-app .env / .env.local.
  • L116: the orgs.png screenshot shows the old UI.
  • L74–82: the "User 1 / User 2" example contradicts the setup steps. That was already the case before this PR.

Angular (angular-oauth2-oidc)

Replaces #40.

Summary

  • Toolchain: re-scaffolded with ng new from Angular CLI 22.2 (standalone, zoneless, @angular/build application builder, Vitest unit tests), plus angular-eslint 22.5 with ESLint 10.
    • Angular 21.1 → 22.2, TypeScript 6.0.
    • zone.js, Karma/Jasmine, @angular-devkit/build-angular, @angular/animations and platform-browser-dynamic are removed.
  • Tailwind CSS 4: now actually configured, through .postcssrc.json and @tailwindcss/postcss. Before, v4 was installed but set up the v3 way: the builder ignored postcss.config.js and loaded the v3 plugin.
  • Install fixed: CI's npm install failed on a jasmine-core peer conflict. The stale package-lock.json (Angular 18) is gone; the example uses pnpm 10.34 (packageManager) and Node 24.
  • Auth library: angular-oauth2-oidc 20 → 22.
    • OAuth is configured in provideAppInitializer, instead of a component constructor that didn't await discovery.
    • A signals-based AuthService exposes the login state and the decoded tokens.
    • Templates use @if, inject() and real <button>s.
    • Discovery errors are shown instead of breaking the bootstrap.
  • Auth config:
    • Comes from the environment files: production uses the hosted demo realm, ng serve uses the local Keycloak from Add shared CI workflows, local Keycloak and contributing guide #28.
    • The redirect and post-logout URIs are the app origin instead of /index.html.
    • The offline_access scope (offline tokens) and showDebugInformation are removed.
  • UI: the shared layout with inline SVG icons (the three SVG files are gone), the className React-ism fixed, and the GitHub link now points to this example.
  • Tests: two working specs replace the broken ones; the old NullInjectorError and missing <h1> issues are gone.
  • Deploy: vercel.json pins the Angular preset and output directory. The workflow calls the shared CI and deploy workflows.

Test plan

  • pnpm install --frozen-lockfile && pnpm lint && pnpm test && pnpm build pass on Node 24; 3 tests pass.
  • tools/e2e-smoke passes against the local Keycloak on port 4200.
  • Reloading keeps the session, logout ends the Keycloak session, and there are no page errors.
  • Vercel preview deploy. It's skipped for PRs from forks, so it first runs after merge.

@pnzrr
pnzrr force-pushed the feat/spa-examples branch from 5849b14 to 1791d4a Compare October 5, 2026 16:41
@pnzrr
pnzrr merged commit 6566d20 into p2-inc:main Oct 5, 2026
14 checks passed
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.

2 participants