Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion packages/client/src/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,17 @@ export async function startAuthorization(
throw new Error(`Incompatible auth server: does not support code challenge method ${AUTHORIZATION_CODE_CHALLENGE_METHOD}`);
}
} else {
// Guard against silent path loss: new URL('/authorize', 'https://example.com/admin')
// → https://example.com/authorize (subpath '/admin' is silently lost).
// Only use root-path fallback when AS is at the domain root.
if (authorizationServerUrl.pathname !== '/') {
throw new Error(
`Authorization server metadata discovery failed for '${authorizationServerUrl.href}'. ` +
`Cannot safely construct '/authorize' — the server URL has a non-root path. ` +
`Ensure the AS metadata endpoint is reachable at '${authorizationServerUrl.origin}/.well-known/oauth-authorization-server${authorizationServerUrl.pathname}' ` +
`or provide metadata explicitly.`
);
}
authorizationUrl = new URL('/authorize', authorizationServerUrl);
}

Expand Down Expand Up @@ -1420,7 +1431,20 @@ export async function executeTokenRequest(
fetchFn?: FetchLike;
}
): Promise<OAuthTokens> {
const tokenUrl = metadata?.token_endpoint ? new URL(metadata.token_endpoint) : new URL('/token', authorizationServerUrl);
const tokenUrl = metadata?.token_endpoint
? new URL(metadata.token_endpoint)
: (() => {
// Guard: same silent-path-loss problem as /authorize above
if (authorizationServerUrl.pathname !== '/') {
throw new Error(
`Token endpoint discovery failed for '${authorizationServerUrl.href}'. ` +
`Cannot safely construct '/token' — the server URL has a non-root path. ` +
`Ensure the AS metadata endpoint is reachable at '${authorizationServerUrl.origin}/.well-known/oauth-authorization-server${authorizationServerUrl.pathname}' ` +
`or provide metadata explicitly.`
);
}
return new URL('/token', authorizationServerUrl);
})();

const headers = new Headers({
'Content-Type': 'application/x-www-form-urlencoded',
Expand All @@ -1439,6 +1463,10 @@ export async function executeTokenRequest(
applyClientAuthentication(authMethod, clientInformation as OAuthClientInformation, headers, tokenRequestParams);
}

// Ensure Content-Type is always form-urlencoded for the token endpoint (OAuth 2.1 §3.2).
// Some addClientAuthentication implementations may have inadvertently set a different value.
headers.set('Content-Type', 'application/x-www-form-urlencoded');

const response = await (fetchFn ?? fetch)(tokenUrl, {
method: 'POST',
headers,
Expand Down Expand Up @@ -1667,6 +1695,15 @@ export async function registerClient(

registrationUrl = new URL(metadata.registration_endpoint);
} else {
// Guard: same silent-path-loss problem as /authorize above
if (authorizationServerUrl.pathname !== '/') {
throw new Error(
`Dynamic client registration failed for '${authorizationServerUrl.href}'. ` +
`Cannot safely construct '/register' — the server URL has a non-root path. ` +
`Ensure the AS metadata endpoint is reachable at '${authorizationServerUrl.origin}/.well-known/oauth-authorization-server${authorizationServerUrl.pathname}' ` +
`or provide metadata explicitly.`
);
}
registrationUrl = new URL('/register', authorizationServerUrl);
}

Expand Down
Loading