mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-28 21:55:54 +00:00
Introduce Issuer handling in the Authentication stack (#248948)
Mostly plumbing... this enables:
```
vscode.authentication.getSession('microsoft', scopes, { issuer: "https://login.microsoftonline.com/common/v2.0" });
```
And the respective API for an auth providers to handle it being passed in.
This props up work in MCP land which needs a way to map an issuer to an auth provider... but I certainly see utility outside of that space.
Fixes https://github.com/microsoft/vscode/issues/248775#issuecomment-2876711396
This commit is contained in:
committed by
GitHub
parent
1e03c9074c
commit
86efdcd2c1
@@ -203,7 +203,7 @@ export class AzureActiveDirectoryService {
|
||||
return this._sessionChangeEmitter.event;
|
||||
}
|
||||
|
||||
public getSessions(scopes?: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise<vscode.AuthenticationSession[]> {
|
||||
public getSessions(scopes: string[] | undefined, { account, issuer }: vscode.AuthenticationProviderSessionOptions = {}): Promise<vscode.AuthenticationSession[]> {
|
||||
if (!scopes) {
|
||||
this._logger.info('Getting sessions for all scopes...');
|
||||
const sessions = this._tokens
|
||||
@@ -226,6 +226,12 @@ export class AzureActiveDirectoryService {
|
||||
if (!modifiedScopes.includes('offline_access')) {
|
||||
modifiedScopes.push('offline_access');
|
||||
}
|
||||
if (issuer) {
|
||||
const tenant = issuer.path.split('/')[1];
|
||||
if (tenant) {
|
||||
modifiedScopes.push(`VSCODE_TENANT:${tenant}`);
|
||||
}
|
||||
}
|
||||
modifiedScopes = modifiedScopes.sort();
|
||||
|
||||
const modifiedScopesStr = modifiedScopes.join(' ');
|
||||
@@ -237,7 +243,7 @@ export class AzureActiveDirectoryService {
|
||||
scopeStr: modifiedScopesStr,
|
||||
// filter our special scopes
|
||||
scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '),
|
||||
tenant: this.getTenantId(scopes),
|
||||
tenant: this.getTenantId(modifiedScopes),
|
||||
};
|
||||
|
||||
this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions` + account ? ` for ${account?.label}` : '');
|
||||
@@ -297,7 +303,7 @@ export class AzureActiveDirectoryService {
|
||||
.map(result => (result as PromiseFulfilledResult<vscode.AuthenticationSession>).value);
|
||||
}
|
||||
|
||||
public createSession(scopes: string[], account?: vscode.AuthenticationSessionAccountInformation): Promise<vscode.AuthenticationSession> {
|
||||
public createSession(scopes: string[], { account, issuer }: vscode.AuthenticationProviderSessionOptions = {}): Promise<vscode.AuthenticationSession> {
|
||||
let modifiedScopes = [...scopes];
|
||||
if (!modifiedScopes.includes('openid')) {
|
||||
modifiedScopes.push('openid');
|
||||
@@ -311,6 +317,12 @@ export class AzureActiveDirectoryService {
|
||||
if (!modifiedScopes.includes('offline_access')) {
|
||||
modifiedScopes.push('offline_access');
|
||||
}
|
||||
if (issuer) {
|
||||
const tenant = issuer.path.split('/')[1];
|
||||
if (tenant) {
|
||||
modifiedScopes.push(`VSCODE_TENANT:${tenant}`);
|
||||
}
|
||||
}
|
||||
modifiedScopes = modifiedScopes.sort();
|
||||
const scopeData: IScopeData = {
|
||||
originalScopes: scopes,
|
||||
@@ -319,7 +331,7 @@ export class AzureActiveDirectoryService {
|
||||
// filter our special scopes
|
||||
scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '),
|
||||
clientId: this.getClientId(scopes),
|
||||
tenant: this.getTenantId(scopes),
|
||||
tenant: this.getTenantId(modifiedScopes),
|
||||
};
|
||||
|
||||
this._logger.trace(`[${scopeData.scopeStr}] Queued creating session`);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Uri } from 'vscode';
|
||||
|
||||
const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56';
|
||||
const DEFAULT_TENANT = 'organizations';
|
||||
|
||||
@@ -43,14 +45,14 @@ export class ScopeData {
|
||||
*/
|
||||
readonly tenantId: string | undefined;
|
||||
|
||||
constructor(readonly originalScopes: readonly string[] = []) {
|
||||
constructor(readonly originalScopes: readonly string[] = [], issuer?: 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);
|
||||
this.tenant = this.getTenant(this.allScopes, issuer);
|
||||
this.tenantId = this.getTenantId(this.tenant);
|
||||
}
|
||||
|
||||
@@ -63,7 +65,14 @@ export class ScopeData {
|
||||
}, undefined) ?? DEFAULT_CLIENT_ID;
|
||||
}
|
||||
|
||||
private getTenant(scopes: string[]): string {
|
||||
private getTenant(scopes: string[], issuer?: Uri): string {
|
||||
if (issuer?.path) {
|
||||
// Get tenant portion of URL
|
||||
const tenant = issuer.path.split('/')[1];
|
||||
if (tenant) {
|
||||
return tenant;
|
||||
}
|
||||
}
|
||||
return scopes.reduce<string | undefined>((prev, current) => {
|
||||
if (current.startsWith('VSCODE_TENANT:')) {
|
||||
return current.split('VSCODE_TENANT:')[1];
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { ScopeData } from '../scopeData';
|
||||
import { Uri } from 'vscode';
|
||||
|
||||
suite('ScopeData', () => {
|
||||
test('should include default scopes if not present', () => {
|
||||
@@ -73,4 +74,22 @@ suite('ScopeData', () => {
|
||||
const scopeData = new ScopeData(['custom_scope', 'VSCODE_TENANT:some_guid']);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
assert.strictEqual(scopeData.tenant, 'url_tenant');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,49 +122,59 @@ export async function activate(context: vscode.ExtensionContext, telemetryReport
|
||||
Environment.AzureCloud);
|
||||
await loginService.initialize();
|
||||
|
||||
context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', {
|
||||
onDidChangeSessions: loginService.onDidChangeSessions,
|
||||
getSessions: (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => loginService.getSessions(scopes, options?.account),
|
||||
createSession: async (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => {
|
||||
try {
|
||||
/* __GDPR__
|
||||
"login" : {
|
||||
"owner": "TylerLeonhardt",
|
||||
"comment": "Used to determine the usage of the Microsoft Auth Provider.",
|
||||
"scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." }
|
||||
}
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('login', {
|
||||
// Get rid of guids from telemetry.
|
||||
scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))),
|
||||
});
|
||||
context.subscriptions.push(vscode.authentication.registerAuthenticationProvider(
|
||||
'microsoft',
|
||||
'Microsoft',
|
||||
{
|
||||
onDidChangeSessions: loginService.onDidChangeSessions,
|
||||
getSessions: (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => loginService.getSessions(scopes, options),
|
||||
createSession: async (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => {
|
||||
try {
|
||||
/* __GDPR__
|
||||
"login" : {
|
||||
"owner": "TylerLeonhardt",
|
||||
"comment": "Used to determine the usage of the Microsoft Auth Provider.",
|
||||
"scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." }
|
||||
}
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('login', {
|
||||
// Get rid of guids from telemetry.
|
||||
scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))),
|
||||
});
|
||||
|
||||
return await loginService.createSession(scopes, options?.account);
|
||||
} catch (e) {
|
||||
/* __GDPR__
|
||||
"loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('loginFailed');
|
||||
return await loginService.createSession(scopes, options);
|
||||
} catch (e) {
|
||||
/* __GDPR__
|
||||
"loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('loginFailed');
|
||||
|
||||
throw e;
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
removeSession: async (id: string) => {
|
||||
try {
|
||||
/* __GDPR__
|
||||
"logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('logout');
|
||||
|
||||
await loginService.removeSessionById(id);
|
||||
} catch (e) {
|
||||
/* __GDPR__
|
||||
"logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('logoutFailed');
|
||||
}
|
||||
}
|
||||
},
|
||||
removeSession: async (id: string) => {
|
||||
try {
|
||||
/* __GDPR__
|
||||
"logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('logout');
|
||||
|
||||
await loginService.removeSessionById(id);
|
||||
} catch (e) {
|
||||
/* __GDPR__
|
||||
"logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('logoutFailed');
|
||||
}
|
||||
{
|
||||
supportsMultipleAccounts: true,
|
||||
supportedIssuers: [
|
||||
vscode.Uri.parse('https://login.microsoftonline.com/*/v2.0')
|
||||
]
|
||||
}
|
||||
}, { supportsMultipleAccounts: true }));
|
||||
));
|
||||
|
||||
let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Environment, EnvironmentParameters } from '@azure/ms-rest-azure-env';
|
||||
import Logger from './logger';
|
||||
import { MsalAuthProvider } from './node/authProvider';
|
||||
import { UriEventHandler } from './UriEventHandler';
|
||||
import { authentication, commands, ExtensionContext, l10n, window, workspace, Disposable } from 'vscode';
|
||||
import { authentication, commands, ExtensionContext, l10n, window, workspace, Disposable, Uri } from 'vscode';
|
||||
import { MicrosoftAuthenticationTelemetryReporter, MicrosoftSovereignCloudAuthenticationTelemetryReporter } from './common/telemetryReporter';
|
||||
|
||||
async function initMicrosoftSovereignCloudAuthProvider(
|
||||
@@ -79,7 +79,12 @@ export async function activate(context: ExtensionContext, mainTelemetryReporter:
|
||||
'microsoft',
|
||||
'Microsoft',
|
||||
authProvider,
|
||||
{ supportsMultipleAccounts: true }
|
||||
{
|
||||
supportsMultipleAccounts: true,
|
||||
supportedIssuers: [
|
||||
Uri.parse('https://login.microsoftonline.com/*/v2.0')
|
||||
]
|
||||
}
|
||||
));
|
||||
|
||||
let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, uriHandler);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { AccountInfo, AuthenticationResult, ClientAuthError, ClientAuthErrorCodes, ServerError } from '@azure/msal-node';
|
||||
import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, EventEmitter, ExtensionContext, ExtensionKind, l10n, LogOutputChannel, window } from 'vscode';
|
||||
import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, EventEmitter, ExtensionContext, ExtensionKind, l10n, LogOutputChannel, Uri, window } from 'vscode';
|
||||
import { Environment } from '@azure/ms-rest-azure-env';
|
||||
import { CachedPublicClientApplicationManager } from './publicClientCache';
|
||||
import { UriEventHandler } from '../UriEventHandler';
|
||||
@@ -154,9 +154,9 @@ export class MsalAuthProvider implements AuthenticationProvider {
|
||||
|
||||
//#region AuthenticationProvider methods
|
||||
|
||||
async getSessions(scopes: string[] | undefined, options?: AuthenticationGetSessionOptions): Promise<AuthenticationSession[]> {
|
||||
async getSessions(scopes: string[] | undefined, options: AuthenticationGetSessionOptions = {}): Promise<AuthenticationSession[]> {
|
||||
const askingForAll = scopes === undefined;
|
||||
const scopeData = new ScopeData(scopes);
|
||||
const scopeData = new ScopeData(scopes, options?.issuer);
|
||||
// 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);
|
||||
const scopeData = new ScopeData(scopes, options.issuer);
|
||||
// Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead.
|
||||
|
||||
this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'starting');
|
||||
|
||||
Reference in New Issue
Block a user