@zitadel/sdk-core 1.0.0-alpha.23 → 1.0.0-alpha.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,14 +28,25 @@ export declare const INTERNAL_HEADERS: ReadonlySet<string>;
28
28
  */
29
29
  export declare function matchesRoutes(pathname: string, routes: readonly string[]): boolean;
30
30
  /**
31
- * Filters upstream response headers for proxying: strips hop-by-hop headers,
32
- * `set-cookie` (handled separately via `getSetCookie()`), and `location`
33
- * (prevents leaking internal upstream URLs).
31
+ * Filters upstream response headers for proxying: strips hop-by-hop headers
32
+ * and `set-cookie` (handled separately via `getSetCookie()`), and reduces
33
+ * `location` to a redirect that can only land on this app.
34
+ *
35
+ * `location` was stripped outright until the identity-provider callback
36
+ * needed it: the provider returns the browser to `/__nextgen/idp/callback`,
37
+ * and the engine answers `302` back to the page the sign-in started on. That
38
+ * is a top-level navigation with no JavaScript in the loop, so a dropped
39
+ * `Location` leaves the browser on an empty page. Outbound redirects to a
40
+ * provider do not travel this way -- they arrive as `step.redirect_url` in a
41
+ * JSON body and the widget navigates -- so nothing legitimate points off-origin.
34
42
  *
35
43
  * @param upstream - The upstream response headers.
44
+ * @param selfUrl - The app's own request URL, which is both the origin a
45
+ * redirect must land on and the base a relative one resolves against.
46
+ * Omitting it keeps the old behaviour of dropping every redirect.
36
47
  * @returns A new `Headers` object with filtered headers.
37
48
  */
