Skip to content

Commit a763eb1

Browse files
authored
feat: integrate @zitadel/tanstack-auth (#2)
* feat: initial example-tanstack-start-auth app with @zitadel/tanstack-start-auth * fix: align scaffold files for byte-level parity with other example repos - 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 * fix: add tailwind CSS entry file and align scaffold files with parity standards * fix: route devcontainer commands through devbox to use project-local npm cache * fix: update import to renamed @zitadel/tanstack-auth package * fix: add depcheck script * chore: bump prettier-plugin-tailwindcss to 0.6.14 * chore: add trailing newline to .nvmrc * chore: add .devbox/ to .gitignore * chore: switch to file: reference to generate package-lock.json * feat: integrate @zitadel/tanstack-auth and refactor auth routes * fix: migrate to @tanstack/react-start v1.168 vite-based API 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). * fix: ignore generated routeTree.gen.ts in prettier; gitignore dist - 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. * chore: align with @zitadel/tanstack-auth directory rename 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 * fix: redirect /api/auth/logout/callback to /logout/success and clear 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. * fix: mount React via hydrateRoot in client entry 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. * fix(auth/login): submit POST form with CSRF token 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. * refactor(profile): redirect unauth users via SDK signInUrl 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. * fix(logout/callback): validate state before clearing cookies 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. * fix(server): implement POST /api/auth/logout properly 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. * fix(logout): add conditional Secure flag to logout_state cookie 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. * chore(env): add AUTH_URL to .env.example Documents the @auth/core v5 base URL env var. Aligns with the other example apps so users get a consistent .env across all 8. * feat(auth/login): render provider name dynamically from /api/auth/providers 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. * chore(playwright): add AUTH_URL to test env Aligns with the other 7 examples; .env.example already had it. * chore(deps): switch @zitadel/tanstack-auth@^1.0.0 from file: to published 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. * chore: trigger CI * fix(deps): regenerate lockfile with linux-x64 optional binaries 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 * chore(devcontainer): decouple from devbox 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. * chore(knip): drop stale unresolved:off rule + SDK ignore 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. * chore: bump tailwind to ^4.3 + prettier plugin to ^0.8 + README v22 * chore: add X-Content-Type-Options + tsconfig moduleResolution PascalCase * feat(api): add /api/userinfo handler in server.tsx 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. * docs: add JSDoc on auth helpers + callbacks (parity with astro/nuxt/sveltekit/qwik) * chore: default callbackUrl to /profile + drop unused demo routes + bump typescript-eslint to ^8.60 * fix(deps): pin @auth/core to ^0.40.0 to match SDK (dedupe single copy) * fix: use client signIn helper on home and bump @zitadel/tanstack-auth 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.
1 parent 13d06c2 commit a763eb1

59 files changed

Lines changed: 9283 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.devcontainer/devcontainer.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "Zitadel Example TanStack Start Auth",
3+
"image": "mcr.microsoft.com/devcontainers/javascript-node:22-bookworm",
4+
"forwardPorts": [3000],
5+
"portsAttributes": {
6+
"3000": {
7+
"label": "Application",
8+
"onAutoForward": "notify"
9+
}
10+
},
11+
"otherPortsAttributes": {
12+
"onAutoForward": "silent"
13+
},
14+
"onCreateCommand": "cp -n .env.example .env",
15+
"updateContentCommand": "npm ci",
16+
"postCreateCommand": "npx playwright install --with-deps chromium",
17+
"customizations": {
18+
"vscode": {
19+
"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
20+
}
21+
}
22+
}

.editorconfig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
indent_style = space
6+
indent_size = 2
7+
end_of_line = lf
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
max_line_length = 120

