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]>
This commit is contained in:
Livio Spring
2026-06-30 13:19:40 +00:00
committed by GitHub
co-authored by Max Peintner
parent f9995ee39c
commit 4dc89b499f
8 changed files with 113 additions and 16 deletions
+10
View File
@@ -208,6 +208,9 @@ importers:
apps/login:
dependencies:
'@braintree/sanitize-url':
specifier: ^7.1.2
version: 7.1.2
'@connectrpc/connect':
specifier: ^2.1.1
version: 2.1.1(@bufbuild/[email protected])
@@ -975,6 +978,7 @@ packages:
'@angular/[email protected]':
resolution: {integrity: sha512-9WLnsJE0xqtd1rVtHMvsAUxFy3OdPks4bdmUIqyw23X/je7ytUALAGWNadffcZBwRpa1A6TUnLr9X4+Draz3kw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
deprecated: '@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.'
peerDependencies:
'@angular/core': 21.2.14
@@ -1105,6 +1109,7 @@ packages:
'@angular/[email protected]':
resolution: {integrity: sha512-m5U4zX8JFnxTAIGpsBXIAyefSmYqdORY/OfHC0aMmZovuFCbXXIYqYRQDBB7+YVNpSDSHllCrKEZFu/CC6dq3g==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
deprecated: '@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.'
peerDependencies:
'@angular/common': 21.2.14
'@angular/compiler': 21.2.14
@@ -1308,6 +1313,9 @@ packages:
'@balena/[email protected]':
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
'@braintree/[email protected]':
resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
'@bufbuild/[email protected]':
resolution: {integrity: sha512-c7owUswBbMmwfHPH9JRBEJu09mrXYGC33V2JQCgraWCBm74Z95AOkhDua50qiBrQnysvJkJ0p/z4MWxJqcpnIA==}
engines: {node: '>=12'}
@@ -13140,6 +13148,8 @@ snapshots:
'@balena/[email protected]': {}
'@braintree/[email protected]': {}
'@bufbuild/[email protected]':
optional: true