inspector/core/auth/ema/transportProvider.ts
Bob Dickinson 5de4e911e3
feat(web,core): enterprise-managed authorization (EMA) (#1509) (#1512)
Adds enterprise-managed authorization (EMA): browser-side IdP login + RFC 8693 token-exchange and JWT-bearer resource-token mint, per-server EMA opt-in in Server Settings, IdP config in Client Settings, and IdP clientSecret keychain storage. Includes the v2/main merge and review follow-ups (400 on malformed client config; normalizeIdpIssuer import consolidation).
2026-06-24 22:15:12 -04:00

103 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
import type {
OAuthClientInformationMixed,
OAuthClientMetadata,
OAuthTokens,
} from "@modelcontextprotocol/sdk/shared/auth.js";
import type { BaseOAuthClientProvider } from "../providers.js";
import {
refreshEmaResourceTokens,
startEmaIdpAuthorization,
type EmaFlowConfig,
} from "./emaFlow.js";
import { isJwtExpired } from "./jwt.js";
function isAccessTokenUsable(
tokens: OAuthTokens,
skewMs = 60_000,
): boolean {
if (!tokens.access_token) return false;
return !isJwtExpired(tokens.access_token, skewMs);
}
/**
* OAuth provider for MCP transport when enterprise-managed authorization is enabled.
*
* Proactively refreshes resource tokens (EMA legs 2–3) via {@link tokens} before
* requests. On 401 when the IdP session is still valid, the SDK {@link auth}
* call may still reach {@link redirectToAuthorization}; we ignore the resource-AS
* URL and redirect to the IdP instead.
*/
export class EmaTransportOAuthProvider implements OAuthClientProvider {
private readonly inner: BaseOAuthClientProvider;
private readonly emaConfig: EmaFlowConfig;
constructor(
inner: BaseOAuthClientProvider,
emaConfig: EmaFlowConfig,
) {
this.inner = inner;
this.emaConfig = emaConfig;
}
get redirectUrl(): string {
return this.inner.redirectUrl;
}
get clientMetadataUrl(): string | undefined {
return this.inner.clientMetadataUrl;
}
get clientMetadata(): OAuthClientMetadata {
return this.inner.clientMetadata;
}
state(): string | Promise<string> {
return this.inner.state();
}
clientInformation():
| OAuthClientInformationMixed
| undefined
| Promise<OAuthClientInformationMixed | undefined> {
return this.inner.clientInformation();
}
saveClientInformation(
clientInformation: OAuthClientInformationMixed,
): void | Promise<void> {
return this.inner.saveClientInformation(clientInformation);
}
async tokens(): Promise<OAuthTokens | undefined> {
const stored = await this.inner.tokens();
if (stored && isAccessTokenUsable(stored)) {
return stored;
}
const refreshed = await refreshEmaResourceTokens(this.emaConfig);
if (refreshed) {
return refreshed;
}
return undefined;
}
saveTokens(tokens: OAuthTokens): void | Promise<void> {
return this.emaConfig.storage.saveTokens(this.emaConfig.serverUrl, tokens, {
enterpriseManaged: true,
});
}
async redirectToAuthorization(_authorizationUrl: URL): Promise<void> {
const idpAuthorizationUrl = await startEmaIdpAuthorization(this.emaConfig);
this.inner.clearCapturedAuthUrl();
this.inner.redirectToAuthorization(idpAuthorizationUrl);
}
saveCodeVerifier(codeVerifier: string): void | Promise<void> {
return this.inner.saveCodeVerifier(codeVerifier);
}
codeVerifier(): string | Promise<string> {
return this.inner.codeVerifier();
}
}