38
- export declare function filterResponseHeaders(upstream: Headers): Headers;
49
+ export declare function filterResponseHeaders(upstream: Headers, selfUrl?: string): Headers;
39
50
  /**
40
51
  * The authenticated session for a signed-in user. Identity follows the
41
52
  * user-ref vocabulary (ADR 058): render `display`, falling back to
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,WAAW,CAAC,MAAM,CAazC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAqC,CAAC;AAIvF;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAUlF;AAID;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAYhE;AAID;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;OAGG;IACH,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,oFAAoF;IACpF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,6CAA6C;AAC7C,MAAM,MAAM,SAAS,GAAG;IAAE,eAAe,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,CAAC;AAE3E,iDAAiD;AACjD,MAAM,MAAM,WAAW,GAAG;IAAE,eAAe,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,CAAC;AAEpE,yCAAyC;AACzC,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,+DAA+D;IAC/D,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,6CAA6C;IAC7C,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,CAAC;AAEF,yDAAyD;AACzD,MAAM,MAAM,eAAe,GAAG;IAAE,eAAe,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAC;AAEhF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAAG,WAAW,CAAC;AAE7D;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAE3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAEzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAE7B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,WAAW,CAAC,MAAM,CAazC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAqC,CAAC;AAIvF;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAUlF;AA2CD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAiBlF;AAID;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;OAGG;IACH,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,oFAAoF;IACpF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,6CAA6C;AAC7C,MAAM,MAAM,SAAS,GAAG;IAAE,eAAe,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,CAAC;AAE3E,iDAAiD;AACjD,MAAM,MAAM,WAAW,GAAG;IAAE,eAAe,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,CAAC;AAEpE,yCAAyC;AACzC,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,+DAA+D;IAC/D,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,6CAA6C;IAC7C,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,CAAC;AAEF,yDAAyD;AACzD,MAAM,MAAM,eAAe,GAAG;IAAE,eAAe,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAC;AAEhF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAAG,WAAW,CAAC;AAE7D;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAE3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAEzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAE7B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC"}
@@ -54,21 +54,78 @@ export function matchesRoutes(pathname, routes) {
54
54
  }
55
55
  // ─── Response header filtering ───────────────────────────────────────────────
56
56
  /**
57
- * Filters upstream response headers for proxying: strips hop-by-hop headers,
58
- * `set-cookie` (handled separately via `getSetCookie()`), and `location`
59
- * (prevents leaking internal upstream URLs).
57
+ * An upstream `Location`, pointed back at this app.
58
+ *
59
+ * Only the path, query and fragment survive; whatever origin the upstream
60
+ * named is discarded and the result is re-based onto the request's own URL.
61
+ * That keeps both properties the blanket strip was protecting -- an internal
62
+ * hostname can never reach the browser, and the redirect can only ever land on
63
+ * this app -- without needing to know what this app's public origin is.
64
+ *
65
+ * Comparing origins instead would have required that knowledge, and there is
66
+ * no reliable source for it: `X-Forwarded-Host` is attacker-settable on a
67
+ * direct request, and the request's own host is wrong behind a proxy that
68
+ * terminates TLS or rewrites it. Discarding the origin sidesteps the question
69
+ * -- a redirect to another origin becomes the same path on this one rather
70
+ * than being dropped, so a deployment behind such a proxy works instead of
71
+ * landing on a blank page.
72
+ *
73
+ * Nothing legitimate on this path points off-origin anyway: an outbound
74
+ * redirect to a provider travels as `step.redirect_url` in a JSON body and the
75
+ * widget performs it, never as a proxied header.
76
+ *
77
+ * A relative reference resolves against the path it came from (RFC 3986 §5),
78
+ * so `next?flow=x` answered to `/__nextgen/idp/callback` means
79
+ * `/__nextgen/idp/next?flow=x`. An unparseable value is the one case with
80
+ * nothing to forward.
81
+ */
82
+ function ontoThisApp(location, selfUrl) {
83
+ if (selfUrl === undefined || selfUrl === "") {
84
+ return undefined;
85
+ }
86
+ try {
87
+ const base = new URL(selfUrl);
88
+ const target = new URL(location, base);
89
+ return new URL(`${target.pathname}${target.search}${target.hash}`, base).toString();
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
95
+ /**
96
+ * Filters upstream response headers for proxying: strips hop-by-hop headers
97
+ * and `set-cookie` (handled separately via `getSetCookie()`), and reduces
98
+ * `location` to a redirect that can only land on this app.
99
+ *
100
+ * `location` was stripped outright until the identity-provider callback
101
+ * needed it: the provider returns the browser to `/__nextgen/idp/callback`,
102
+ * and the engine answers `302` back to the page the sign-in started on. That
103
+ * is a top-level navigation with no JavaScript in the loop, so a dropped
104
+ * `Location` leaves the browser on an empty page. Outbound redirects to a
105
+ * provider do not travel this way -- they arrive as `step.redirect_url` in a
106
+ * JSON body and the widget navigates -- so nothing legitimate points off-origin.
60
107
  *
61
108
  * @param upstream - The upstream response headers.
109
+ * @param selfUrl - The app's own request URL, which is both the origin a
110
+ * redirect must land on and the base a relative one resolves against.
111
+ * Omitting it keeps the old behaviour of dropping every redirect.
62
112
  * @returns A new `Headers` object with filtered headers.
63
113
  */
64
- export function filterResponseHeaders(upstream) {
114
+ export function filterResponseHeaders(upstream, selfUrl) {
65
115
  const filtered = new Headers();
66
116
  upstream.forEach((value, key) => {
67
- if (!HOP_BY_HOP.has(key.toLowerCase()) &&
68
- key.toLowerCase() !== "set-cookie" &&
69
- key.toLowerCase() !== "location") {
70
- filtered.set(key, value);
117
+ const name = key.toLowerCase();
118
+ if (HOP_BY_HOP.has(name) || name === "set-cookie") {
119
+ return;
120
+ }
121
+ if (name === "location") {
122
+ const onThisApp = ontoThisApp(value, selfUrl);
123
+ if (onThisApp !== undefined) {
124
+ filtered.set("location", onThisApp);
125
+ }
126
+ return;
71
127
  }
128
+ filtered.set(key, value);
72
129
  });
73
130
  return filtered;
74
131
  }