-
Notifications
You must be signed in to change notification settings - Fork 38.5k
Enable WWW-Authenticate Step-up #269884
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
Enable WWW-Authenticate Step-up #269884
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -178,14 +178,13 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { | |
| this._servers.get(id)?.pushMessage(message); | ||
| } | ||
|
|
||
| async $getTokenFromServerMetadata(id: number, authServerComponents: UriComponents, serverMetadata: IAuthorizationServerMetadata, resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined, errorOnUserInteraction?: boolean): Promise<string | undefined> { | ||
| async $getTokenFromServerMetadata(id: number, authServerComponents: UriComponents, serverMetadata: IAuthorizationServerMetadata, resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined, scopes: string[] | undefined, errorOnUserInteraction?: boolean): Promise<string | undefined> { | ||
| const server = this._serverDefinitions.get(id); | ||
| if (!server) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const authorizationServer = URI.revive(authServerComponents); | ||
| const scopesSupported = resourceMetadata?.scopes_supported || serverMetadata.scopes_supported || []; | ||
| const scopesSupported = scopes ?? resourceMetadata?.scopes_supported ?? serverMetadata.scopes_supported ?? []; | ||
|
||
| let providerId = await this._authenticationService.getOrActivateProviderIdForServer(authorizationServer); | ||
| if (!providerId) { | ||
| const provider = await this._authenticationService.createDynamicAuthenticationProvider(authorizationServer, serverMetadata, resourceMetadata); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,7 +8,7 @@ import { DeferredPromise, raceCancellationError, Sequencer, timeout } from '../. | |||||||||||||||
| import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; | ||||||||||||||||
| import { CancellationError } from '../../../base/common/errors.js'; | ||||||||||||||||
| import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; | ||||||||||||||||
| import { AUTH_SERVER_METADATA_DISCOVERY_PATH, fetchResourceMetadata, getDefaultMetadataForUrl, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, isAuthorizationServerMetadata, OPENID_CONNECT_DISCOVERY_PATH, parseWWWAuthenticateHeader } from '../../../base/common/oauth.js'; | ||||||||||||||||
| import { AUTH_SCOPE_SEPARATOR, AUTH_SERVER_METADATA_DISCOVERY_PATH, fetchResourceMetadata, getDefaultMetadataForUrl, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, isAuthorizationServerMetadata, OPENID_CONNECT_DISCOVERY_PATH, parseWWWAuthenticateHeader, scopesMatch } from '../../../base/common/oauth.js'; | ||||||||||||||||
| import { SSEParser } from '../../../base/common/sseParser.js'; | ||||||||||||||||
| import { URI, UriComponents } from '../../../base/common/uri.js'; | ||||||||||||||||
| import { ConfigurationTarget } from '../../../platform/configuration/common/configuration.js'; | ||||||||||||||||
|
|
@@ -215,6 +215,7 @@ export class McpHTTPHandle extends Disposable { | |||||||||||||||
| authorizationServer: URI; | ||||||||||||||||
| serverMetadata: IAuthorizationServerMetadata; | ||||||||||||||||
| resourceMetadata?: IAuthorizationProtectedResourceMetadata; | ||||||||||||||||
| scopes?: string[]; | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| constructor( | ||||||||||||||||
|
|
@@ -337,22 +338,11 @@ export class McpHTTPHandle extends Disposable { | |||||||||||||||
| private async _populateAuthMetadata(mcpUrl: string, originalResponse: CommonResponse): Promise<void> { | ||||||||||||||||
| // If there is a resource_metadata challenge, use that to get the oauth server. This is done in 2 steps. | ||||||||||||||||
| // First, extract the resource_metada challenge from the WWW-Authenticate header (if available) | ||||||||||||||||
| let resourceMetadataChallenge: string | undefined; | ||||||||||||||||
| if (originalResponse.headers.has('WWW-Authenticate')) { | ||||||||||||||||
| const authHeader = originalResponse.headers.get('WWW-Authenticate')!; | ||||||||||||||||
| const challenges = parseWWWAuthenticateHeader(authHeader); | ||||||||||||||||
| for (const challenge of challenges) { | ||||||||||||||||
| if (challenge.scheme === 'Bearer' && challenge.params['resource_metadata']) { | ||||||||||||||||
| this._log(LogLevel.Debug, `Found resource_metadata challenge in WWW-Authenticate header: ${challenge.params['resource_metadata']}`); | ||||||||||||||||
| resourceMetadataChallenge = challenge.params['resource_metadata']; | ||||||||||||||||
| break; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| const { resourceMetadataChallenge, scopesChallenge: scopesChallengeFromHeader } = this._parseWWWAuthenticateHeader(originalResponse); | ||||||||||||||||
| // Second, fetch the resource metadata either from the challenge URL or from well-known URIs | ||||||||||||||||
| let serverMetadataUrl: string | undefined; | ||||||||||||||||
| let scopesSupported: string[] | undefined; | ||||||||||||||||
| let resource: IAuthorizationProtectedResourceMetadata | undefined; | ||||||||||||||||
| let scopesChallenge = scopesChallengeFromHeader; | ||||||||||||||||
| try { | ||||||||||||||||
| const resourceMetadata = await fetchResourceMetadata(mcpUrl, resourceMetadataChallenge, { | ||||||||||||||||
| sameOriginHeaders: { | ||||||||||||||||
|
|
@@ -365,7 +355,7 @@ export class McpHTTPHandle extends Disposable { | |||||||||||||||
| // Consider using one that has an auth provider first, over the dynamic flow | ||||||||||||||||
| serverMetadataUrl = resourceMetadata.authorization_servers?.[0]; | ||||||||||||||||
| this._log(LogLevel.Debug, `Using auth server metadata url: ${serverMetadataUrl}`); | ||||||||||||||||
| scopesSupported = resourceMetadata.scopes_supported; | ||||||||||||||||
| scopesChallenge ??= resourceMetadata.scopes_supported; | ||||||||||||||||
TylerLeonhardt marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
| resource = resourceMetadata; | ||||||||||||||||
| } catch (e) { | ||||||||||||||||
| this._log(LogLevel.Debug, `Could not fetch resource metadata: ${String(e)}`); | ||||||||||||||||
|
|
@@ -389,7 +379,8 @@ export class McpHTTPHandle extends Disposable { | |||||||||||||||
| this._authMetadata = { | ||||||||||||||||
| authorizationServer: URI.parse(serverMetadataUrl), | ||||||||||||||||
| serverMetadata: serverMetadataResponse, | ||||||||||||||||
| resourceMetadata: resource | ||||||||||||||||
| resourceMetadata: resource, | ||||||||||||||||
| scopes: scopesChallenge | ||||||||||||||||
| }; | ||||||||||||||||
| return; | ||||||||||||||||
| } catch (e) { | ||||||||||||||||
|
|
@@ -398,11 +389,11 @@ export class McpHTTPHandle extends Disposable { | |||||||||||||||
|
|
||||||||||||||||
| // If there's no well-known server metadata, then use the default values based off of the url. | ||||||||||||||||
| const defaultMetadata = getDefaultMetadataForUrl(new URL(baseUrl)); | ||||||||||||||||
| defaultMetadata.scopes_supported = scopesSupported ?? defaultMetadata.scopes_supported ?? []; | ||||||||||||||||
| this._authMetadata = { | ||||||||||||||||
| authorizationServer: URI.parse(baseUrl), | ||||||||||||||||
| serverMetadata: defaultMetadata, | ||||||||||||||||
| resourceMetadata: resource | ||||||||||||||||
| resourceMetadata: resource, | ||||||||||||||||
| scopes: scopesChallenge | ||||||||||||||||
| }; | ||||||||||||||||
| this._log(LogLevel.Info, 'Using default auth metadata'); | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -663,7 +654,7 @@ export class McpHTTPHandle extends Disposable { | |||||||||||||||
| private async _addAuthHeader(headers: Record<string, string>) { | ||||||||||||||||
| if (this._authMetadata) { | ||||||||||||||||
| try { | ||||||||||||||||
| const token = await this._proxy.$getTokenFromServerMetadata(this._id, this._authMetadata.authorizationServer, this._authMetadata.serverMetadata, this._authMetadata.resourceMetadata, this._errorOnUserInteraction); | ||||||||||||||||
| const token = await this._proxy.$getTokenFromServerMetadata(this._id, this._authMetadata.authorizationServer, this._authMetadata.serverMetadata, this._authMetadata.resourceMetadata, this._authMetadata.scopes, this._errorOnUserInteraction); | ||||||||||||||||
| if (token) { | ||||||||||||||||
| headers['Authorization'] = `Bearer ${token}`; | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -684,6 +675,34 @@ export class McpHTTPHandle extends Disposable { | |||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| private _parseWWWAuthenticateHeader(response: CommonResponse): { resourceMetadataChallenge: string | undefined; scopesChallenge: string[] | undefined } { | ||||||||||||||||
| let resourceMetadataChallenge: string | undefined; | ||||||||||||||||
| let scopesChallenge: string[] | undefined; | ||||||||||||||||
| if (response.headers.has('WWW-Authenticate')) { | ||||||||||||||||
| const authHeader = response.headers.get('WWW-Authenticate')!; | ||||||||||||||||
| const challenges = parseWWWAuthenticateHeader(authHeader); | ||||||||||||||||
| for (const challenge of challenges) { | ||||||||||||||||
| if (challenge.scheme === 'Bearer') { | ||||||||||||||||
| if (!resourceMetadataChallenge && challenge.params['resource_metadata']) { | ||||||||||||||||
| resourceMetadataChallenge = challenge.params['resource_metadata']; | ||||||||||||||||
| this._log(LogLevel.Debug, `Found resource_metadata challenge in WWW-Authenticate header: ${resourceMetadataChallenge}`); | ||||||||||||||||
| } | ||||||||||||||||
| if (!scopesChallenge && challenge.params['scope']) { | ||||||||||||||||
| const scopes = challenge.params['scope'].split(AUTH_SCOPE_SEPARATOR); | ||||||||||||||||
| if (scopes.length) { | ||||||||||||||||
| this._log(LogLevel.Debug, `Found scope challenge in WWW-Authenticate header: ${challenge.params['scope']}`); | ||||||||||||||||
| scopesChallenge = scopes; | ||||||||||||||||
|
Comment on lines
+692
to
+694
|
||||||||||||||||
| if (scopes.length) { | |
| this._log(LogLevel.Debug, `Found scope challenge in WWW-Authenticate header: ${challenge.params['scope']}`); | |
| scopesChallenge = scopes; | |
| const filteredScopes = scopes.filter(s => s.trim()); | |
| if (filteredScopes.length) { | |
| this._log(LogLevel.Debug, `Found scope challenge in WWW-Authenticate header: ${challenge.params['scope']}`); | |
| scopesChallenge = filteredScopes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: a URL for the RFC would be nice