Files
zitadel/apps/login/package.json
T
4dc89b499f fix(login): allow custom protocols for native apps again (#12332)
# Which Problems Are Solved

- Native apps (mobile/desktop) use custom protocol schemes (e.g.
`myapp://callback`, `io.zitadel.app://auth`) as OAuth/OIDC redirect
URIs. The previous redirect-safety check in the login UI only allowed
`http:`/`https:` absolute URLs, so these custom-scheme callbacks were
wrongly rejected and native app login flows were broken.
- Even for valid external redirects, `router.push()` triggers an RSC
prefetch `fetch()` that is blocked by the CSP `connect-src 'self'`
directive for non-same-origin URLs, so navigation to
external/custom-protocol targets failed.
- SAML and OIDC callback URLs returned from the backend were not
consistently validated before being used for redirects/POSTs across the
various flow entry points.

# How the Problems Are Solved

- Reworked `isSafeRedirectUri` to use `@braintree/sanitize-url` instead
of a strict `http/https`-only allowlist. Custom protocol schemes are now
permitted, while dangerous schemes (`javascript:`, `data:`, plus an
explicit block of `file:`, `blob:`, `about:`) are still rejected.
- Added `isExternalUrl` helper to distinguish internal relative paths
(`/...`) from external/custom-protocol targets.
- In `handleServerActionResponse`, external/custom-protocol redirects
now navigate via `window.location.href` (full navigation) instead of
`router.push()`, avoiding the CSP-blocked RSC prefetch. Internal paths
continue to use `router.push()`.
- Added consistent redirect-URI validation for SAML and OIDC callbacks
across all relevant entry points (`oidc.ts`, `saml.ts`,
`server/flow-initiation.ts` including the `prompt=none` path, and the
SAML `samlData` POST case), blocking unsafe URLs with a clear warning
and error response.

# Additional Changes

- Added `@braintree/sanitize-url` (`^7.1.2`) dependency to `apps/login`
(`package.json` / `pnpm-lock.yaml`).
- Added unit tests for `isExternalUrl` covering relative paths, absolute
HTTP/HTTPS URLs, custom protocol schemes, and protocol-relative URLs.
- Annotated the intentional `window.location.href` navigation with a
CodeQL suppression comment, documenting that the URL is validated by
`isSafeRedirectUri` beforehand.
- Minor formatting cleanup in `console` JWT provider component.

# Additional Context

- This restores behavior that previously worked for native apps but
regressed when redirect validation was tightened to `http`/`https` only.

---------

Co-authored-by: Max Peintner <[email protected]>
2026-06-30 13:19:40 +00:00

118 lines
4.2 KiB
JSON

{
"packageManager": "[email protected]+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
"name": "@zitadel/login",
"private": true,
"type": "module",
"scripts": {
"build": "NEXT_OUTPUT_MODE=standalone next build && cp -r public scripts/* .next/standalone/ && mv .next/standalone/apps/login/server.js .next/standalone/apps/login/next-server.js && cp .next/standalone/server.mjs .next/standalone/apps/login/server.js && cp -r .next/static .next/standalone/apps/login/.next/ && rm .next/standalone/apps/login/.env",
"build-vercel": "next build",
"dev": "HOSTNAME=127.0.0.1 ./scripts/entrypoint.sh next dev",
"prod": "cd ./.next/standalone && HOSTNAME=127.0.0.1 ./entrypoint.sh node apps/login/server.js",
"lint-check-next": "eslint .",
"lint-check-prettier": "prettier --check .",
"lint-fix": "prettier --write .",
"test-unit": "vitest --run",
"test-dockerized": "vitest --run --config vitest.config.dockerized.ts",
"lint-staged": "lint-staged",
"clean": "rm -rf node_modules .next"
},
"git": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@braintree/sanitize-url": "^7.1.2",
"@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-node": "^2.1.1",
"@headlessui/react": "^2.2.10",
"@heroicons/react": "^2.2.0",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api-logs": "^0.218.0",
"@opentelemetry/auto-instrumentations-node": "^0.76.0",
"@opentelemetry/resource-detector-container": "^0.8.8",
"@opentelemetry/resource-detector-gcp": "^0.53.0",
"@opentelemetry/resources": "^2.7.1",
"@opentelemetry/sdk-node": "^0.218.0",
"@opentelemetry/winston-transport": "^0.28.0",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/forms": "^0.5.11",
"clsx": "^2.1.1",
"copy-to-clipboard": "^3.3.3",
"deepmerge": "^4.3.1",
"escape-html": "^1.0.3",
"lru-cache": "^11.3.6",
"lucide-react": "^0.577.0",
"moment": "^2.30.1",
"next": "16.2.6",
"next-intl": "^4.11.2",
"next-themes": "^0.4.6",
"nice-grpc": "^2.1.16",
"qrcode.react": "^4.2.0",
"react": "19.2.6",
"react-dom": "19.2.6",
"react-hook-form": "^7.75.0",
"server-only": "^0.0.1",
"tinycolor2": "^1.6.0",
"uuid": "^14.0.0",
"winston": "^3.19.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@faker-js/faker": "^9.9.0",
"@next/eslint-plugin-next": "16.2.6",
"@opentelemetry/sdk-trace-base": "^2.7.1",
"@opentelemetry/sdk-trace-node": "^2.7.1",
"@otplib/core": "^12.0.1",
"@otplib/plugin-crypto": "^12.0.1",
"@otplib/plugin-thirty-two": "^12.0.1",
"@playwright/test": "^1.60.0",
"@tailwindcss/postcss": "^4.3.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/escape-html": "^1.0.4",
"@types/node": "^25.7.0",
"@types/node-forge": "^1.3.14",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/tinycolor2": "^1.4.6",
"@types/uuid": "^11.0.0",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"@vercel/git-hooks": "^1.0.0",
"@vitejs/plugin-react": "^4.7.0",
"@zitadel/client": "workspace:*",
"@zitadel/proto": "workspace:*",
"concurrently": "^9.2.1",
"dotenv-cli": "^11.0.0",
"env-cmd": "^11.0.0",
"esbuild": "^0.28.0",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-cypress": "^4.3.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react-hooks": "^7.1.1",
"gaxios": "^7.1.4",
"globals": "^17.6.0",
"grpc-tools": "^1.13.1",
"jsdom": "^26.1.0",
"lint-staged": "^16.4.0",
"node-forge": "^1.4.0",
"nodemon": "^3.1.14",
"postcss": "^8.5.14",
"prettier": "^3.8.3",
"prettier-plugin-organize-imports": "^4.3.0",
"prettier-plugin-tailwindcss": "^0.8.0",
"sass": "^1.99.0",
"start-server-and-test": "^3.0.4",
"tailwindcss": "^4.3.0",
"testcontainers": "^11.14.0",
"ts-proto": "^2.11.8",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.1.6"
}
}