Skip to content

Support Microsoft Azure as Auth Server #863

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
46 changes: 43 additions & 3 deletions src/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,30 @@ async function discoverMetadataWithFallback(
return response;
}

/**
* Using metadata, identifies common issuers that need special handling.
* Only for large, unusual issuers, fully spec compliant issuers should not be identified, small issuers should not be given special treatment.
* e.g. Azure no PKCE advertised, scope param instead of resource param.
*/
function identifyQuirkyIssuer(metadata: AuthorizationServerMetadata): "azure_v2" | undefined {
const issuerString = metadata.issuer;
let issuerUrl: URL;
// Parse issuer URL and treat failed parse as normal issuer.
try {
issuerUrl = new URL(issuerString);
} catch (e) {
if (e instanceof TypeError && e.message === "Invalid URL") {
return undefined;
}
throw e;
}
// Check for known issuer types needing conditional handling
if (issuerUrl.hostname === "login.microsoftonline.com" && issuerUrl.pathname.endsWith('/v2.0')) {
return "azure_v2";
}
return undefined;
}

/**
* Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
*
Expand Down Expand Up @@ -778,6 +802,10 @@ export async function discoverAuthorizationServerMetadata(
return OAuthMetadataSchema.parse(await response.json());
} else {
const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
// Azure Bypass
if (identifyQuirkyIssuer(metadata) === "azure_v2" && !metadata.code_challenge_methods_supported) {
metadata.code_challenge_methods_supported = ["S256"];
}

// MCP spec requires OIDC providers to support S256 PKCE
if (!metadata.code_challenge_methods_supported?.includes('S256')) {
Expand Down Expand Up @@ -869,7 +897,11 @@ export async function startAuthorization(
}

if (resource) {
authorizationUrl.searchParams.set("resource", resource.href);
if (metadata && identifyQuirkyIssuer(metadata) === "azure_v2") {
authorizationUrl.searchParams.set("scope", `${resource.href}/.default`);
} else {
authorizationUrl.searchParams.set("resource", resource.href);
}
}

return { authorizationUrl, codeVerifier };
Expand Down Expand Up @@ -947,7 +979,11 @@ export async function exchangeAuthorization(
}

if (resource) {
params.set("resource", resource.href);
if (metadata && identifyQuirkyIssuer(metadata) === "azure_v2") {
params.set("scope", `${resource.href}/.default`);
} else {
params.set("resource", resource.href);
}
}

const response = await (fetchFn ?? fetch)(tokenUrl, {
Expand Down Expand Up @@ -1031,7 +1067,11 @@ export async function refreshAuthorization(
}

if (resource) {
params.set("resource", resource.href);
if (metadata && identifyQuirkyIssuer(metadata) === "azure_v2") {
params.set("scope", `${resource.href}/.default`);
} else {
params.set("resource", resource.href);
}
}

const response = await (fetchFn ?? fetch)(tokenUrl, {
Expand Down