.env.example

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# -----------------------------------------------------------------------------
2+
# App Configuration
3+
# -----------------------------------------------------------------------------
4+
# The environment in which the application is running. This should be set to
5+
# 'production' on your live server to enable security features like secure
6+
# cookies. For local development, 'development' is appropriate.
7+
NODE_ENV=development
8+
9+
# The network port on which the TanStack Start dev server will listen for incoming
10+
# connections. Change this if port 3000 is already in use on your system.
11+
PORT=3000
12+
13+
# -----------------------------------------------------------------------------
14+
# Session Configuration
15+
# -----------------------------------------------------------------------------
16+
# A long, random, and secret string used to sign the session cookie. This
17+
# prevents the cookie from being tampered with. It must be kept private.
18+
# Generate a secure key using:
19+
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
20+
SESSION_SECRET="your-very-secret-and-strong-session-key"
21+
22+
# The total duration of the session in seconds. After this period of
23+
# inactivity, the user will be effectively logged out.
24+
# Default is 3600, which is 1 hour (60 * 60).
25+
SESSION_DURATION=3600
26+
27+
# -----------------------------------------------------------------------------
28+
# ZITADEL OpenID Connect (OIDC) Configuration
29+
# -----------------------------------------------------------------------------
30+
# The full domain URL of your ZITADEL instance. You can find this in your
31+
# ZITADEL organization's settings.
32+
# Example: https://my-org-a1b2c3.zitadel.cloud
33+
ZITADEL_DOMAIN="https://your-zitadel-domain"
34+
35+
# The unique Client ID for your application, obtained from the ZITADEL Console.
36+
# This identifier tells ZITADEL which application is making the request.
37+
ZITADEL_CLIENT_ID="your-zitadel-application-client-id"
38+
39+
# While the Authorization Code Flow with PKCE for public clients
40+
# does not strictly require a client secret for OIDC specification compliance,
41+
# Auth.js will still require a value for its internal configuration.
42+
# Therefore, please provide a randomly generated string here.
43+
# You can generate a secure key using:
44+
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
45+
ZITADEL_CLIENT_SECRET="your-randomly-generated-client-secret"
46+
47+
# The full URL where ZITADEL redirects the user after they have authenticated.
48+
# This MUST exactly match one of the "Redirect URIs" you have configured in
49+
# your ZITADEL application settings.
50+
ZITADEL_CALLBACK_URL="http://localhost:3000/api/auth/callback/zitadel"
51+
52+
# The internal URL within your application where users are sent after a
53+
# successful login is processed at the callback URL.
54+
# Defaults to "/profile" if not specified.
55+
ZITADEL_POST_LOGIN_URL="/profile"
56+
57+
# The full URL where ZITADEL redirects the user after they have logged out.
58+
# This MUST exactly match one of the "Post Logout Redirect URIs" configured
59+
# in your ZITADEL application settings.
60+
ZITADEL_POST_LOGOUT_URL="http://localhost:3000/api/auth/logout/callback"
61+
62+
# The full public URL of your application.
63+
# Auth.js requires this to create secure callback and redirect links.
64+
# This is optional for local development but REQUIRED for production.
65+
AUTH_URL="http://localhost:3000"

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf

.github/dependabot.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: 'npm'
4+
directory: '/'
5+
schedule:
6+
interval: 'weekly'
7+
commit-message:
8+
prefix: 'chore(deps):'
9+
open-pull-requests-limit: 10
10+
groups:
11+
npm-version-updates:
12+
patterns:
13+
- '*'
14+
applies-to: 'version-updates'
15+
npm-security-updates:
16+
patterns:
17+
- '*'
18+
applies-to: 'security-updates'
19+
20+
- package-ecosystem: 'github-actions'
21+
directory: '/'
22+
schedule:
23+
interval: 'weekly'
24+
commit-message:
25+
prefix: 'chore(deps):'
26+
open-pull-requests-limit: 10
27+
groups:
28+
actions-version-updates:
29+
patterns:
30+
- '*'
31+
applies-to: 'version-updates'
32+
actions-security-updates:
33+
patterns:
34+
- '*'
35+
applies-to: 'security-updates'
36+
37+
- package-ecosystem: 'docker'
38+
directory: '/'
39+
schedule:
40+
interval: 'weekly'
41+
commit-message:
42+
prefix: 'chore(deps):'
43+
open-pull-requests-limit: 10
44+
groups:
45+
docker-version-updates:
46+
patterns:
47+
- '*'
48+
applies-to: 'version-updates'
49+
docker-security-updates:
50+
patterns:
51+
- '*'
52+
applies-to: 'security-updates'

.github/pull_request_template.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!--- Provide a general summary of your changes in the Title above -->
2+
3+
## Description
4+
5+
<!--- Describe your changes -->
6+
7+
## Related Issue
8+
9+
<!--- This project only accepts pull requests related to open issues -->
10+
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
11+
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
12+
<!--- Please link to the issue here: -->
13+
14+
## Motivation and Context
15+
16+
<!--- Why is this change required? What problem does it solve? -->
17+
18+
## How Has This Been Tested?
19+
20+
<!--- Please describe in detail how you tested your changes. -->
21+
<!--- Include details of your testing environment, and the tests you ran to -->
22+
<!--- see how your change affects other areas of the code, etc. -->
23+
24+
## Documentation:
25+
26+
<!--- Upon PR's approval, link the wiki page for your corresponding changes here. -->
27+
28+
## Checklist:
29+
30+
- [ ] I have updated the documentation accordingly.
31+
- [ ] I have assigned the correct milestone or created one if non-existent.
32+
- [ ] I have correctly labeled this pull request.
33+
- [ ] I have linked the corresponding issue in this description.
34+
- [ ] I have requested a review from at least 2 reviewers
35+
- [ ] I have checked the base branch of this pull request
36+
- [ ] I have checked my code for any possible security vulnerabilities

