Repository navigation
Migrate the SPA examples: React, Vue, Nuxt, Angular and multitenant - #44
Merged
Merged
Conversation
This was referenced Sep 24, 2026
pnzrr
self-requested a review
October 5, 2026 16:35
pnzrr
force-pushed
the
feat/spa-examples
branch
from
October 5, 2026 16:41
5849b14 to
1791d4a
Compare
pnzrr
approved these changes
Oct 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
frameworks/reactjs/oidc-client-tsframeworks/reactjs/oidc-spaframeworks/reactjs/oidc-spa-starterframeworks/vueframeworks/nuxt/keycloak-jsframeworks/nuxt/oidc-client-tsmultitenantframeworks/angularReact (oidc-client-ts)
Replaces #29.
Summary
.env(VITE_OIDC_ISSUER_URI,VITE_OIDC_CLIENT_ID), which holds the public hosted demo values..env.local.sampletargets the local Keycloak from Add shared CI workflows, local Keycloak and contributing guide #28.signoutRedirect, so the Keycloak session ends too. It used to callremoveUser, which only cleared local state.@iconify/reactis no longer needed.packageManager, Node 24 via.nvmrcandengines.vercel.jsonpins the Vite preset, since the Vercel project was set up for CRA.Test plan
pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm buildpass on Node 24.tools/e2e-smokepasses against the local Keycloak: log in, decoded tokens shown, log out.React (oidc-spa)
Replaces #30.
Summary
src/oidc.ts:oidcSpafromoidc-spa/react-spa, with a zodlooseObjectschema sodecodedIdTokenis typed and still shows every claim.bootstrapOidcreadsVITE_OIDC_*env vars, including a mock mode (VITE_OIDC_USE_MOCK=true) and debug logs.fetchWithAuthfor calling APIs with the access token.oidcSpa()plugin withbrowserRuntimeFreeze.OidcInitializationGatearound the status area.logout({ redirectTo: "home" }).oidc-spa/decode-jwt, refreshed on rotation) and ID token panels.createReactOidc/publicUrlsetup.process.env.BASE_URLwas always undefined under CRA.console.logand the circular import from the entry file.vercel.jsonpins the Vite preset. The workflow calls the shared CI and deploy workflows.Test plan
pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm buildpass on Node 24.tools/e2e-smokepasses against the local Keycloak: log in, decoded tokens shown, log out.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.
.envalready 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.pnpm add oidc-spa zod, thensrc/oidc.ts;OidcInitializationGate;Auth.tsxcomponent.Test plan
pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm buildpass on Node 24.diff -ragainst the oidc-spa example shows only the auth files,vercel.json, dependencies and titles.Vue (oidc-client-ts)
Replaces #32.
Summary
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.src/auth.ts: a singleUserManager, configured from.env(VITE_OIDC_ISSUER_URI,VITE_OIDC_CLIENT_ID). It now uses the library's default session storage; the old explicitWebStorageStateStorestored tokens in localStorage.src/composables/useAuth.ts: reactive user state driven by oidc-client-ts events, withsignIn/signOut.<script setup lang="ts">everywhere. This replaces the Options API and the untyped$authglobal, which was assigned aftermount()./silent-refreshnow callssigninSilentCallback(). It used to call a non-existentrenewToken()./authreturns to the page the login started from./unauthenticatedroute.iconify-iconweb component).className/htmlForReact-isms, the "(keycloak-js)" header label and the README titled "React".AppHeader,AppFooter,UserStatus,TokenPanels), aseslint-plugin-vuerequires.packageManager, Node 24 via.nvmrc/engines.vercel.jsonpins the Vite preset and keeps the SPA rewrite.Test plan
pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm buildpass on Node 24.tools/e2e-smokepasses against the local Keycloak: log in, decoded tokens shown, log out.Nuxt (keycloak-js)
Replaces #33.
Summary
app/directory layout, TypeScript 6.0 (nuxt typecheck),@nuxt/eslintwith ESLint 10, and Tailwind CSS 4 through@tailwindcss/vite. The PostCSS setup,tailwind.config.jsand the pnpmshamefully-hoist.npmrcare gone, and@nuxt/devtools: "latest"is no longer pinned.app/plugins/keycloak.client.ts:initbefore the app renders, so the logged-out UI no longer flashes.check-ssowith a silent check page (public/silent-check-sso.html) instead of a full-page round trip on every load, plus PKCE S256 andcheckLoginIframe: false.onTokenExpired; it was never refreshed before.provide.runtimeConfig.public(NUXT_PUBLIC_KEYCLOAK_URL,_REALM,_CLIENT_ID) instead of hardcoded values..env.exampletargets the local Keycloak from Add shared CI workflows, local Keycloak and contributing guide #28.useKeycloak()holds it inuseState, and logout redirects back to the app.app.vuerenders<NuxtLayout><NuxtPage/>. Before, the pages and layout were dead code.iconify-iconandjwt-decode, since keycloak-js already provides parsed tokens).<script setup>everywhere, and thehtmlForReact-isms are fixed.packageManager(withonlyBuiltDependencies), Node 24. The workflow calls the shared CI and deploy workflows.Test plan
pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm buildpass on Node 24; the build produces.vercel/output.tools/e2e-smokepasses against the local Keycloak: log in, decoded tokens shown, log out.check-sso.Nuxt (oidc-client-ts)
Replaces #34.
Summary
app/layout), TypeScript 6.0,@nuxt/eslintwith ESLint 10, and Tailwind CSS 4 through@tailwindcss/vite. Same scaffold as the Nuxt keycloak-js example above.@pinia/nuxt0.4 → 1.0.services/application-service.tsimported a./keycloak-configfile that was deleted in 2024.app/plugins/oidc.client.ts: a singleUserManager. Before, everyuseServices()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, withsignIn,signOutand the callback handlers. Components read it withstoreToRefs; the old component copiedauthStore.userand lost reactivity.app/middleware/auth.global.ts: loads the stored user once, before any page renders.runtimeConfig.public(NUXT_PUBLIC_OIDC_ISSUER_URI,NUXT_PUBLIC_OIDC_CLIENT_ID), replacing therequire("dotenv")call (dotenv wasn't a declared dependency)./auth/realms/appended.WebStorageStateStoreused localStorage./logoutpage (logout issignOutRedirectfrom 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 retiredusw2.auth.achost and the wrong client (remix-example).<script setup>everywhere, and the fixed "[oidc-client-js]" label.Test plan
pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm buildpass on Node 24.tools/e2e-smokepasses against the local Keycloak: log in, decoded tokens shown, log out.KEYCLOAK_*env vars on the Vercel project become unused and can be removed.Multi-tenant (Nx)
Replaces #39.
Summary
nx migrate 23.2.1and applied its migrations.migrations.jsonis removed, analytics are off, and Nx's deprecated Vite plugins are replaced with Vite 8's ownresolve.tsconfigPaths.package-lock.jsonfiles are gone. Node 24.baseUrl, noignoreDeprecations), Tailwind CSS 4 configured in CSS. The old Tailwind themes were empty, so thep2blueclasses never rendered.eslint-plugin-react, used by Nx's React config, doesn't support ESLint 10), and Prettier 3.@nx/vitest. Nx 23's Vitest plugin doesn't support Vitest 5 yet. The old specs couldn't pass; the new ones cover:Authorizationheader;src/oidc.tsin each app, which also ends the circular imports intomain.tsx.OidcInitializationGate..env(VITE_OIDC_ISSUER_URI,VITE_OIDC_CLIENT_ID) instead of the<CLIENT>/<REALM>placeholders in code.libs/api-manageris nowcreateOrgsApi({ issuerUri, getAccessToken }). It derives the base URL and realm from the issuer and asks oidc-spa for a fresh token on every request.await oidc.getTokens().accessTokenwould also have sentBearer undefinedwith oidc-spa 10.masterbranch, which now returns 404.openapitools.json, and generated code isn't hand-edited.multitenant.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 buildall 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/mereturns 200:actionlint passes on the workflow.
Docs drift
blog/2024-11-11-multi-tenancy-with-organizations.md:*redirect URIs. The example defaults to the local Keycloak, where clients, users (jane/jacques) and organizations are preconfigured. Clients need PKCE andhttp://localhost:4200/*orhttp://localhost:4201/*.newyorkwith display name "New York"..env/.env.local.orgs.pngscreenshot shows the old UI.Angular (angular-oauth2-oidc)
Replaces #40.
Summary
ng newfrom Angular CLI 22.2 (standalone, zoneless,@angular/buildapplication builder, Vitest unit tests), plusangular-eslint22.5 with ESLint 10.@angular-devkit/build-angular,@angular/animationsandplatform-browser-dynamicare removed..postcssrc.jsonand@tailwindcss/postcss. Before, v4 was installed but set up the v3 way: the builder ignoredpostcss.config.jsand loaded the v3 plugin.npm installfailed on ajasmine-corepeer conflict. The stalepackage-lock.json(Angular 18) is gone; the example uses pnpm 10.34 (packageManager) and Node 24.provideAppInitializer, instead of a component constructor that didn't await discovery.AuthServiceexposes the login state and the decoded tokens.@if,inject()and real<button>s.ng serveuses the local Keycloak from Add shared CI workflows, local Keycloak and contributing guide #28./index.html.offline_accessscope (offline tokens) andshowDebugInformationare removed.classNameReact-ism fixed, and the GitHub link now points to this example.NullInjectorErrorand missing<h1>issues are gone.vercel.jsonpins 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 buildpass on Node 24; 3 tests pass.tools/e2e-smokepasses against the local Keycloak on port 4200.