mirror of https://github.com/microsoft/vscode.git
`issuer` -> `authorizationServer` refactor (#250359)
* `issuer` -> `authorizationServer` refactor Also: * adds `authorizationServerGlobs` to the authentication contribution schema * removes ugly MCP issuer hack and instead plumbs the authorizationServer down to the new auth providers
This commit is contained in:
parent
a2ebac10a3
commit
c235626145
|
@ -35,14 +35,14 @@
|
|||
{
|
||||
"label": "GitHub",
|
||||
"id": "github",
|
||||
"issuerGlobs": [
|
||||
"authorizationServerGlobs": [
|
||||
"https://github.com/login/oauth"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "GitHub Enterprise Server",
|
||||
"id": "github-enterprise",
|
||||
"issuerGlobs": [
|
||||
"authorizationServerGlobs": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -143,7 +143,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
|
|||
this,
|
||||
{
|
||||
supportsMultipleAccounts: true,
|
||||
supportedIssuers: [
|
||||
supportedAuthorizationServers: [
|
||||
ghesUri ?? vscode.Uri.parse('https://github.com/login/oauth')
|
||||
]
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
{
|
||||
"label": "Microsoft",
|
||||
"id": "microsoft",
|
||||
"issuerGlobs": [
|
||||
"authorizationServerGlobs": [
|
||||
"https://login.microsoftonline.com/*/v2.0"
|
||||
]
|
||||
},
|
||||
|
|
|
@ -203,7 +203,7 @@ export class AzureActiveDirectoryService {
|
|||
return this._sessionChangeEmitter.event;
|
||||
}
|
||||
|
||||
public getSessions(scopes: string[] | undefined, { account, issuer }: vscode.AuthenticationProviderSessionOptions = {}): Promise<vscode.AuthenticationSession[]> {
|
||||
public getSessions(scopes: string[] | undefined, { account, authorizationServer }: vscode.AuthenticationProviderSessionOptions = {}): Promise<vscode.AuthenticationSession[]> {
|
||||
if (!scopes) {
|
||||
this._logger.info('Getting sessions for all scopes...');
|
||||
const sessions = this._tokens
|
||||
|
@ -226,8 +226,8 @@ export class AzureActiveDirectoryService {
|
|||
if (!modifiedScopes.includes('offline_access')) {
|
||||
modifiedScopes.push('offline_access');
|
||||
}
|
||||
if (issuer) {
|
||||
const tenant = issuer.path.split('/')[1];
|
||||
if (authorizationServer) {
|
||||
const tenant = authorizationServer.path.split('/')[1];
|
||||
if (tenant) {
|
||||
modifiedScopes.push(`VSCODE_TENANT:${tenant}`);
|
||||
}
|
||||
|
@ -303,7 +303,7 @@ export class AzureActiveDirectoryService {
|
|||
.map(result => (result as PromiseFulfilledResult<vscode.AuthenticationSession>).value);
|
||||
}
|
||||
|
||||
public createSession(scopes: string[], { account, issuer }: vscode.AuthenticationProviderSessionOptions = {}): Promise<vscode.AuthenticationSession> {
|
||||
public createSession(scopes: string[], { account, authorizationServer }: vscode.AuthenticationProviderSessionOptions = {}): Promise<vscode.AuthenticationSession> {
|
||||
let modifiedScopes = [...scopes];
|
||||
if (!modifiedScopes.includes('openid')) {
|
||||
modifiedScopes.push('openid');
|
||||
|
@ -317,8 +317,8 @@ export class AzureActiveDirectoryService {
|
|||
if (!modifiedScopes.includes('offline_access')) {
|
||||
modifiedScopes.push('offline_access');
|
||||
}
|
||||
if (issuer) {
|
||||
const tenant = issuer.path.split('/')[1];
|
||||
if (authorizationServer) {
|
||||
const tenant = authorizationServer.path.split('/')[1];
|
||||
if (tenant) {
|
||||
modifiedScopes.push(`VSCODE_TENANT:${tenant}`);
|
||||
}
|
||||
|
|
|
@ -45,14 +45,14 @@ export class ScopeData {
|
|||
*/
|
||||
readonly tenantId: string | undefined;
|
||||
|
||||
constructor(readonly originalScopes: readonly string[] = [], issuer?: Uri) {
|
||||
constructor(readonly originalScopes: readonly string[] = [], authorizationServer?: Uri) {
|
||||
const modifiedScopes = [...originalScopes];
|
||||
modifiedScopes.sort();
|
||||
this.allScopes = modifiedScopes;
|
||||
this.scopeStr = modifiedScopes.join(' ');
|
||||
this.scopesToSend = this.getScopesToSend(modifiedScopes);
|
||||
this.clientId = this.getClientId(this.allScopes);
|
||||
this.tenant = this.getTenant(this.allScopes, issuer);
|
||||
this.tenant = this.getTenant(this.allScopes, authorizationServer);
|
||||
this.tenantId = this.getTenantId(this.tenant);
|
||||
}
|
||||
|
||||
|
@ -65,10 +65,10 @@ export class ScopeData {
|
|||
}, undefined) ?? DEFAULT_CLIENT_ID;
|
||||
}
|
||||
|
||||
private getTenant(scopes: string[], issuer?: Uri): string {
|
||||
if (issuer?.path) {
|
||||
private getTenant(scopes: string[], authorizationServer?: Uri): string {
|
||||
if (authorizationServer?.path) {
|
||||
// Get tenant portion of URL
|
||||
const tenant = issuer.path.split('/')[1];
|
||||
const tenant = authorizationServer.path.split('/')[1];
|
||||
if (tenant) {
|
||||
return tenant;
|
||||
}
|
||||
|
|
|
@ -75,21 +75,21 @@ suite('ScopeData', () => {
|
|||
assert.strictEqual(scopeData.tenantId, 'some_guid');
|
||||
});
|
||||
|
||||
test('should extract tenant from issuer URL path', () => {
|
||||
const issuer = Uri.parse('https://login.microsoftonline.com/tenant123/oauth2/v2.0');
|
||||
const scopeData = new ScopeData(['custom_scope'], issuer);
|
||||
test('should extract tenant from authorization server URL path', () => {
|
||||
const authorizationServer = Uri.parse('https://login.microsoftonline.com/tenant123/oauth2/v2.0');
|
||||
const scopeData = new ScopeData(['custom_scope'], authorizationServer);
|
||||
assert.strictEqual(scopeData.tenant, 'tenant123');
|
||||
});
|
||||
|
||||
test('should fallback to default tenant if issuer URL has no path segments', () => {
|
||||
const issuer = Uri.parse('https://login.microsoftonline.com');
|
||||
const scopeData = new ScopeData(['custom_scope'], issuer);
|
||||
test('should fallback to default tenant if authorization server URL has no path segments', () => {
|
||||
const authorizationServer = Uri.parse('https://login.microsoftonline.com');
|
||||
const scopeData = new ScopeData(['custom_scope'], authorizationServer);
|
||||
assert.strictEqual(scopeData.tenant, 'organizations');
|
||||
});
|
||||
|
||||
test('should prioritize issuer URL over VSCODE_TENANT scope', () => {
|
||||
const issuer = Uri.parse('https://login.microsoftonline.com/url_tenant/oauth2/v2.0');
|
||||
const scopeData = new ScopeData(['custom_scope', 'VSCODE_TENANT:scope_tenant'], issuer);
|
||||
test('should prioritize authorization server URL over VSCODE_TENANT scope', () => {
|
||||
const authorizationServer = Uri.parse('https://login.microsoftonline.com/url_tenant/oauth2/v2.0');
|
||||
const scopeData = new ScopeData(['custom_scope', 'VSCODE_TENANT:scope_tenant'], authorizationServer);
|
||||
assert.strictEqual(scopeData.tenant, 'url_tenant');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -170,7 +170,7 @@ export async function activate(context: vscode.ExtensionContext, telemetryReport
|
|||
},
|
||||
{
|
||||
supportsMultipleAccounts: true,
|
||||
supportedIssuers: [
|
||||
supportedAuthorizationServers: [
|
||||
vscode.Uri.parse('https://login.microsoftonline.com/*/v2.0')
|
||||
]
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ export async function activate(context: ExtensionContext, mainTelemetryReporter:
|
|||
authProvider,
|
||||
{
|
||||
supportsMultipleAccounts: true,
|
||||
supportedIssuers: [
|
||||
supportedAuthorizationServers: [
|
||||
Uri.parse('https://login.microsoftonline.com/*/v2.0')
|
||||
]
|
||||
}
|
||||
|
|
|
@ -156,7 +156,7 @@ export class MsalAuthProvider implements AuthenticationProvider {
|
|||
|
||||
async getSessions(scopes: string[] | undefined, options: AuthenticationGetSessionOptions = {}): Promise<AuthenticationSession[]> {
|
||||
const askingForAll = scopes === undefined;
|
||||
const scopeData = new ScopeData(scopes, options?.issuer);
|
||||
const scopeData = new ScopeData(scopes, options?.authorizationServer);
|
||||
// Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead.
|
||||
this._logger.info('[getSessions]', askingForAll ? '[all]' : `[${scopeData.scopeStr}]`, 'starting');
|
||||
|
||||
|
@ -186,7 +186,7 @@ export class MsalAuthProvider implements AuthenticationProvider {
|
|||
}
|
||||
|
||||
async createSession(scopes: readonly string[], options: AuthenticationProviderSessionOptions): Promise<AuthenticationSession> {
|
||||
const scopeData = new ScopeData(scopes, options.issuer);
|
||||
const scopeData = new ScopeData(scopes, options.authorizationServer);
|
||||
// Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead.
|
||||
|
||||
this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'starting');
|
||||
|
|
|
@ -26,7 +26,7 @@ export interface IAuthorizationProtectedResourceMetadata {
|
|||
resource_name?: string;
|
||||
|
||||
/**
|
||||
* OPTIONAL. JSON array containing a list of OAuth authorization server issuer identifiers.
|
||||
* OPTIONAL. JSON array containing a list of OAuth authorization server identifiers.
|
||||
*/
|
||||
authorization_servers?: string[];
|
||||
|
||||
|
@ -624,12 +624,12 @@ export function isAuthorizationDeviceTokenErrorResponse(obj: unknown): obj is IA
|
|||
|
||||
//#endregion
|
||||
|
||||
export function getDefaultMetadataForUrl(issuer: URL): IRequiredAuthorizationServerMetadata & IRequiredAuthorizationServerMetadata {
|
||||
export function getDefaultMetadataForUrl(authorizationServer: URL): IRequiredAuthorizationServerMetadata & IRequiredAuthorizationServerMetadata {
|
||||
return {
|
||||
issuer: issuer.toString(),
|
||||
authorization_endpoint: new URL('/authorize', issuer).toString(),
|
||||
token_endpoint: new URL('/token', issuer).toString(),
|
||||
registration_endpoint: new URL('/register', issuer).toString(),
|
||||
issuer: authorizationServer.toString(),
|
||||
authorization_endpoint: new URL('/authorize', authorizationServer).toString(),
|
||||
token_endpoint: new URL('/token', authorizationServer).toString(),
|
||||
registration_endpoint: new URL('/register', authorizationServer).toString(),
|
||||
// Default values for Dynamic OpenID Providers
|
||||
// https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
response_types_supported: ['code', 'id_token', 'id_token token'],
|
||||
|
|
|
@ -197,8 +197,8 @@ suite('OAuth', () => {
|
|||
|
||||
suite('Utility Functions', () => {
|
||||
test('getDefaultMetadataForUrl should return correct default endpoints', () => {
|
||||
const issuer = new URL('https://auth.example.com');
|
||||
const metadata = getDefaultMetadataForUrl(issuer);
|
||||
const authorizationServer = new URL('https://auth.example.com');
|
||||
const metadata = getDefaultMetadataForUrl(authorizationServer);
|
||||
|
||||
assert.strictEqual(metadata.issuer, 'https://auth.example.com/');
|
||||
assert.strictEqual(metadata.authorization_endpoint, 'https://auth.example.com/authorize');
|
||||
|
|
|
@ -104,7 +104,7 @@ export interface ICodeActionContribution {
|
|||
export interface IAuthenticationContribution {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly issuerGlobs?: string[];
|
||||
readonly authorizationServerGlobs?: string[];
|
||||
}
|
||||
|
||||
export interface IWalkthroughStep {
|
||||
|
|
|
@ -40,7 +40,7 @@ export interface AuthenticationGetSessionOptions {
|
|||
forceNewSession?: boolean | AuthenticationInteractiveOptions;
|
||||
silent?: boolean;
|
||||
account?: AuthenticationSessionAccount;
|
||||
issuer?: UriComponents;
|
||||
authorizationServer?: UriComponents;
|
||||
}
|
||||
|
||||
export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider {
|
||||
|
@ -52,7 +52,7 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut
|
|||
public readonly id: string,
|
||||
public readonly label: string,
|
||||
public readonly supportsMultipleAccounts: boolean,
|
||||
public readonly issuers: ReadonlyArray<URI>,
|
||||
public readonly authorizationServers: ReadonlyArray<URI>,
|
||||
private readonly notificationService: INotificationService,
|
||||
onDidChangeSessionsEmitter: Emitter<AuthenticationSessionsChangeEvent>,
|
||||
) {
|
||||
|
@ -115,13 +115,15 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
this._register(authenticationService.registerAuthenticationProviderHostDelegate({
|
||||
// Prefer Node.js extension hosts when they're available. No CORS issues etc.
|
||||
priority: extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1,
|
||||
create: async (serverMetadata, resource) => {
|
||||
const clientId = this.dynamicAuthProviderStorageService.getClientId(serverMetadata.issuer);
|
||||
create: async (authorizationServer, serverMetadata, resource) => {
|
||||
const authProviderId = authorizationServer.toString(true);
|
||||
const clientId = this.dynamicAuthProviderStorageService.getClientId(authProviderId);
|
||||
let initialTokens: (IAuthorizationTokenResponse & { created_at: number })[] | undefined = undefined;
|
||||
if (clientId) {
|
||||
initialTokens = await this.dynamicAuthProviderStorageService.getSessionsForDynamicAuthProvider(serverMetadata.issuer, clientId);
|
||||
initialTokens = await this.dynamicAuthProviderStorageService.getSessionsForDynamicAuthProvider(authProviderId, clientId);
|
||||
}
|
||||
return await this._proxy.$registerDynamicAuthProvider(
|
||||
authorizationServer,
|
||||
serverMetadata,
|
||||
resource,
|
||||
clientId,
|
||||
|
@ -131,7 +133,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
}));
|
||||
}
|
||||
|
||||
async $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedIssuers: UriComponents[] = []): Promise<void> {
|
||||
async $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedAuthorizationServer: UriComponents[] = []): Promise<void> {
|
||||
if (!this.authenticationService.declaredProviders.find(p => p.id === id)) {
|
||||
// If telemetry shows that this is not happening much, we can instead throw an error here.
|
||||
this.logService.warn(`Authentication provider ${id} was not declared in the Extension Manifest.`);
|
||||
|
@ -144,8 +146,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
}
|
||||
const emitter = new Emitter<AuthenticationSessionsChangeEvent>();
|
||||
this._registrations.set(id, emitter);
|
||||
const supportedIssuerUris = supportedIssuers.map(i => URI.revive(i));
|
||||
const provider = new MainThreadAuthenticationProvider(this._proxy, id, label, supportsMultipleAccounts, supportedIssuerUris, this.notificationService, emitter);
|
||||
const supportedAuthorizationServerUris = supportedAuthorizationServer.map(i => URI.revive(i));
|
||||
const provider = new MainThreadAuthenticationProvider(this._proxy, id, label, supportsMultipleAccounts, supportedAuthorizationServerUris, this.notificationService, emitter);
|
||||
this.authenticationService.registerAuthenticationProvider(id, provider);
|
||||
}
|
||||
|
||||
|
@ -212,9 +214,9 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
return deferredPromise.p;
|
||||
}
|
||||
|
||||
async $registerDynamicAuthenticationProvider(id: string, label: string, issuer: UriComponents, clientId: string): Promise<void> {
|
||||
await this.$registerAuthenticationProvider(id, label, false, [issuer]);
|
||||
this.dynamicAuthProviderStorageService.storeClientId(id, clientId, label, URI.revive(issuer).toString());
|
||||
async $registerDynamicAuthenticationProvider(id: string, label: string, authorizationServer: UriComponents, clientId: string): Promise<void> {
|
||||
await this.$registerAuthenticationProvider(id, label, false, [authorizationServer]);
|
||||
this.dynamicAuthProviderStorageService.storeClientId(id, URI.revive(authorizationServer).toString(true), clientId, label);
|
||||
}
|
||||
|
||||
async $setSessionsForDynamicAuthProvider(authProviderId: string, clientId: string, sessions: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void> {
|
||||
|
@ -288,8 +290,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
}
|
||||
|
||||
private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise<AuthenticationSession | undefined> {
|
||||
const issuer = URI.revive(options.issuer);
|
||||
const sessions = await this.authenticationService.getSessions(providerId, scopes, { account: options.account, issuer }, true);
|
||||
const authorizationServer = URI.revive(options.authorizationServer);
|
||||
const sessions = await this.authenticationService.getSessions(providerId, scopes, { account: options.account, authorizationServer }, true);
|
||||
const provider = this.authenticationService.getProvider(providerId);
|
||||
|
||||
// Error cases
|
||||
|
@ -360,7 +362,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
{
|
||||
activateImmediate: true,
|
||||
account: accountToCreate,
|
||||
issuer
|
||||
authorizationServer
|
||||
});
|
||||
} while (
|
||||
accountToCreate
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Emitter } from '../../../base/common/event.js';
|
|||
import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
|
||||
import { ISettableObservable, observableValue } from '../../../base/common/observable.js';
|
||||
import Severity from '../../../base/common/severity.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { URI, UriComponents } from '../../../base/common/uri.js';
|
||||
import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js';
|
||||
import { LogLevel } from '../../../platform/log/common/log.js';
|
||||
import { IMcpMessageTransport, IMcpRegistry } from '../../contrib/mcp/common/mcpRegistryTypes.js';
|
||||
|
@ -138,24 +138,23 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape {
|
|||
this._servers.get(id)?.pushMessage(message);
|
||||
}
|
||||
|
||||
async $getTokenFromServerMetadata(id: number, metadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<string | undefined> {
|
||||
async $getTokenFromServerMetadata(id: number, authServerComponents: UriComponents, serverMetadata: IAuthorizationServerMetadata, resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined): Promise<string | undefined> {
|
||||
const server = this._serverDefinitions.get(id);
|
||||
if (!server) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const issuer = URI.parse(metadata.issuer);
|
||||
// Some better default?
|
||||
const scopesSupported = metadata.scopes_supported || [];
|
||||
let providerId = await this._authenticationService.getOrActivateProviderIdForIssuer(issuer);
|
||||
const authorizationServer = URI.revive(authServerComponents);
|
||||
const scopesSupported = resourceMetadata?.scopes_supported || serverMetadata.scopes_supported || [];
|
||||
let providerId = await this._authenticationService.getOrActivateProviderIdForServer(authorizationServer);
|
||||
if (!providerId) {
|
||||
const provider = await this._authenticationService.createDynamicAuthenticationProvider(metadata, resource);
|
||||
const provider = await this._authenticationService.createDynamicAuthenticationProvider(authorizationServer, serverMetadata, resourceMetadata);
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
providerId = provider.id;
|
||||
}
|
||||
const sessions = await this._authenticationService.getSessions(providerId, scopesSupported, { issuer }, true);
|
||||
const sessions = await this._authenticationService.getSessions(providerId, scopesSupported, { authorizationServer: authorizationServer }, true);
|
||||
const accountNamePreference = this.authenticationMcpServersService.getAccountPreference(server.id, providerId);
|
||||
let matchingAccountPreferenceSession: AuthenticationSession | undefined;
|
||||
if (accountNamePreference) {
|
||||
|
@ -193,7 +192,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape {
|
|||
{
|
||||
activateImmediate: true,
|
||||
account: accountToCreate,
|
||||
issuer
|
||||
authorizationServer
|
||||
});
|
||||
} while (
|
||||
accountToCreate
|
||||
|
|
|
@ -300,7 +300,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
) {
|
||||
checkProposedApiEnabled(extension, 'authLearnMore');
|
||||
}
|
||||
if (options?.issuer) {
|
||||
if (options?.authorizationServer) {
|
||||
checkProposedApiEnabled(extension, 'authIssuers');
|
||||
}
|
||||
return extHostAuthentication.getSession(extension, providerId, scopes, options as any);
|
||||
|
@ -317,7 +317,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
return _asExtensionEvent(extHostAuthentication.getExtensionScopedSessionsEvent(extension.identifier.value));
|
||||
},
|
||||
registerAuthenticationProvider(id: string, label: string, provider: vscode.AuthenticationProvider, options?: vscode.AuthenticationProviderOptions): vscode.Disposable {
|
||||
if (options?.supportedIssuers) {
|
||||
if (options?.supportedAuthorizationServers) {
|
||||
checkProposedApiEnabled(extension, 'authIssuers');
|
||||
}
|
||||
return extHostAuthentication.registerAuthenticationProvider(id, label, provider, options);
|
||||
|
|
|
@ -182,7 +182,7 @@ export interface AuthenticationGetSessionOptions {
|
|||
}
|
||||
|
||||
export interface MainThreadAuthenticationShape extends IDisposable {
|
||||
$registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedIssuers?: UriComponents[]): Promise<void>;
|
||||
$registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedAuthorizationServers?: UriComponents[]): Promise<void>;
|
||||
$unregisterAuthenticationProvider(id: string): Promise<void>;
|
||||
$ensureProvider(id: string): Promise<void>;
|
||||
$sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): Promise<void>;
|
||||
|
@ -192,7 +192,7 @@ export interface MainThreadAuthenticationShape extends IDisposable {
|
|||
$waitForUriHandler(expectedUri: UriComponents): Promise<UriComponents>;
|
||||
$showContinueNotification(message: string): Promise<boolean>;
|
||||
$showDeviceCodeModal(userCode: string, verificationUri: string): Promise<boolean>;
|
||||
$registerDynamicAuthenticationProvider(id: string, label: string, issuer: UriComponents, clientId: string): Promise<void>;
|
||||
$registerDynamicAuthenticationProvider(id: string, label: string, authorizationServer: UriComponents, clientId: string): Promise<void>;
|
||||
$setSessionsForDynamicAuthProvider(authProviderId: string, clientId: string, sessions: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -1990,7 +1990,7 @@ export interface ExtHostAuthenticationShape {
|
|||
$removeSession(id: string, sessionId: string): Promise<void>;
|
||||
$onDidChangeAuthenticationSessions(id: string, label: string, extensionIdFilter?: string[]): Promise<void>;
|
||||
$onDidUnregisterAuthenticationProvider(id: string): Promise<void>;
|
||||
$registerDynamicAuthProvider(serverMetadata: IAuthorizationServerMetadata, resource?: IAuthorizationProtectedResourceMetadata, clientId?: string, initialTokens?: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<string>;
|
||||
$registerDynamicAuthProvider(authorizationServer: UriComponents, serverMetadata: IAuthorizationServerMetadata, resource?: IAuthorizationProtectedResourceMetadata, clientId?: string, initialTokens?: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<string>;
|
||||
$onDidChangeDynamicAuthProviderTokens(authProviderId: string, clientId: string, tokens?: (IAuthorizationTokenResponse & { created_at: number })[]): Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -3019,7 +3019,7 @@ export interface MainThreadMcpShape {
|
|||
$onDidReceiveMessage(id: number, message: string): void;
|
||||
$upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, servers: McpServerDefinition.Serialized[]): void;
|
||||
$deleteMcpCollection(collectionId: string): void;
|
||||
$getTokenFromServerMetadata(id: number, metadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<string | undefined>;
|
||||
$getTokenFromServerMetadata(id: number, authorizationServer: UriComponents, serverMetadata: IAuthorizationServerMetadata, resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
export interface ExtHostLocalizationShape {
|
||||
|
|
|
@ -12,7 +12,7 @@ import { IExtensionDescription, ExtensionIdentifier } from '../../../platform/ex
|
|||
import { INTERNAL_AUTH_PROVIDER_PREFIX } from '../../services/authentication/common/authentication.js';
|
||||
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
|
||||
import { IExtHostRpcService } from './extHostRpcService.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { URI, UriComponents } from '../../../base/common/uri.js';
|
||||
import { fetchDynamicRegistration, getClaimsFromJWT, IAuthorizationJWTClaims, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, IAuthorizationTokenResponse, isAuthorizationTokenResponse } from '../../../base/common/oauth.js';
|
||||
import { IExtHostWindow } from './extHostWindow.js';
|
||||
import { IExtHostInitDataService } from './extHostInitDataService.js';
|
||||
|
@ -114,7 +114,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
|
|||
|
||||
this._authenticationProviders.set(id, { label, provider, options: options ?? { supportsMultipleAccounts: false } });
|
||||
const listener = provider.onDidChangeSessions(e => this._proxy.$sendDidChangeSessions(id, e));
|
||||
this._proxy.$registerAuthenticationProvider(id, label, options?.supportsMultipleAccounts ?? false, options?.supportedIssuers);
|
||||
this._proxy.$registerAuthenticationProvider(id, label, options?.supportsMultipleAccounts ?? false, options?.supportedAuthorizationServers);
|
||||
|
||||
return new Disposable(() => {
|
||||
listener.dispose();
|
||||
|
@ -126,7 +126,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
|
|||
async $createSession(providerId: string, scopes: string[], options: vscode.AuthenticationProviderSessionOptions): Promise<vscode.AuthenticationSession> {
|
||||
const providerData = this._authenticationProviders.get(providerId);
|
||||
if (providerData) {
|
||||
options.issuer = URI.revive(options.issuer);
|
||||
options.authorizationServer = URI.revive(options.authorizationServer);
|
||||
return await providerData.provider.createSession(scopes, options);
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
|
|||
async $getSessions(providerId: string, scopes: ReadonlyArray<string> | undefined, options: vscode.AuthenticationProviderSessionOptions): Promise<ReadonlyArray<vscode.AuthenticationSession>> {
|
||||
const providerData = this._authenticationProviders.get(providerId);
|
||||
if (providerData) {
|
||||
options.issuer = URI.revive(options.issuer);
|
||||
options.authorizationServer = URI.revive(options.authorizationServer);
|
||||
return await providerData.provider.getSessions(scopes, options);
|
||||
}
|
||||
|
||||
|
@ -170,6 +170,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
|
|||
}
|
||||
|
||||
async $registerDynamicAuthProvider(
|
||||
authorizationServerComponents: UriComponents,
|
||||
serverMetadata: IAuthorizationServerMetadata,
|
||||
resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined,
|
||||
clientId: string | undefined,
|
||||
|
@ -193,6 +194,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
|
|||
this._extHostProgress,
|
||||
this._extHostLoggerService,
|
||||
this._proxy,
|
||||
URI.revive(authorizationServerComponents),
|
||||
serverMetadata,
|
||||
resourceMetadata,
|
||||
clientId,
|
||||
|
@ -209,7 +211,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
|
|||
options: { supportsMultipleAccounts: false }
|
||||
}
|
||||
);
|
||||
await this._proxy.$registerDynamicAuthenticationProvider(provider.id, provider.label, provider.issuer, provider.clientId);
|
||||
await this._proxy.$registerDynamicAuthenticationProvider(provider.id, provider.label, provider.authorizationServer, provider.clientId);
|
||||
return provider.id;
|
||||
}
|
||||
|
||||
|
@ -236,7 +238,6 @@ class TaskSingler<T> {
|
|||
export class DynamicAuthProvider implements vscode.AuthenticationProvider {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly issuer: URI;
|
||||
|
||||
private _onDidChangeSessions = new Emitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
|
||||
readonly onDidChangeSessions = this._onDidChangeSessions.event;
|
||||
|
@ -258,19 +259,20 @@ export class DynamicAuthProvider implements vscode.AuthenticationProvider {
|
|||
@IExtHostProgress private readonly _extHostProgress: IExtHostProgress,
|
||||
@ILoggerService loggerService: ILoggerService,
|
||||
protected readonly _proxy: MainThreadAuthenticationShape,
|
||||
readonly authorizationServer: URI,
|
||||
protected readonly _serverMetadata: IAuthorizationServerMetadata,
|
||||
protected readonly _resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined,
|
||||
readonly clientId: string,
|
||||
onDidDynamicAuthProviderTokensChange: Emitter<{ authProviderId: string; clientId: string; tokens: IAuthorizationToken[] }>,
|
||||
initialTokens: IAuthorizationToken[],
|
||||
) {
|
||||
this.issuer = URI.parse(_serverMetadata.issuer);
|
||||
const stringifiedServer = authorizationServer.toString(true);
|
||||
this.id = _resourceMetadata?.resource
|
||||
? _serverMetadata.issuer + ' ' + _resourceMetadata?.resource
|
||||
: _serverMetadata.issuer;
|
||||
this.label = _resourceMetadata?.resource_name ?? this.issuer.authority;
|
||||
? stringifiedServer + ' ' + _resourceMetadata?.resource
|
||||
: stringifiedServer;
|
||||
this.label = _resourceMetadata?.resource_name ?? this.authorizationServer.authority;
|
||||
|
||||
this._logger = loggerService.createLogger(_serverMetadata.issuer, { name: this.label });
|
||||
this._logger = loggerService.createLogger(stringifiedServer, { name: this.label });
|
||||
this._disposable = new DisposableStore();
|
||||
this._disposable.add(this._onDidChangeSessions);
|
||||
const scopedEvent = Event.chain(onDidDynamicAuthProviderTokensChange.event, $ => $
|
||||
|
@ -280,7 +282,7 @@ export class DynamicAuthProvider implements vscode.AuthenticationProvider {
|
|||
this._tokenStore = this._disposable.add(new TokenStore(
|
||||
{
|
||||
onDidChange: scopedEvent,
|
||||
set: (tokens) => _proxy.$setSessionsForDynamicAuthProvider(this._serverMetadata.issuer, this.clientId, tokens),
|
||||
set: (tokens) => _proxy.$setSessionsForDynamicAuthProvider(stringifiedServer, this.clientId, tokens),
|
||||
},
|
||||
initialTokens,
|
||||
this._logger
|
||||
|
@ -412,8 +414,7 @@ export class DynamicAuthProvider implements vscode.AuthenticationProvider {
|
|||
|
||||
// Generate a random state value to prevent CSRF
|
||||
const nonce = this.generateRandomString(32);
|
||||
const issuer = URI.parse(this._serverMetadata.issuer);
|
||||
const callbackUri = URI.parse(`${this._initData.environment.appUriScheme}://dynamicauthprovider/${issuer.authority}/authorize?nonce=${nonce}`);
|
||||
const callbackUri = URI.parse(`${this._initData.environment.appUriScheme}://dynamicauthprovider/${this.authorizationServer.authority}/authorize?nonce=${nonce}`);
|
||||
let state: URI;
|
||||
try {
|
||||
state = await this._extHostUrls.createAppUri(callbackUri);
|
||||
|
|
|
@ -189,8 +189,9 @@ class McpHTTPHandle extends Disposable {
|
|||
private readonly _cts = new CancellationTokenSource();
|
||||
private readonly _abortCtrl = new AbortController();
|
||||
private _authMetadata?: {
|
||||
server: IAuthorizationServerMetadata;
|
||||
resource?: IAuthorizationProtectedResourceMetadata;
|
||||
authorizationServer: URI;
|
||||
serverMetadata: IAuthorizationServerMetadata;
|
||||
resourceMetadata?: IAuthorizationProtectedResourceMetadata;
|
||||
};
|
||||
|
||||
constructor(
|
||||
|
@ -354,17 +355,9 @@ class McpHTTPHandle extends Disposable {
|
|||
const serverMetadataResponse = await this._getAuthorizationServerMetadata(serverMetadataUrl, addtionalHeaders);
|
||||
const serverMetadataWithDefaults = getMetadataWithDefaultValues(serverMetadataResponse);
|
||||
this._authMetadata = {
|
||||
server: {
|
||||
...serverMetadataWithDefaults,
|
||||
// HACK: For now, just use the serverMetadataUrl as the issuer. I found an example, Entra,
|
||||
// that uses a placeholder for the tenant... https://login.microsoftonline.com/{tenant}/v2.0
|
||||
// literally... it contains `{tenant}`... instead of `organizations`. This may change our
|
||||
// API a bit to instead pass in these other endpoints, but for now, just user the serverMetadataUrl
|
||||
// as the isser.
|
||||
issuer: serverMetadataUrl,
|
||||
scopes_supported: scopesSupported ?? serverMetadataWithDefaults.scopes_supported
|
||||
},
|
||||
resource
|
||||
authorizationServer: URI.parse(serverMetadataUrl),
|
||||
serverMetadata: serverMetadataWithDefaults,
|
||||
resourceMetadata: resource
|
||||
};
|
||||
return;
|
||||
} catch (e) {
|
||||
|
@ -375,8 +368,9 @@ class McpHTTPHandle extends Disposable {
|
|||
const defaultMetadata = getDefaultMetadataForUrl(new URL(baseUrl));
|
||||
defaultMetadata.scopes_supported = scopesSupported ?? defaultMetadata.scopes_supported ?? [];
|
||||
this._authMetadata = {
|
||||
server: defaultMetadata,
|
||||
resource
|
||||
authorizationServer: URI.parse(serverMetadataUrl),
|
||||
serverMetadata: defaultMetadata,
|
||||
resourceMetadata: resource
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -414,8 +408,8 @@ class McpHTTPHandle extends Disposable {
|
|||
// For the oauth server metadata discovery path, we _INSERT_
|
||||
// the well known path after the origin and before the path.
|
||||
// https://datatracker.ietf.org/doc/html/rfc8414#section-3
|
||||
const issuer = new URL(authorizationServer);
|
||||
const extraPath = issuer.pathname === '/' ? '' : issuer.pathname;
|
||||
const authorizationServerUrl = new URL(authorizationServer);
|
||||
const extraPath = authorizationServerUrl.pathname === '/' ? '' : authorizationServerUrl.pathname;
|
||||
const pathToFetch = new URL(AUTH_SERVER_METADATA_DISCOVERY_PATH, authorizationServer).toString() + extraPath;
|
||||
let authServerMetadataResponse = await fetch(pathToFetch, {
|
||||
method: 'GET',
|
||||
|
@ -648,7 +642,7 @@ 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.server, this._authMetadata.resource);
|
||||
const token = await this._proxy.$getTokenFromServerMetadata(this._id, this._authMetadata.authorizationServer, this._authMetadata.serverMetadata, this._authMetadata.resourceMetadata);
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import { DeferredPromise, raceCancellationError } from '../../../base/common/asy
|
|||
import { IExtHostProgress } from '../common/extHostProgress.js';
|
||||
import { IProgressStep } from '../../../platform/progress/common/progress.js';
|
||||
import { CancellationError, isCancellationError } from '../../../base/common/errors.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
|
||||
interface IOAuthResult {
|
||||
code: string;
|
||||
|
@ -200,6 +201,7 @@ export class NodeDynamicAuthProvider extends DynamicAuthProvider {
|
|||
extHostProgress: IExtHostProgress,
|
||||
loggerService: ILoggerService,
|
||||
proxy: MainThreadAuthenticationShape,
|
||||
authorizationServer: URI,
|
||||
serverMetadata: IAuthorizationServerMetadata,
|
||||
resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined,
|
||||
clientId: string,
|
||||
|
@ -213,6 +215,7 @@ export class NodeDynamicAuthProvider extends DynamicAuthProvider {
|
|||
extHostProgress,
|
||||
loggerService,
|
||||
proxy,
|
||||
authorizationServer,
|
||||
serverMetadata,
|
||||
resourceMetadata,
|
||||
clientId,
|
||||
|
|
|
@ -54,7 +54,7 @@ class AuthenticationDataRenderer extends Disposable implements IExtensionFeature
|
|||
return [
|
||||
auth.label,
|
||||
auth.id,
|
||||
(auth.issuerGlobs ?? []).join(',\n')
|
||||
(auth.authorizationServerGlobs ?? []).join(',\n')
|
||||
];
|
||||
});
|
||||
|
||||
|
|
|
@ -60,6 +60,14 @@ const authenticationDefinitionSchema: IJSONSchema = {
|
|||
label: {
|
||||
type: 'string',
|
||||
description: localize('authentication.label', 'The human readable name of the authentication provider.'),
|
||||
},
|
||||
authorizationServerGlobs: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
description: localize('authentication.authorizationServerGlobs', 'A list of globs that match the authorization servers that this provider supports.'),
|
||||
},
|
||||
description: localize('authentication.authorizationServerGlobsDescription', 'A list of globs that match the authorization servers that this provider supports.')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -258,15 +266,15 @@ export class AuthenticationService extends Disposable implements IAuthentication
|
|||
async getSessions(id: string, scopes?: string[], options?: IAuthenticationGetSessionsOptions, activateImmediate: boolean = false): Promise<ReadonlyArray<AuthenticationSession>> {
|
||||
const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate);
|
||||
if (authProvider) {
|
||||
// Check if the issuer is in the list of supported issuers
|
||||
if (options?.issuer) {
|
||||
const issuerStr = options.issuer.toString(true);
|
||||
// Check if the authorization server is in the list of supported authorization servers
|
||||
if (options?.authorizationServer) {
|
||||
const authServerStr = options.authorizationServer.toString(true);
|
||||
// TODO: something is off here...
|
||||
if (!authProvider.issuers?.some(i => i.toString(true) === issuerStr || match(i.toString(true), issuerStr))) {
|
||||
throw new Error(`The issuer '${issuerStr}' is not supported by the authentication provider '${id}'.`);
|
||||
if (!authProvider.authorizationServers?.some(i => i.toString(true) === authServerStr || match(i.toString(true), authServerStr))) {
|
||||
throw new Error(`The authorization server '${authServerStr}' is not supported by the authentication provider '${id}'.`);
|
||||
}
|
||||
}
|
||||
return await authProvider.getSessions(scopes, { account: options?.account, issuer: options?.issuer });
|
||||
return await authProvider.getSessions(scopes, { account: options?.account, authorizationServer: options?.authorizationServer });
|
||||
} else {
|
||||
throw new Error(`No authentication provider '${id}' is currently registered.`);
|
||||
}
|
||||
|
@ -277,7 +285,7 @@ export class AuthenticationService extends Disposable implements IAuthentication
|
|||
if (authProvider) {
|
||||
return await authProvider.createSession(scopes, {
|
||||
account: options?.account,
|
||||
issuer: options?.issuer
|
||||
authorizationServer: options?.authorizationServer
|
||||
});
|
||||
} else {
|
||||
throw new Error(`No authentication provider '${id}' is currently registered.`);
|
||||
|
@ -293,36 +301,36 @@ export class AuthenticationService extends Disposable implements IAuthentication
|
|||
}
|
||||
}
|
||||
|
||||
async getOrActivateProviderIdForIssuer(issuer: URI): Promise<string | undefined> {
|
||||
async getOrActivateProviderIdForServer(authorizationServer: URI): Promise<string | undefined> {
|
||||
for (const provider of this._authenticationProviders.values()) {
|
||||
if (provider.issuers?.some(i => i.toString(true) === issuer.toString(true) || match(i.toString(true), issuer.toString(true)))) {
|
||||
if (provider.authorizationServers?.some(i => i.toString(true) === authorizationServer.toString(true) || match(i.toString(true), authorizationServer.toString(true)))) {
|
||||
return provider.id;
|
||||
}
|
||||
}
|
||||
|
||||
const issuerStr = issuer.toString(true);
|
||||
const authServerStr = authorizationServer.toString(true);
|
||||
const providers = this._declaredProviders
|
||||
// Only consider providers that are not already registered since we already checked them
|
||||
.filter(p => !this._authenticationProviders.has(p.id))
|
||||
.filter(p => !!p.issuerGlobs?.some(i => match(i, issuerStr)));
|
||||
.filter(p => !!p.authorizationServerGlobs?.some(i => match(i, authServerStr)));
|
||||
// TODO:@TylerLeonhardt fan out?
|
||||
for (const provider of providers) {
|
||||
const activeProvider = await this.tryActivateProvider(provider.id, true);
|
||||
// Check the resolved issuers
|
||||
if (activeProvider.issuers?.some(i => match(i.toString(true), issuerStr))) {
|
||||
// Check the resolved authorization servers
|
||||
if (activeProvider.authorizationServers?.some(i => match(i.toString(true), authServerStr))) {
|
||||
return activeProvider.id;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async createDynamicAuthenticationProvider(serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<IAuthenticationProvider | undefined> {
|
||||
async createDynamicAuthenticationProvider(authorizationServer: URI, serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<IAuthenticationProvider | undefined> {
|
||||
const delegate = this._delegates[0];
|
||||
if (!delegate) {
|
||||
this._logService.error('No authentication provider host delegate found');
|
||||
return undefined;
|
||||
}
|
||||
const providerId = await delegate.create(serverMetadata, resource);
|
||||
const providerId = await delegate.create(authorizationServer, serverMetadata, resource);
|
||||
const provider = this._authenticationProviders.get(providerId);
|
||||
if (provider) {
|
||||
this._logService.debug(`Created dynamic authentication provider: ${providerId}`);
|
||||
|
|
|
@ -56,12 +56,12 @@ export class DynamicAuthenticationProviderStorageService extends Disposable impl
|
|||
return provider?.clientId;
|
||||
}
|
||||
|
||||
storeClientId(providerId: string, clientId: string, label?: string, issuer?: string): void {
|
||||
storeClientId(providerId: string, authorizationServer: string, clientId: string, label?: string): void {
|
||||
// Store provider information in single location
|
||||
this._trackProvider(providerId, clientId, label, issuer);
|
||||
this._trackProvider(providerId, authorizationServer, clientId, label);
|
||||
}
|
||||
|
||||
private _trackProvider(providerId: string, clientId: string, label?: string, issuer?: string): void {
|
||||
private _trackProvider(providerId: string, authorizationServer: string, clientId: string, label?: string): void {
|
||||
const providers = this._getStoredProviders();
|
||||
|
||||
// Check if provider already exists
|
||||
|
@ -71,7 +71,7 @@ export class DynamicAuthenticationProviderStorageService extends Disposable impl
|
|||
const newProvider: DynamicAuthenticationProviderInfo = {
|
||||
providerId,
|
||||
label: label || providerId, // Use provided label or providerId as default
|
||||
issuer: issuer || providerId, // Use provided issuer or providerId as default
|
||||
authorizationServer,
|
||||
clientId
|
||||
};
|
||||
providers.push(newProvider);
|
||||
|
@ -82,7 +82,7 @@ export class DynamicAuthenticationProviderStorageService extends Disposable impl
|
|||
const updatedProvider: DynamicAuthenticationProviderInfo = {
|
||||
providerId,
|
||||
label: label || existingProvider.label,
|
||||
issuer: issuer || existingProvider.issuer,
|
||||
authorizationServer,
|
||||
clientId
|
||||
};
|
||||
providers[existingProviderIndex] = updatedProvider;
|
||||
|
@ -93,7 +93,14 @@ export class DynamicAuthenticationProviderStorageService extends Disposable impl
|
|||
private _getStoredProviders(): DynamicAuthenticationProviderInfo[] {
|
||||
const stored = this.storageService.get(DynamicAuthenticationProviderStorageService.PROVIDERS_STORAGE_KEY, StorageScope.APPLICATION, '[]');
|
||||
try {
|
||||
return JSON.parse(stored);
|
||||
const providerInfos = JSON.parse(stored);
|
||||
// MIGRATION: remove after an iteration or 2
|
||||
for (const providerInfo of providerInfos) {
|
||||
if (!providerInfo.authorizationServer) {
|
||||
providerInfo.authorizationServer = providerInfo.issuer;
|
||||
}
|
||||
}
|
||||
return providerInfos;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export interface AuthenticationSessionsChangeEvent {
|
|||
export interface AuthenticationProviderInformation {
|
||||
id: string;
|
||||
label: string;
|
||||
issuerGlobs?: ReadonlyArray<string>;
|
||||
authorizationServerGlobs?: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
export interface IAuthenticationCreateSessionOptions {
|
||||
|
@ -46,10 +46,10 @@ export interface IAuthenticationCreateSessionOptions {
|
|||
*/
|
||||
account?: AuthenticationSessionAccount;
|
||||
/**
|
||||
* The issuer URI to use for this creation request. If passed in, first we validate that
|
||||
* the provider can use this issuer, then it is passed down to the auth provider.
|
||||
* The authorization server URI to use for this creation request. If passed in, first we validate that
|
||||
* the provider can use this authorization server, then it is passed down to the auth provider.
|
||||
*/
|
||||
issuer?: URI;
|
||||
authorizationServer?: URI;
|
||||
}
|
||||
|
||||
export interface IAuthenticationGetSessionsOptions {
|
||||
|
@ -59,10 +59,10 @@ export interface IAuthenticationGetSessionsOptions {
|
|||
*/
|
||||
account?: AuthenticationSessionAccount;
|
||||
/**
|
||||
* The issuer URI to use for this request. If passed in, first we validate that
|
||||
* the provider can use this issuer, then it is passed down to the auth provider.
|
||||
* The authorization server URI to use for this request. If passed in, first we validate that
|
||||
* the provider can use this authorization server, then it is passed down to the auth provider.
|
||||
*/
|
||||
issuer?: URI;
|
||||
authorizationServer?: URI;
|
||||
}
|
||||
|
||||
export interface AllowedExtension {
|
||||
|
@ -82,7 +82,7 @@ export interface AllowedExtension {
|
|||
export interface IAuthenticationProviderHostDelegate {
|
||||
/** Priority for this delegate, delegates are tested in descending priority order */
|
||||
readonly priority: number;
|
||||
create(serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<string>;
|
||||
create(authorizationServer: URI, serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<string>;
|
||||
}
|
||||
|
||||
export const IAuthenticationService = createDecorator<IAuthenticationService>('IAuthenticationService');
|
||||
|
@ -189,10 +189,10 @@ export interface IAuthenticationService {
|
|||
removeSession(providerId: string, sessionId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets a provider id for a specified issuer
|
||||
* @param issuer The issuer url that this provider is responsible for
|
||||
* Gets a provider id for a specified authorization server
|
||||
* @param authorizationServer The authorization server url that this provider is responsible for
|
||||
*/
|
||||
getOrActivateProviderIdForIssuer(issuer: URI): Promise<string | undefined>;
|
||||
getOrActivateProviderIdForServer(authorizationServer: URI): Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Allows the ability register a delegate that will be used to start authentication providers
|
||||
|
@ -204,7 +204,7 @@ export interface IAuthenticationService {
|
|||
* Creates a dynamic authentication provider for the given server metadata
|
||||
* @param serverMetadata The metadata for the server that is being authenticated against
|
||||
*/
|
||||
createDynamicAuthenticationProvider(serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined): Promise<IAuthenticationProvider | undefined>;
|
||||
createDynamicAuthenticationProvider(authorizationServer: URI, serverMetadata: IAuthorizationServerMetadata, resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined): Promise<IAuthenticationProvider | undefined>;
|
||||
}
|
||||
|
||||
export function isAuthenticationSession(thing: unknown): thing is AuthenticationSession {
|
||||
|
@ -301,10 +301,10 @@ export interface IAuthenticationProviderSessionOptions {
|
|||
*/
|
||||
account?: AuthenticationSessionAccount;
|
||||
/**
|
||||
* The issuer that is being asked about. If this is passed in, the provider should
|
||||
* attempt to return sessions that are only related to this issuer.
|
||||
* The authorization server that is being asked about. If this is passed in, the provider should
|
||||
* attempt to return sessions that are only related to this authorization server.
|
||||
*/
|
||||
issuer?: URI;
|
||||
authorizationServer?: URI;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -322,9 +322,9 @@ export interface IAuthenticationProvider {
|
|||
readonly label: string;
|
||||
|
||||
/**
|
||||
* The resolved issuers. These can still contain globs, but should be concrete URIs
|
||||
* The resolved authorization servers. These can still contain globs, but should be concrete URIs
|
||||
*/
|
||||
readonly issuers?: ReadonlyArray<URI>;
|
||||
readonly authorizationServers?: ReadonlyArray<URI>;
|
||||
|
||||
/**
|
||||
* Indicates whether the authentication provider supports multiple accounts.
|
||||
|
|
|
@ -12,7 +12,11 @@ export const IDynamicAuthenticationProviderStorageService = createDecorator<IDyn
|
|||
export interface DynamicAuthenticationProviderInfo {
|
||||
readonly providerId: string;
|
||||
readonly label: string;
|
||||
readonly issuer: string;
|
||||
/**
|
||||
* @deprecated in favor of authorizationServer
|
||||
*/
|
||||
readonly issuer?: string;
|
||||
readonly authorizationServer: string;
|
||||
readonly clientId: string;
|
||||
}
|
||||
|
||||
|
@ -35,19 +39,19 @@ export interface IDynamicAuthenticationProviderStorageService {
|
|||
|
||||
/**
|
||||
* Get the client ID for a dynamic authentication provider.
|
||||
* @param providerId The provider ID or issuer URL.
|
||||
* @param providerId The provider ID or authorization server URL.
|
||||
* @returns The client ID if it exists, undefined otherwise.
|
||||
*/
|
||||
getClientId(providerId: string): string | undefined;
|
||||
|
||||
/**
|
||||
* Store the client ID for a dynamic authentication provider.
|
||||
* @param providerId The provider ID or issuer URL.
|
||||
* @param providerId The provider ID or authorization server URL.
|
||||
* @param clientId The client ID to store.
|
||||
* @param label Optional label for the provider.
|
||||
* @param issuer Optional issuer URL for the provider.
|
||||
* @param authorizationServer Optional authorization server URL for the provider.
|
||||
*/
|
||||
storeClientId(providerId: string, clientId: string, label?: string, issuer?: string): void;
|
||||
storeClientId(providerId: string, authorizationServer: string, clientId: string, label?: string): void;
|
||||
|
||||
/**
|
||||
* Get all dynamic authentication providers that have been interacted with.
|
||||
|
|
|
@ -129,73 +129,73 @@ suite('AuthenticationService', () => {
|
|||
assert.deepEqual(retrievedProvider, provider);
|
||||
});
|
||||
|
||||
test('getOrActivateProviderIdForIssuer - should return undefined when no provider matches the issuer', async () => {
|
||||
const issuer = URI.parse('https://example.com');
|
||||
const result = await authenticationService.getOrActivateProviderIdForIssuer(issuer);
|
||||
test('getOrActivateProviderIdForServer - should return undefined when no provider matches the authorization server', async () => {
|
||||
const authorizationServer = URI.parse('https://example.com');
|
||||
const result = await authenticationService.getOrActivateProviderIdForServer(authorizationServer);
|
||||
assert.strictEqual(result, undefined);
|
||||
});
|
||||
|
||||
test('getOrActivateProviderIdForIssuer - should return provider id if issuerGlobs matches and issuers match', async () => {
|
||||
// Register a declared provider with an issuer glob
|
||||
test('getOrActivateProviderIdForServer - should return provider id if authorizationServerGlobs matches and authorizationServers match', async () => {
|
||||
// Register a declared provider with an authorization server glob
|
||||
const provider: AuthenticationProviderInformation = {
|
||||
id: 'github',
|
||||
label: 'GitHub',
|
||||
issuerGlobs: ['https://github.com/*']
|
||||
authorizationServerGlobs: ['https://github.com/*']
|
||||
};
|
||||
authenticationService.registerDeclaredAuthenticationProvider(provider);
|
||||
|
||||
// Register an authentication provider with matching issuers
|
||||
// Register an authentication provider with matching authorization servers
|
||||
const authProvider = createProvider({
|
||||
id: 'github',
|
||||
label: 'GitHub',
|
||||
issuers: [URI.parse('https://github.com/login')]
|
||||
authorizationServers: [URI.parse('https://github.com/login')]
|
||||
});
|
||||
authenticationService.registerAuthenticationProvider('github', authProvider);
|
||||
|
||||
// Test with a matching URI
|
||||
const issuer = URI.parse('https://github.com/login');
|
||||
const result = await authenticationService.getOrActivateProviderIdForIssuer(issuer);
|
||||
const authorizationServer = URI.parse('https://github.com/login');
|
||||
const result = await authenticationService.getOrActivateProviderIdForServer(authorizationServer);
|
||||
|
||||
// Verify the result
|
||||
assert.strictEqual(result, 'github');
|
||||
});
|
||||
|
||||
test('getOrActivateProviderIdForIssuer - should return undefined if issuerGlobs match but issuers do not match', async () => {
|
||||
// Register a declared provider with an issuer glob
|
||||
test('getOrActivateProviderIdForServer - should return undefined if authorizationServerGlobs match but authorizationServers do not match', async () => {
|
||||
// Register a declared provider with an authorization server glob
|
||||
const provider: AuthenticationProviderInformation = {
|
||||
id: 'github',
|
||||
label: 'GitHub',
|
||||
issuerGlobs: ['https://github.com/*']
|
||||
authorizationServerGlobs: ['https://github.com/*']
|
||||
};
|
||||
authenticationService.registerDeclaredAuthenticationProvider(provider);
|
||||
|
||||
// Register an authentication provider with non-matching issuers
|
||||
// Register an authentication provider with non-matching authorization servers
|
||||
const authProvider = createProvider({
|
||||
id: 'github',
|
||||
label: 'GitHub',
|
||||
issuers: [URI.parse('https://github.com/different')]
|
||||
authorizationServers: [URI.parse('https://github.com/different')]
|
||||
});
|
||||
authenticationService.registerAuthenticationProvider('github', authProvider);
|
||||
|
||||
// Test with a non-matching URI
|
||||
const issuer = URI.parse('https://github.com/login');
|
||||
const result = await authenticationService.getOrActivateProviderIdForIssuer(issuer);
|
||||
const authorizationServer = URI.parse('https://github.com/login');
|
||||
const result = await authenticationService.getOrActivateProviderIdForServer(authorizationServer);
|
||||
|
||||
// Verify the result
|
||||
assert.strictEqual(result, undefined);
|
||||
});
|
||||
|
||||
test('getOrActivateProviderIdForIssuer - should check multiple providers and return the first match', async () => {
|
||||
// Register two declared providers with issuer globs
|
||||
test('getOrActivateProviderIdForAuthorizationServer - should check multiple providers and return the first match', async () => {
|
||||
// Register two declared providers with authorization server globs
|
||||
const provider1: AuthenticationProviderInformation = {
|
||||
id: 'github',
|
||||
label: 'GitHub',
|
||||
issuerGlobs: ['https://github.com/*']
|
||||
authorizationServerGlobs: ['https://github.com/*']
|
||||
};
|
||||
const provider2: AuthenticationProviderInformation = {
|
||||
id: 'microsoft',
|
||||
label: 'Microsoft',
|
||||
issuerGlobs: ['https://login.microsoftonline.com/*']
|
||||
authorizationServerGlobs: ['https://login.microsoftonline.com/*']
|
||||
};
|
||||
authenticationService.registerDeclaredAuthenticationProvider(provider1);
|
||||
authenticationService.registerDeclaredAuthenticationProvider(provider2);
|
||||
|
@ -204,20 +204,20 @@ suite('AuthenticationService', () => {
|
|||
const githubProvider = createProvider({
|
||||
id: 'github',
|
||||
label: 'GitHub',
|
||||
issuers: [URI.parse('https://github.com/different')]
|
||||
authorizationServers: [URI.parse('https://github.com/different')]
|
||||
});
|
||||
authenticationService.registerAuthenticationProvider('github', githubProvider);
|
||||
|
||||
const microsoftProvider = createProvider({
|
||||
id: 'microsoft',
|
||||
label: 'Microsoft',
|
||||
issuers: [URI.parse('https://login.microsoftonline.com/common')]
|
||||
authorizationServers: [URI.parse('https://login.microsoftonline.com/common')]
|
||||
});
|
||||
authenticationService.registerAuthenticationProvider('microsoft', microsoftProvider);
|
||||
|
||||
// Test with a URI that should match the second provider
|
||||
const issuer = URI.parse('https://login.microsoftonline.com/common');
|
||||
const result = await authenticationService.getOrActivateProviderIdForIssuer(issuer);
|
||||
const authorizationServer = URI.parse('https://login.microsoftonline.com/common');
|
||||
const result = await authenticationService.getOrActivateProviderIdForServer(authorizationServer);
|
||||
|
||||
// Verify the result
|
||||
assert.strictEqual(result, 'microsoft');
|
||||
|
@ -240,7 +240,7 @@ suite('AuthenticationService', () => {
|
|||
assert.ok(isCalled);
|
||||
});
|
||||
|
||||
test('getSessions - issuer is not registered', async () => {
|
||||
test('getSessions - authorization server is not registered', async () => {
|
||||
let isCalled = false;
|
||||
const provider = createProvider({
|
||||
getSessions: async () => {
|
||||
|
@ -249,7 +249,7 @@ suite('AuthenticationService', () => {
|
|||
},
|
||||
});
|
||||
authenticationService.registerAuthenticationProvider(provider.id, provider);
|
||||
assert.rejects(() => authenticationService.getSessions(provider.id, [], { issuer: URI.parse('https://example.com') }));
|
||||
assert.rejects(() => authenticationService.getSessions(provider.id, [], { authorizationServer: URI.parse('https://example.com') }));
|
||||
assert.ok(!isCalled);
|
||||
});
|
||||
|
||||
|
|
|
@ -6,25 +6,25 @@
|
|||
declare module 'vscode' {
|
||||
export interface AuthenticationProviderOptions {
|
||||
/**
|
||||
* When specified, this provider will be associated with these issuers. They can still contain globs
|
||||
* When specified, this provider will be associated with these authorization servers. They can still contain globs
|
||||
* just like their extension contribution counterparts.
|
||||
*/
|
||||
readonly supportedIssuers?: Uri[];
|
||||
readonly supportedAuthorizationServers?: Uri[];
|
||||
}
|
||||
|
||||
export interface AuthenticationProviderSessionOptions {
|
||||
/**
|
||||
* When specified, the authentication provider will use the provided issuer URL to
|
||||
* authenticate the user. This is only used when a provider `supportsIssuerOverride` is set to true
|
||||
* When specified, the authentication provider will use the provided authorization server URL to
|
||||
* authenticate the user. This is only used when a provider has `supportsAuthorizationServers` set
|
||||
*/
|
||||
issuer?: Uri;
|
||||
authorizationServer?: Uri;
|
||||
}
|
||||
|
||||
export interface AuthenticationGetSessionOptions {
|
||||
/**
|
||||
* When specified, the authentication provider will use the provided issuer URL to
|
||||
* authenticate the user. This is only used when a provider `supportsIssuerOverride` is set to true
|
||||
* When specified, the authentication provider will use the provided authorization server URL to
|
||||
* authenticate the user. This is only used when a provider has `supportsAuthorizationServers` set
|
||||
*/
|
||||
issuer?: Uri;
|
||||
authorizationServer?: Uri;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue