Skip to content

feat: integrate @zitadel/tanstack-auth#2

Merged
mridang merged 36 commits into
mainfrom
feat/use-zitadel-auth
May 29, 2026
Merged

feat: integrate @zitadel/tanstack-auth#2
mridang merged 36 commits into
mainfrom
feat/use-zitadel-auth

Conversation

@mridang
Copy link
Copy Markdown
Collaborator

@mridang mridang commented May 26, 2026

Description

Migrates this example to the published @zitadel/tanstack-auth SDK, replacing the hand-rolled auth wiring with the SDK's factory pattern (handlers, getSession, signIn, signOut, signInUrl, signOutUrl). Switches the dependency from file:../tanstack-auth to the npm-published version and aligns the /auth/login, /auth/error, and /profile routes with the rest of the SDK family. Dropped the ~/auth.server import from the /profile loader — tanstack-start's import-protection plugin rejects **/*.server.* imports from route files.

Related Issue

N/A — part of the family-wide migration to the @zitadel/*-auth SDKs.

Motivation and Context

The example previously hand-rolled OIDC/PKCE wiring on top of @auth/core and pulled the SDK via a file: link. Both go away once @zitadel/tanstack-auth is published: the example becomes a small consumer of the SDK and any consumer can copy it without local checkouts. This also brings the example's env vars (AUTH_URL, AUTH_SECRET, callback paths under /api/auth) in line with the other 7 example apps.

How Has This Been Tested?

Locally via devbox: npm run lint, npm run format:check, npm run prepack (typecheck), and npm run build all pass. Playwright E2E suite runs in CI. Manual smoke: full login → /profile → logout against a real Zitadel instance.

Documentation:

N/A — README updates for the new SDK shape landed in earlier commits on this branch.

Checklist:

  • I have updated the documentation accordingly.
  • I have assigned the correct milestone or created one if non-existent.
  • I have correctly labeled this pull request.
  • I have linked the corresponding issue in this description.
  • I have requested a review from at least 2 reviewers
  • I have checked the base branch of this pull request
  • I have checked my code for any possible security vulnerabilities

mridang added 29 commits May 26, 2026 14:19
- Add missing .editorconfig
- Update devbox.lock to Node.js 22.22.0 (matches other repos)
- Fix .gitignore: remove .astro/.svelte-kit/dist artifacts, add .vinxi for Vinxi build
- Fix .prettierignore: remove .astro (not used by TanStack Start)
- Fix .env.example: correct framework name in comment
- Fix playwright.config.ts: align ZITADEL_CALLBACK_URL to match other repos
- Fix README.md: comprehensive documentation matching pattern of other examples
- Fix knip.config.js: add TanStack Start-specific entry points
The installed @tanstack/react-start v1.168 no longer exports
'@tanstack/react-start/config' or '@tanstack/react-start/api', and
Meta/Scripts/StartClient are gone. This commit aligns the example with
the new vite-plugin-based API the SDK playground uses:

- Replace vinxi + app.config.ts with vite.config.ts using
  tanstackStart({srcDirectory:'app'}) and @vitejs/plugin-react.
- Rewrite app/server.tsx to use createStartHandler + createServerEntry
  and inline the /api/auth/* and /api/userinfo handlers (replacing the
  removed createAPIFileRoute pattern).
- Rewrite app/client.tsx to hydrateStart() and app/router.tsx to use
  createRouter with routeTree.gen.
- Add app/session.ts with a createServerFn-based fetchSession helper.
- Switch /profile to a loader-based pattern using fetchSession.
- Drop validateSearch from /auth/login to avoid the search-param
  redirect loop (matches SDK playground).
- Move Session/JWT augmentation into app/types/auth.d.ts and add
  tsconfig paths to dedup @auth/core.
- Turn off plain no-unused-vars in eslint config (it false-positives
  on TS function-type parameter names).
- Clean up knip.config (drop stale entries, ignore routeTree.gen.ts).
- Add app/routeTree.gen.ts to .prettierignore so format:check no
  longer fails on the auto-generated route tree.
- Add dist/ to .gitignore and remove the previously-committed build
  output. The build is reproducible from source.
The sibling SDK directory was renamed from tanstack-start-auth to
tanstack-auth; this commit updates the example's package metadata to
match:

- package.json — name now matches the SDK family
- @zitadel/tanstack-auth file: link points at ../tanstack-auth (was
  ../tanstack-start-auth)
- regenerated package-lock.json
…logout_state cookie

The handler in app/server.tsx was redirecting to /, but the test in
test/app.spec.ts expects /logout/success — which is the actual page
the user should land on after RP-initiated logout completes. It also
wasn't clearing the logout_state cookie. logout_state is set with
Path=/api/auth/logout/callback when the logout flow starts, so the
clearing Set-Cookie needs the same Path attribute or the browser
will retain the cookie.
The previous client.tsx only called hydrateStart() from
@tanstack/react-start/client, which initialises the TanStack router
but does not mount React. As a result, no useEffect ran and no event
handlers attached — every page was static HTML pretending to be a
React app. SSR worked and every test that only clicked plain anchors
continued to pass, hiding the bug.

Switches client.tsx to the canonical TanStack Start template:
hydrateRoot(document, <StrictMode><StartClient /></StrictMode>) inside
a startTransition. StartClient internally calls hydrateStart, so this
is a strict superset of the previous behaviour.
The previous anchor pointed at GET /api/auth/signin/zitadel which
Auth.js rejects (the per-provider endpoint requires a POST with a
CSRF token); the user landed on /auth/error?error=Configuration on
click. Matches the pattern used by example-sveltekit-auth and
example-solidstart-auth: fetch /api/auth/csrf in useEffect, populate
a hidden input, submit a POST form to /api/auth/signin/zitadel.
Replaces the hardcoded throw redirect({ to: '/auth/login' }) with
throw redirect({ href: signInUrl({ redirectTo: '/profile' }) }).
signInUrl is the canonical way to encapsulate the sign-in URL
across the SDK family.
The previous handler cleared authjs.* cookies and the logout_state
cookie unconditionally on any GET /api/auth/logout/callback. The
other seven example apps validate the `state` query parameter
against the `logout_state` cookie before clearing — preventing an
attacker from triggering an unwanted logout via a crafted link.

Brings tanstack into parity with the other seven by:
1. Reading `state` from the URL and `logout_state` from the cookie jar
2. Only clearing cookies when both are present and match
3. Otherwise redirecting to /logout/error?reason=Invalid+or+missing+state+parameter.
4. Adding Clear-Site-Data: "cookies" + HttpOnly; SameSite=Lax on the
   logout_state expiry header to match the other seven verbatim

The existing app.spec.ts already exercises the success path
(provides state + matching cookie) and continues to pass.
Previously stubbed with 405 ("back-channel logout not implemented"),
so clicking SignOutButton hit a dead endpoint and the user never
left the app. Replaces the stub with the same handler the other
seven examples use: load session, generate state, set
Path-scoped logout_state cookie, redirect to Zitadel's
end_session_endpoint. The /api/auth/logout/callback handler then
validates state on return (already in place from the prior commit).

Verified in-browser via the Sign out button: now redirects through
Zitadel and lands on /logout/success with cookies cleared.
The logout_state cookie was being set without the Secure flag in any
environment. In production (HTTPS) the CSRF cookie should always be
Secure; in dev (HTTP) it must be unset so the browser doesn't drop
the cookie and silently break the state round-trip.

Apply the same conditional pattern used by example-remix-auth.
Documents the @auth/core v5 base URL env var. Aligns with the other
example apps so users get a consistent .env across all 8.
…viders

Match the dynamic-provider pattern used by the other examples: fetch
/api/auth/providers + /api/auth/csrf in parallel, render the form
with provider.signinUrl and "Sign in with {provider.name}" instead
of hardcoded values.
Aligns with the other 7 examples; .env.example already had it.
…shed version

Switch package.json from file:../tanstack-auth to ^1.0.0 (now
published on npm). Also drop the signInUrl import from
~/auth.server in profile.tsx — tanstack-start's import-protection
plugin rejects `**/*.server.*` imports from route files (since
routes run in both environments), and signInUrl was only used to
build a static redirect URL. Use a route-relative redirect instead.
Adds cross-platform install entries for native deps (@rollup,
@oxc-resolver, @oxc-parser) so 'npm ci' on Linux runners finds the
linux-x64-gnu binaries. The previous lockfile was generated on
darwin-arm64 only, omitting the Linux entries; CI hit npm/cli#4828
and refused to install them.

Regenerated via:
  npm install --include=optional --os=linux --cpu=x64 --package-lock-only
  npm install --include=optional
devbox is a local-only nix-based package manager; coupling it to the
devcontainer image (which already provides Node via the base image)
creates an unnecessary dependency for contributors using the
devcontainer. Remove the devbox feature and use plain npm ci /
playwright install instead.
The unresolved:off rule was added for an earlier tanstack-router issue that is no longer reproducible. The SDK ignore was always wrong since the SDK is imported by app code.
@mridang mridang force-pushed the feat/use-zitadel-auth branch from 761f261 to 53fd4ad Compare May 27, 2026 12:23
mridang added 7 commits May 27, 2026 16:05
Adds a /api/userinfo GET handler to the server entry's path-match
chain. Mirrors the other 7 examples; sits next to the existing
/api/auth/* and /api/(un)?protected blocks. TanStack Start doesn't
have a file-based API route convention so this lives inline.
… to 1.1.2

The home Login button hardcoded a GET to /api/auth/signin/zitadel, which
Auth.js v5 rejects with a Configuration error. Use the SDK client signIn
helper (CSRF+POST) and bump the SDK to the version that ships the fix.
@mridang mridang merged commit a763eb1 into main May 29, 2026
7 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.

1 participant