.github/workflows/commitlint.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Commits
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
ref:
7+
required: true
8+
type: string
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
lint-commits:
15+
permissions:
16+
contents: read
17+
pull-requests: read
18+
runs-on: ubuntu-latest
19+
name: Validate Commits
20+
21+
steps:
22+
- name: Harden runner
23+
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
24+
with:
25+
egress-policy: audit
26+
27+
- name: Checkout code
28+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29+
with:
30+
ref: ${{ inputs.ref }}
31+
fetch-depth: 0
32+
33+
- name: Inspect Commits
34+
uses: mridang/action-commit-lint@v1
35+
with:
36+
github-token: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/depcheck.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Dependency Review
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: read
8+
9+
jobs:
10+
dependency-review:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Harden Runner
14+
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
15+
with:
16+
egress-policy: audit
17+
18+
- name: Checkout code
19+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
20+
21+
- name: Review Dependencies
22+
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1

.github/workflows/linting.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Linting
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
ref:
7+
required: true
8+
type: string
9+
commit_changes:
10+
required: false
11+
type: boolean
12+
default: false
13+
14+
defaults:
15+
run:
16+
working-directory: ./
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
lint-format:
23+
permissions:
24+
contents: write
25+
runs-on: ubuntu-latest
26+
name: Reformat Code
27+
28+
steps:
29+
- name: Harden runner
30+
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
31+
with:
32+
egress-policy: audit
33+
34+
- name: Checkout code
35+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
36+
with:
37+
ref: ${{ inputs.ref }}
38+
39+
- name: Setup Node
40+
uses: actions/setup-node@v4
41+
with:
42+
cache: 'npm'
43+
node-version-file: '.nvmrc'
44+
45+
- name: Install Dependencies
46+
run: npm ci --no-progress
47+
48+
- name: Run Formatter
49+
run: npm run format
50+
51+
- name: Commit Changes
52+
if: ${{ inputs.commit_changes == true }}
53+
uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0
54+
with:
55+
commit_message: 'style: Apply automated code formatting [skip ci]'
56+
commit_options: '--no-verify'
57+
repository: .
58+
commit_user_name: github-actions[bot]
59+
commit_user_email: github-actions[bot]@users.noreply.github.com
60+
commit_author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

.github/workflows/pipeline.yml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: Pipeline
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- master
8+
- next
9+
- next-major
10+
- beta
11+
- alpha
12+
- '[0-9]*.x'
13+
- develop
14+
- 'release/**'
15+
- 'hotfix/**'
16+
- 'support/**'
17+
pull_request:
18+
19+
permissions:
20+
contents: write
21+
actions: read
22+
checks: write
23+
pull-requests: write
24+
25+
jobs:
26+
lint-commits:
27+
name: Run Commitlint Checks
28+
if: github.event_name == 'pull_request'
29+
uses: ./.github/workflows/commitlint.yml
30+
with:
31+
ref: ${{ github.event.pull_request.head.sha }}
32+
secrets: inherit
33+
34+
code-style:
35+
name: Run Linter Formatter
36+
uses: ./.github/workflows/linting.yml
37+
with:
38+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
39+
commit_changes: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
40+
secrets: inherit
41+
42+
type-check:
43+
name: Run Type Checks
44+
uses: ./.github/workflows/typecheck.yml
45+
with:
46+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
47+
secrets: inherit
48+
49+
run-tests:
50+
name: Run Test Suite
51+
uses: ./.github/workflows/test.yml
52+
with:
53+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
54+
secrets: inherit
55+
56+
check-deps:
57+
name: Run Dependency Checks
58+
uses: ./.github/workflows/unused.yml
59+
with:
60+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
61+
secrets: inherit
62+
63+
all-passed:
64+
name: Check Build Status
65+
runs-on: ubuntu-latest
66+
if: always()
67+
needs:
68+
- lint-commits
69+
- code-style
70+
- type-check
71+
- run-tests
72+
- check-deps
73+
steps:
74+
- name: Harden runner
75+
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
76+
with:
77+
egress-policy: audit
78+
79+
- name: Verify all jobs passed
80+
run: |
81+
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
82+
echo "One or more jobs failed or were cancelled."
83+
exit 1
84+
fi

0 commit comments

Comments
 (0)