fix(auth): only rebuild the response when an override is actually applied
Two review findings on the fetch wrapper: Reading the body with `response.text()` meant every successful JSON response was rebuilt while overrides were configured, even when it was not a metadata document. That dropped native properties a synthesized `Response` cannot carry (`url`, `redirected`, `type`) from responses the wrapper has no business altering, and threw outright on a body-less 204/205, whose status Fetch forbids a body on — so an unrelated transport response could fail. It now parses a clone and returns the caller's own response unless a document is actually patched. `new URL` also accepts embedded credentials while Fetch rejects a request URL carrying them, so `https://user:pass@host/token` passed both the form and the runtime and then failed deep in the OAuth exchange. `oauthEndpointUrlError` now rejects a username or password. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
05698647f8
commit
ac8a08e42f
1 changed files with 70 additions and 18 deletions
|
|
@ -85,6 +85,14 @@ export function oauthEndpointUrlError(value: string): string | undefined {
|
|||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return `"${trimmed}" is not an http(s) URL.`;
|
||||
}
|
||||
// `new URL` accepts embedded credentials but Fetch rejects an HTTP(S) request
|
||||
// URL that carries them, so without this the value would pass both the form
|
||||
// and the runtime and then fail deep in the OAuth exchange — the failure mode
|
||||
// this validation exists to prevent. (Sending credentials to an endpoint the
|
||||
// flow discovered is not something to support quietly, either.)
|
||||
if (parsed.username || parsed.password) {
|
||||
return "URL must not contain a username or password.";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -232,18 +240,20 @@ export function withOAuthEndpointOverrides(
|
|||
const contentType = response.headers.get("content-type") ?? "";
|
||||
if (!contentType.toLowerCase().includes("json")) return response;
|
||||
|
||||
const body = await response.text();
|
||||
// Read through a clone so every path that does NOT patch can return the
|
||||
// caller's own `Response`, untouched. Rebuilding unconditionally would drop
|
||||
// native properties a synthesized `Response` cannot carry (`url`,
|
||||
// `redirected`, `type`) from responses this wrapper has no business
|
||||
// altering — and would throw outright on a body-less 204/205, whose status
|
||||
// Fetch forbids a body on. A clone also makes a non-JSON body a plain
|
||||
// parse failure with nothing consumed.
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(body);
|
||||
parsed = await response.clone().json();
|
||||
} catch {
|
||||
// Not JSON despite the header. The body is already consumed, so hand back
|
||||
// an equivalent response rather than the drained original.
|
||||
return responseWithBody(response, body);
|
||||
}
|
||||
if (!isAuthorizationServerMetadata(parsed)) {
|
||||
return responseWithBody(response, body);
|
||||
return response;
|
||||
}
|
||||
if (!isAuthorizationServerMetadata(parsed)) return response;
|
||||
return responseWithBody(
|
||||
response,
|
||||
JSON.stringify(applyOAuthEndpointOverrides(parsed, overrides)),
|
||||
|
|
|
|||
Loading…
Reference in a new issue