Skip to content

Commit af9ba79

Browse files
Make the Dynamic Auth Providers Resource Specific (#249931)
Pretty much throws away any efforts of reuse for these auth providers... but it should support endpoints that care about the `resource` query parameter. And lets us use the `resource_name` as something we can nicely show to the user.
1 parent 9b5c56c commit af9ba79

File tree

8 files changed

+99
-80
lines changed

8 files changed

+99
-80
lines changed

src/vs/base/common/oauth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export interface IAuthorizationProtectedResourceMetadata {
2020
*/
2121
resource: string;
2222

23+
/**
24+
* OPTIONAL. Human-readable name of the protected resource intended for display to the end user.
25+
*/
26+
resource_name?: string;
27+
2328
/**
2429
* OPTIONAL. JSON array containing a list of OAuth authorization server issuer identifiers.
2530
*/

src/vs/workbench/api/browser/mainThreadAuthentication.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,18 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
113113
this._register(authenticationService.registerAuthenticationProviderHostDelegate({
114114
// Prefer Node.js extension hosts when they're available. No CORS issues etc.
115115
priority: extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1,
116-
create: async (serverMetadata) => {
116+
create: async (serverMetadata, resource) => {
117117
const clientId = this.dynamicAuthProviderStorageService.getClientId(serverMetadata.issuer);
118118
let initialTokens: (IAuthorizationTokenResponse & { created_at: number })[] | undefined = undefined;
119119
if (clientId) {
120120
initialTokens = await this.dynamicAuthProviderStorageService.getSessionsForDynamicAuthProvider(serverMetadata.issuer, clientId);
121121
}
122-
return this._proxy.$registerDynamicAuthProvider(serverMetadata, clientId, initialTokens);
122+
return await this._proxy.$registerDynamicAuthProvider(
123+
serverMetadata,
124+
resource,
125+
clientId,
126+
initialTokens
127+
);
123128
}
124129
}));
125130
}

src/vs/workbench/api/browser/mainThreadMcp.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions
2424
import { Proxied } from '../../services/extensions/common/proxyIdentifier.js';
2525
import { ExtHostContext, ExtHostMcpShape, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js';
2626
import { CancellationError } from '../../../base/common/errors.js';
27-
import { IAuthorizationServerMetadata } from '../../../base/common/oauth.js';
27+
import { IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata } from '../../../base/common/oauth.js';
2828

2929
@extHostNamedCustomer(MainContext.MainThreadMcp)
3030
export class MainThreadMcp extends Disposable implements MainThreadMcpShape {
@@ -138,7 +138,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape {
138138
this._servers.get(id)?.pushMessage(message);
139139
}
140140

141-
async $getTokenFromServerMetadata(id: number, metadata: IAuthorizationServerMetadata): Promise<string | undefined> {
141+
async $getTokenFromServerMetadata(id: number, metadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<string | undefined> {
142142
const server = this._serverDefinitions.get(id);
143143
if (!server) {
144144
return undefined;
@@ -149,7 +149,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape {
149149
const scopesSupported = metadata.scopes_supported || [];
150150
let providerId = await this._authenticationService.getOrActivateProviderIdForIssuer(issuer);
151151
if (!providerId) {
152-
const provider = await this._authenticationService.createDynamicAuthenticationProvider(metadata);
152+
const provider = await this._authenticationService.createDynamicAuthenticationProvider(metadata, resource);
153153
if (!provider) {
154154
return undefined;
155155
}
@@ -235,8 +235,8 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape {
235235

236236
private async loginPrompt(mcpLabel: string, providerLabel: string, recreatingSession: boolean): Promise<boolean> {
237237
const message = recreatingSession
238-
? nls.localize('confirmRelogin', "The MCP Server '{0}' wants you to sign in again using {1}.", mcpLabel, providerLabel)
239-
: nls.localize('confirmLogin', "The MCP Server '{0}' wants to sign in using {1}.", mcpLabel, providerLabel);
238+
? nls.localize('confirmRelogin', "The MCP Server Definition '{0}' wants you to authenticate to {1}.", mcpLabel, providerLabel)
239+
: nls.localize('confirmLogin', "The MCP Server Definition '{0}' wants to authenticate to {1}.", mcpLabel, providerLabel);
240240

241241
const buttons: IPromptButton<boolean | undefined>[] = [
242242
{

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { IRelativePattern } from '../../../base/common/glob.js';
1111
import { IMarkdownString } from '../../../base/common/htmlContent.js';
1212
import { IJSONSchema } from '../../../base/common/jsonSchema.js';
1313
import { IDisposable } from '../../../base/common/lifecycle.js';
14-
import { IAuthorizationServerMetadata, IAuthorizationTokenResponse } from '../../../base/common/oauth.js';
14+
import { IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, IAuthorizationTokenResponse } from '../../../base/common/oauth.js';
1515
import * as performance from '../../../base/common/performance.js';
1616
import Severity from '../../../base/common/severity.js';
1717
import { ThemeColor, ThemeIcon } from '../../../base/common/themables.js';
@@ -1988,7 +1988,7 @@ export interface ExtHostAuthenticationShape {
19881988
$removeSession(id: string, sessionId: string): Promise<void>;
19891989
$onDidChangeAuthenticationSessions(id: string, label: string, extensionIdFilter?: string[]): Promise<void>;
19901990
$onDidUnregisterAuthenticationProvider(id: string): Promise<void>;
1991-
$registerDynamicAuthProvider(serverMetadata: IAuthorizationServerMetadata, clientId?: string, initialTokens?: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void>;
1991+
$registerDynamicAuthProvider(serverMetadata: IAuthorizationServerMetadata, resource?: IAuthorizationProtectedResourceMetadata, clientId?: string, initialTokens?: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<string>;
19921992
$onDidChangeDynamicAuthProviderTokens(authProviderId: string, clientId: string, tokens?: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void>;
19931993
}
19941994

@@ -3017,7 +3017,7 @@ export interface MainThreadMcpShape {
30173017
$onDidReceiveMessage(id: number, message: string): void;
30183018
$upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, servers: McpServerDefinition.Serialized[]): void;
30193019
$deleteMcpCollection(collectionId: string): void;
3020-
$getTokenFromServerMetadata(id: number, metadata: IAuthorizationServerMetadata): Promise<string | undefined>;
3020+
$getTokenFromServerMetadata(id: number, metadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<string | undefined>;
30213021
}
30223022

30233023
export interface ExtHostLocalizationShape {

src/vs/workbench/api/common/extHostAuthentication.ts

Lines changed: 51 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { INTERNAL_AUTH_PROVIDER_PREFIX } from '../../services/authentication/com
1212
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
1313
import { IExtHostRpcService } from './extHostRpcService.js';
1414
import { URI } from '../../../base/common/uri.js';
15-
import { fetchDynamicRegistration, getClaimsFromJWT, IAuthorizationJWTClaims, IAuthorizationServerMetadata, IAuthorizationTokenResponse, isAuthorizationTokenResponse } from '../../../base/common/oauth.js';
15+
import { fetchDynamicRegistration, getClaimsFromJWT, IAuthorizationJWTClaims, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, IAuthorizationTokenResponse, isAuthorizationTokenResponse } from '../../../base/common/oauth.js';
1616
import { IExtHostWindow } from './extHostWindow.js';
1717
import { IExtHostInitDataService } from './extHostInitDataService.js';
1818
import { ILogger, ILoggerService } from '../../../platform/log/common/log.js';
@@ -161,29 +161,47 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
161161
return Promise.resolve();
162162
}
163163

164-
async $registerDynamicAuthProvider(serverMetadata: IAuthorizationServerMetadata, clientId?: string, initialTokens?: IAuthorizationToken[]): Promise<void> {
165-
const issuerUri = URI.parse(serverMetadata.issuer);
166-
const provider = await DynamicAuthProvider.create(
164+
async $registerDynamicAuthProvider(
165+
serverMetadata: IAuthorizationServerMetadata,
166+
resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined,
167+
clientId: string | undefined,
168+
initialTokens: IAuthorizationToken[] | undefined
169+
): Promise<string> {
170+
if (!clientId) {
171+
if (!serverMetadata.registration_endpoint) {
172+
throw new Error('Server does not support dynamic registration');
173+
}
174+
try {
175+
const registration = await fetchDynamicRegistration(serverMetadata.registration_endpoint, this._initData.environment.appName);
176+
clientId = registration.client_id;
177+
} catch (err) {
178+
throw new Error(`Dynamic registration failed: ${err.message}`);
179+
}
180+
}
181+
const provider = new DynamicAuthProvider(
167182
this._extHostWindow,
168183
this._extHostUrls,
169184
this._initData,
185+
this._extHostLoggerService,
170186
this._proxy,
171-
this._extHostLoggerService.createLogger(serverMetadata.issuer, { name: issuerUri.authority }),
172187
serverMetadata,
188+
resourceMetadata,
189+
clientId,
173190
this._onDidDynamicAuthProviderTokensChange,
174-
{ clientId, initialTokens }
191+
initialTokens || []
175192
);
176-
const disposable = provider.onDidChangeSessions(e => this._proxy.$sendDidChangeSessions(serverMetadata.issuer, e));
193+
const disposable = provider.onDidChangeSessions(e => this._proxy.$sendDidChangeSessions(provider.id, e));
177194
this._authenticationProviders.set(
178-
serverMetadata.issuer,
195+
provider.id,
179196
{
180-
label: issuerUri.authority,
197+
label: provider.label,
181198
provider,
182199
disposable: Disposable.from(provider, disposable),
183200
options: { supportsMultipleAccounts: false }
184201
}
185202
);
186-
await this._proxy.$registerDynamicAuthenticationProvider(serverMetadata.issuer, issuerUri.authority, issuerUri, provider.clientId);
203+
await this._proxy.$registerDynamicAuthenticationProvider(provider.id, provider.label, provider.issuer, provider.clientId);
204+
return provider.id;
187205
}
188206

189207
async $onDidChangeDynamicAuthProviderTokens(authProviderId: string, clientId: string, tokens: IAuthorizationToken[]): Promise<void> {
@@ -207,28 +225,45 @@ class TaskSingler<T> {
207225
}
208226

209227
export class DynamicAuthProvider implements vscode.AuthenticationProvider {
228+
readonly id: string;
229+
readonly label: string;
230+
readonly issuer: URI;
231+
210232
private _onDidChangeSessions = new Emitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
211233
readonly onDidChangeSessions = this._onDidChangeSessions.event;
212234

213235
private readonly _tokenStore: TokenStore;
214236

215237
private readonly _createFlows: Array<(scopes: string[]) => Promise<IAuthorizationTokenResponse>>;
216238

239+
private readonly _logger: ILogger;
217240
private readonly _disposable: DisposableStore;
218241

219242
constructor(
220243
@IExtHostWindow private readonly _extHostWindow: IExtHostWindow,
221244
@IExtHostUrlsService private readonly _extHostUrls: IExtHostUrlsService,
222245
@IExtHostInitDataService private readonly _initData: IExtHostInitDataService,
246+
@ILoggerService loggerService: ILoggerService,
223247
private readonly _proxy: MainThreadAuthenticationShape,
224-
private readonly _logger: ILogger,
225248
private readonly _serverMetadata: IAuthorizationServerMetadata,
249+
private readonly _resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined,
226250
readonly clientId: string,
227-
scopedEvent: Event<IAuthorizationToken[]>,
251+
onDidDynamicAuthProviderTokensChange: Emitter<{ authProviderId: string; clientId: string; tokens: IAuthorizationToken[] }>,
228252
initialTokens: IAuthorizationToken[],
229253
) {
254+
this.issuer = URI.parse(_serverMetadata.issuer);
255+
this.id = _resourceMetadata?.resource
256+
? _serverMetadata.issuer + ' ' + _resourceMetadata?.resource
257+
: _serverMetadata.issuer;
258+
this.label = _resourceMetadata?.resource_name ?? this.issuer.authority;
259+
260+
this._logger = loggerService.createLogger(_serverMetadata.issuer, { name: this.label });
230261
this._disposable = new DisposableStore();
231262
this._disposable.add(this._onDidChangeSessions);
263+
const scopedEvent = Event.chain(onDidDynamicAuthProviderTokensChange.event, $ => $
264+
.filter(e => e.authProviderId === this.id && e.clientId === clientId)
265+
.map(e => e.tokens)
266+
);
232267
this._tokenStore = this._disposable.add(new TokenStore(
233268
{
234269
onDidChange: scopedEvent,
@@ -242,46 +277,6 @@ export class DynamicAuthProvider implements vscode.AuthenticationProvider {
242277
this._createFlows = [scopes => this._createWithUrlHandler(scopes)];
243278
}
244279

245-
static async create(
246-
@IExtHostWindow extHostWindow: IExtHostWindow,
247-
@IExtHostUrlsService extHostUrls: IExtHostUrlsService,
248-
@IExtHostInitDataService initData: IExtHostInitDataService,
249-
proxy: MainThreadAuthenticationShape,
250-
logger: ILogger,
251-
serverMetadata: IAuthorizationServerMetadata,
252-
onDidDynamicAuthProviderTokensChange: Emitter<{ authProviderId: string; clientId: string; tokens: IAuthorizationToken[] }>,
253-
existingState: { clientId?: string; initialTokens?: IAuthorizationToken[] } = {},
254-
): Promise<DynamicAuthProvider> {
255-
let { clientId, initialTokens } = existingState;
256-
try {
257-
if (!clientId) {
258-
if (!serverMetadata.registration_endpoint) {
259-
throw new Error('Server does not support dynamic registration');
260-
}
261-
const registration = await fetchDynamicRegistration(serverMetadata.registration_endpoint, initData.environment.appName);
262-
clientId = registration.client_id;
263-
}
264-
const scopedEvent = Event.chain(onDidDynamicAuthProviderTokensChange.event, $ => $
265-
.filter(e => e.authProviderId === serverMetadata.issuer && e.clientId === clientId)
266-
.map(e => e.tokens)
267-
);
268-
const provider = new DynamicAuthProvider(
269-
extHostWindow,
270-
extHostUrls,
271-
initData,
272-
proxy,
273-
logger,
274-
serverMetadata,
275-
clientId,
276-
scopedEvent,
277-
initialTokens || []
278-
);
279-
return provider;
280-
} catch (err) {
281-
throw new Error(`Dynamic registration failed: ${err.message}`);
282-
}
283-
}
284-
285280
async getSessions(scopes: readonly string[] | undefined, _options: vscode.AuthenticationProviderSessionOptions): Promise<vscode.AuthenticationSession[]> {
286281
this._logger.info(`Getting sessions for scopes: ${scopes?.join(' ') ?? 'all'}`);
287282
if (!scopes) {
@@ -399,6 +394,10 @@ export class DynamicAuthProvider implements vscode.AuthenticationProvider {
399394
authorizationUrl.searchParams.append('state', state.toString());
400395
authorizationUrl.searchParams.append('code_challenge', codeChallenge);
401396
authorizationUrl.searchParams.append('code_challenge_method', 'S256');
397+
if (this._resourceMetadata?.resource) {
398+
// If a resource is specified, include it in the request
399+
authorizationUrl.searchParams.append('resource', this._resourceMetadata.resource);
400+
}
402401

403402
// Use a redirect URI that matches what was registered during dynamic registration
404403
const redirectUri = 'https://vscode.dev/redirect';

src/vs/workbench/api/common/extHostMcp.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ class McpHTTPHandle extends Disposable {
184184
private _mode: HttpModeT = { value: HttpMode.Unknown };
185185
private readonly _cts = new CancellationTokenSource();
186186
private readonly _abortCtrl = new AbortController();
187-
private _authMetadata?: IAuthorizationServerMetadata;
187+
private _authMetadata?: {
188+
server: IAuthorizationServerMetadata;
189+
resource?: IAuthorizationProtectedResourceMetadata;
190+
};
188191

189192
constructor(
190193
private readonly _id: number,
@@ -316,12 +319,14 @@ class McpHTTPHandle extends Disposable {
316319
// Second, fetch that url's well-known server metadata
317320
let serverMetadataUrl: string | undefined;
318321
let scopesSupported: string[] | undefined;
322+
let resource: IAuthorizationProtectedResourceMetadata | undefined;
319323
if (resourceMetadataChallenge) {
320324
const resourceMetadata = await this._getResourceMetadata(resourceMetadataChallenge);
321325
// TODO:@TylerLeonhardt support multiple authorization servers
322326
// Consider using one that has an auth provider first, over the dynamic flow
323327
serverMetadataUrl = resourceMetadata.authorization_servers?.[0];
324328
scopesSupported = resourceMetadata.scopes_supported;
329+
resource = resourceMetadata;
325330
}
326331

327332
const baseUrl = new URL(originalResponse.url).origin;
@@ -340,14 +345,17 @@ class McpHTTPHandle extends Disposable {
340345
const serverMetadataResponse = await this._getAuthorizationServerMetadata(serverMetadataUrl, addtionalHeaders);
341346
const serverMetadataWithDefaults = getMetadataWithDefaultValues(serverMetadataResponse);
342347
this._authMetadata = {
343-
...serverMetadataWithDefaults,
344-
// HACK: For now, just use the serverMetadataUrl as the issuer. I found an example, Entra,
345-
// that uses a placeholder for the tenant... https://login.microsoftonline.com/{tenant}/v2.0
346-
// literally... it contains `{tenant}`... instead of `organizations`. This may change our
347-
// API a bit to instead pass in these other endpoints, but for now, just user the serverMetadataUrl
348-
// as the isser.
349-
issuer: serverMetadataUrl,
350-
scopes_supported: scopesSupported ?? serverMetadataWithDefaults.scopes_supported
348+
server: {
349+
...serverMetadataWithDefaults,
350+
// HACK: For now, just use the serverMetadataUrl as the issuer. I found an example, Entra,
351+
// that uses a placeholder for the tenant... https://login.microsoftonline.com/{tenant}/v2.0
352+
// literally... it contains `{tenant}`... instead of `organizations`. This may change our
353+
// API a bit to instead pass in these other endpoints, but for now, just user the serverMetadataUrl
354+
// as the isser.
355+
issuer: serverMetadataUrl,
356+
scopes_supported: scopesSupported ?? serverMetadataWithDefaults.scopes_supported
357+
},
358+
resource
351359
};
352360
return;
353361
} catch (e) {
@@ -357,7 +365,10 @@ class McpHTTPHandle extends Disposable {
357365
// If there's no well-known server metadata, then use the default values based off of the url.
358366
const defaultMetadata = getDefaultMetadataForUrl(new URL(baseUrl));
359367
defaultMetadata.scopes_supported = scopesSupported ?? defaultMetadata.scopes_supported ?? [];
360-
this._authMetadata = defaultMetadata;
368+
this._authMetadata = {
369+
server: defaultMetadata,
370+
resource
371+
};
361372
}
362373

363374
private async _getResourceMetadata(resourceMetadata: string): Promise<IAuthorizationProtectedResourceMetadata> {
@@ -628,7 +639,7 @@ class McpHTTPHandle extends Disposable {
628639
private async _addAuthHeader(headers: Record<string, string>) {
629640
if (this._authMetadata) {
630641
try {
631-
const token = await this._proxy.$getTokenFromServerMetadata(this._id, this._authMetadata);
642+
const token = await this._proxy.$getTokenFromServerMetadata(this._id, this._authMetadata.server, this._authMetadata.resource);
632643
if (token) {
633644
headers['Authorization'] = `Bearer ${token}`;
634645
}

src/vs/workbench/services/authentication/browser/authenticationService.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { IJSONSchema } from '../../../../base/common/jsonSchema.js';
2020
import { ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js';
2121
import { match } from '../../../../base/common/glob.js';
2222
import { URI } from '../../../../base/common/uri.js';
23-
import { IAuthorizationServerMetadata } from '../../../../base/common/oauth.js';
23+
import { IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata } from '../../../../base/common/oauth.js';
2424

2525
export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; }
2626

@@ -316,14 +316,13 @@ export class AuthenticationService extends Disposable implements IAuthentication
316316
return undefined;
317317
}
318318

319-
async createDynamicAuthenticationProvider(serverMetadata: IAuthorizationServerMetadata): Promise<IAuthenticationProvider | undefined> {
319+
async createDynamicAuthenticationProvider(serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<IAuthenticationProvider | undefined> {
320320
const delegate = this._delegates[0];
321321
if (!delegate) {
322322
this._logService.error('No authentication provider host delegate found');
323323
return undefined;
324324
}
325-
await delegate.create(serverMetadata);
326-
const providerId = serverMetadata.issuer;
325+
const providerId = await delegate.create(serverMetadata, resource);
327326
const provider = this._authenticationProviders.get(providerId);
328327
if (provider) {
329328
this._logService.debug(`Created dynamic authentication provider: ${providerId}`);

0 commit comments

Comments
 (0)