diff --git a/extensions/microsoft-authentication/extension-browser.webpack.config.js b/extensions/microsoft-authentication/extension-browser.webpack.config.js index c9cb43b944b..c32a44124a2 100644 --- a/extensions/microsoft-authentication/extension-browser.webpack.config.js +++ b/extensions/microsoft-authentication/extension-browser.webpack.config.js @@ -24,9 +24,6 @@ module.exports = withBrowserDefaults({ 'keytar': 'commonjs keytar', }, resolve: { - fallback: { - 'querystring': require.resolve('querystring-es3') - }, alias: { './node/crypto': path.resolve(__dirname, 'src/browser/crypto'), './node/authServer': path.resolve(__dirname, 'src/browser/authServer'), diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 702b5cbfd1f..ecd2573ebf6 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -41,20 +41,58 @@ { "title": "Microsoft Sovereign Cloud", "properties": { - "microsoft-sovereign-cloud.endpoint": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "enum": [ - "Azure China", - "Azure US Government" - ] - } + "microsoft-sovereign-cloud.environment": { + "type": "string", + "markdownDescription": "%microsoft-sovereign-cloud.environment.description%", + "enum": [ + "ChinaCloud", + "USGovernment", + "custom" ], - "description": "%microsoft-sovereign-cloud.endpoint.description%" + "enumDescriptions": [ + "%microsoft-sovereign-cloud.environment.enumDescriptions.AzureChinaCloud%", + "%microsoft-sovereign-cloud.environment.enumDescriptions.AzureUSGovernment%", + "%microsoft-sovereign-cloud.environment.enumDescriptions.custom%" + ] + }, + "microsoft-sovereign-cloud.customEnvironment": { + "type": "object", + "additionalProperties": true, + "markdownDescription": "%microsoft-sovereign-cloud.customEnvironment.description%", + "properties": { + "name": { + "type": "string", + "description": "%microsoft-sovereign-cloud.customEnvironment.name.description%" + }, + "portalUrl": { + "type": "string", + "description": "%microsoft-sovereign-cloud.customEnvironment.portalUrl.description%" + }, + "managementEndpointUrl": { + "type": "string", + "description": "%microsoft-sovereign-cloud.customEnvironment.managementEndpointUrl.description%" + }, + "resourceManagerEndpointUrl": { + "type": "string", + "description": "%microsoft-sovereign-cloud.customEnvironment.resourceManagerEndpointUrl.description%" + }, + "activeDirectoryEndpointUrl": { + "type": "string", + "description": "%microsoft-sovereign-cloud.customEnvironment.activeDirectoryEndpointUrl.description%" + }, + "activeDirectoryResourceId": { + "type": "string", + "description": "%microsoft-sovereign-cloud.customEnvironment.activeDirectoryResourceId.description%" + } + }, + "required": [ + "name", + "portalUrl", + "managementEndpointUrl", + "resourceManagerEndpointUrl", + "activeDirectoryEndpointUrl", + "activeDirectoryResourceId" + ] } } } @@ -75,11 +113,11 @@ "@types/node-fetch": "^2.5.7", "@types/randombytes": "^2.0.0", "@types/sha.js": "^2.4.0", - "@types/uuid": "8.0.0", - "querystring-es3": "^0.2.1" + "@types/uuid": "8.0.0" }, "dependencies": { "node-fetch": "2.6.7", + "@azure/ms-rest-azure-env": "^2.0.0", "@vscode/extension-telemetry": "0.7.5" }, "repository": { diff --git a/extensions/microsoft-authentication/package.nls.json b/extensions/microsoft-authentication/package.nls.json index 12f51bb0163..14c625dc762 100644 --- a/extensions/microsoft-authentication/package.nls.json +++ b/extensions/microsoft-authentication/package.nls.json @@ -3,5 +3,27 @@ "description": "Microsoft authentication provider", "signIn": "Sign In", "signOut": "Sign Out", - "microsoft-sovereign-cloud.endpoint.description": "Login endpoint for Azure authentication. Select a national cloud or enter the login URL for a custom Azure cloud." + "microsoft-sovereign-cloud.environment.description": { + "message": "The Sovereign Cloud to use for authentication. If you select `custom`, you must also set the `#microsoft-sovereign-cloud.customEnvironment#` setting.", + "comment": [ + "{Locked='`#microsoft-sovereign-cloud.customEnvironment#`'}", + "The `#microsoft-sovereign-cloud.customEnvironment#` syntax will turn into a link. Do not translate it." + ] + }, + "microsoft-sovereign-cloud.environment.enumDescriptions.AzureChinaCloud": "Azure China", + "microsoft-sovereign-cloud.environment.enumDescriptions.AzureUSGovernment": "Azure US Government", + "microsoft-sovereign-cloud.environment.enumDescriptions.custom": "A custom Microsoft Sovereign Cloud", + "microsoft-sovereign-cloud.customEnvironment.description": { + "message": "The custom configuration for the Sovereign Cloud to use with the Microsoft Sovereign Cloud authentication provider. This along with setting `#microsoft-sovereign-cloud.environment#` to `custom` is required to use this feature.", + "comment": [ + "{Locked='`#microsoft-sovereign-cloud.environment#`'}", + "The `#microsoft-sovereign-cloud.environment#` syntax will turn into a link. Do not translate it." + ] + }, + "microsoft-sovereign-cloud.customEnvironment.name.description": "The name of the custom Sovereign Cloud.", + "microsoft-sovereign-cloud.customEnvironment.portalUrl.description": "The portal URL for the custom Sovereign Cloud.", + "microsoft-sovereign-cloud.customEnvironment.managementEndpointUrl.description": "The management endpoint for the custom Sovereign Cloud.", + "microsoft-sovereign-cloud.customEnvironment.resourceManagerEndpointUrl.description": "The resource manager endpoint for the custom Sovereign Cloud.", + "microsoft-sovereign-cloud.customEnvironment.activeDirectoryEndpointUrl.description": "The Active Directory endpoint for the custom Sovereign Cloud.", + "microsoft-sovereign-cloud.customEnvironment.activeDirectoryResourceId.description": "The Active Directory resource ID for the custom Sovereign Cloud." } diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 9d77be2a4f8..187cbf21a48 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as querystring from 'querystring'; import * as path from 'path'; import { isSupportedEnvironment } from './utils'; import { generateCodeChallenge, generateCodeVerifier, randomUUID } from './cryptoUtils'; @@ -14,9 +13,10 @@ import { base64Decode } from './node/buffer'; import { fetching } from './node/fetch'; import { UriEventHandler } from './UriEventHandler'; import TelemetryReporter from '@vscode/extension-telemetry'; +import { Environment } from '@azure/ms-rest-azure-env'; const redirectUrl = 'https://vscode.dev/redirect'; -const defaultLoginEndpointUrl = 'https://login.microsoftonline.com/'; +const defaultActiveDirectoryEndpointUrl = Environment.AzureCloud.activeDirectoryEndpointUrl; const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56'; const DEFAULT_TENANT = 'organizations'; const MSA_TID = '9188040d-6c67-4c5b-b112-36a304b66dad'; @@ -102,7 +102,7 @@ export class AzureActiveDirectoryService { private readonly _uriHandler: UriEventHandler, private readonly _tokenStorage: BetterTokenStorage, private readonly _telemetryReporter: TelemetryReporter, - private readonly _loginEndpointUrl: string = defaultLoginEndpointUrl + private readonly _env: Environment ) { _context.subscriptions.push(this._tokenStorage.onDidChangeInOtherWindow((e) => this.checkForUpdates(e))); } @@ -301,7 +301,7 @@ export class AzureActiveDirectoryService { const runsRemote = vscode.env.remoteName !== undefined; const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web; - if (runsServerless && this._loginEndpointUrl !== defaultLoginEndpointUrl) { + if (runsServerless && this._env.activeDirectoryEndpointUrl !== defaultActiveDirectoryEndpointUrl) { throw new Error('Sign in to non-public clouds is not supported on the web.'); } @@ -338,7 +338,7 @@ export class AzureActiveDirectoryService { code_challenge_method: 'S256', code_challenge: codeChallenge, }).toString(); - const loginUrl = `${this._loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`; + const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`, this._env.activeDirectoryEndpointUrl).toString(); const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl); await server.start(); @@ -368,8 +368,8 @@ export class AzureActiveDirectoryService { const state = encodeURIComponent(callbackUri.toString(true)); const codeVerifier = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); - const signInUrl = `${this._loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize`; - const oauthStartQuery = new URLSearchParams({ + const signInUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize`, this._env.activeDirectoryEndpointUrl); + signInUrl.search = new URLSearchParams({ response_type: 'code', client_id: encodeURIComponent(scopeData.clientId), response_mode: 'query', @@ -379,8 +379,8 @@ export class AzureActiveDirectoryService { prompt: 'select_account', code_challenge_method: 'S256', code_challenge: codeChallenge, - }); - const uri = vscode.Uri.parse(`${signInUrl}?${oauthStartQuery.toString()}`); + }).toString(); + const uri = vscode.Uri.parse(signInUrl.toString()); vscode.env.openExternal(uri); let inputBox: vscode.InputBox | undefined; @@ -601,19 +601,15 @@ export class AzureActiveDirectoryService { private async doRefreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise { this._logger.info(`Refreshing token for scopes: ${scopeData.scopeStr}`); - const postData = querystring.stringify({ + const postData = new URLSearchParams({ refresh_token: refreshToken, client_id: scopeData.clientId, grant_type: 'refresh_token', scope: scopeData.scopesToSend - }); - - const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); - const endpointUrl = proxyEndpoints?.microsoft || this._loginEndpointUrl; - const endpoint = `${endpointUrl}${scopeData.tenant}/oauth2/v2.0/token`; + }).toString(); try { - const json = await this.fetchTokenResponse(endpoint, postData, scopeData); + const json = await this.fetchTokenResponse(postData, scopeData); const token = this.convertToTokenSync(json, scopeData, sessionId); if (token.expiresIn) { this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER); @@ -666,8 +662,9 @@ export class AzureActiveDirectoryService { return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => { uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => { try { - const query = querystring.parse(uri.query); - let { code, nonce } = query; + const query = new URLSearchParams(uri.query); + let code = query.get('code'); + let nonce = query.get('nonce'); if (Array.isArray(code)) { code = code[0]; } @@ -735,28 +732,16 @@ export class AzureActiveDirectoryService { this._logger.info(`Exchanging login code for token for scopes: ${scopeData.scopeStr}`); let token: IToken | undefined; try { - const postData = querystring.stringify({ + const postData = new URLSearchParams({ grant_type: 'authorization_code', code: code, client_id: scopeData.clientId, scope: scopeData.scopesToSend, code_verifier: codeVerifier, redirect_uri: redirectUrl - }); + }).toString(); - let endpointUrl: string; - - if (this._loginEndpointUrl !== defaultLoginEndpointUrl) { - // If this is for sovereign clouds, don't try using the proxy endpoint, which supports only public cloud - endpointUrl = this._loginEndpointUrl; - } else { - const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); - endpointUrl = proxyEndpoints?.microsoft || this._loginEndpointUrl; - } - - const endpoint = `${endpointUrl}${scopeData.tenant}/oauth2/v2.0/token`; - - const json = await this.fetchTokenResponse(endpoint, postData, scopeData); + const json = await this.fetchTokenResponse(postData, scopeData); this._logger.info(`Exchanging login code for token (for scopes: ${scopeData.scopeStr}) succeeded!`); token = this.convertToTokenSync(json, scopeData); } catch (e) { @@ -772,7 +757,17 @@ export class AzureActiveDirectoryService { return await this.convertToSession(token, scopeData); } - private async fetchTokenResponse(endpoint: string, postData: string, scopeData: IScopeData): Promise { + private async fetchTokenResponse(postData: string, scopeData: IScopeData): Promise { + let endpointUrl: string; + if (this._env.activeDirectoryEndpointUrl !== defaultActiveDirectoryEndpointUrl) { + // If this is for sovereign clouds, don't try using the proxy endpoint, which supports only public cloud + endpointUrl = this._env.activeDirectoryEndpointUrl; + } else { + const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); + endpointUrl = proxyEndpoints?.microsoft || this._env.activeDirectoryEndpointUrl; + } + const endpoint = new URL(`${scopeData.tenant}/oauth2/v2.0/token`, endpointUrl); + let attempts = 0; while (attempts <= 3) { attempts++; @@ -869,7 +864,7 @@ export class AzureActiveDirectoryService { refreshToken: token.refreshToken, scope: token.scope, account: token.account, - endpoint: this._loginEndpointUrl, + endpoint: this._env.activeDirectoryEndpointUrl, }); this._logger.info(`Stored token for scopes: ${scopeData.scopeStr}`); } @@ -933,9 +928,9 @@ export class AzureActiveDirectoryService { private sessionMatchesEndpoint(session: IStoredSession): boolean { // For older sessions with no endpoint set, it can be assumed to be the default endpoint - session.endpoint ||= defaultLoginEndpointUrl; + session.endpoint ||= defaultActiveDirectoryEndpointUrl; - return session.endpoint === this._loginEndpointUrl; + return session.endpoint === this._env.activeDirectoryEndpointUrl; } //#endregion diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 121872f260f..767c0a17963 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -4,36 +4,46 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { Environment, EnvironmentParameters } from '@azure/ms-rest-azure-env'; import { AzureActiveDirectoryService, IStoredSession } from './AADHelper'; import { BetterTokenStorage } from './betterSecretStorage'; import { UriEventHandler } from './UriEventHandler'; import TelemetryReporter from '@vscode/extension-telemetry'; async function initMicrosoftSovereignCloudAuthProvider(context: vscode.ExtensionContext, telemetryReporter: TelemetryReporter, uriHandler: UriEventHandler, tokenStorage: BetterTokenStorage): Promise { - let settingValue = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get('endpoint'); + const environment = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get('environment'); let authProviderName: string | undefined; - if (!settingValue) { + if (!environment) { return undefined; - } else if (settingValue === 'Azure China') { - authProviderName = settingValue; - settingValue = 'https://login.chinacloudapi.cn/'; - } else if (settingValue === 'Azure US Government') { - authProviderName = settingValue; - settingValue = 'https://login.microsoftonline.us/'; } - // validate user value - let uri: vscode.Uri; - try { - uri = vscode.Uri.parse(settingValue, true); - } catch (e) { - vscode.window.showErrorMessage(vscode.l10n.t('Microsoft Sovereign Cloud login URI is not a valid URI: {0}', e.message ?? e)); - return; + if (environment === 'custom') { + const customEnv = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get('customEnvironment'); + if (!customEnv) { + const res = await vscode.window.showErrorMessage(vscode.l10n.t('You must also specify a custom environment in order to use the custom environment auth provider.'), vscode.l10n.t('Open settings')); + if (res) { + await vscode.commands.executeCommand('workbench.action.openSettingsJson', 'microsoft-sovereign-cloud.customEnvironment'); + } + return undefined; + } + try { + Environment.add(customEnv); + } catch (e) { + const res = await vscode.window.showErrorMessage(vscode.l10n.t('Error validating custom environment setting: {0}', e.message), vscode.l10n.t('Open settings')); + if (res) { + await vscode.commands.executeCommand('workbench.action.openSettings', 'microsoft-sovereign-cloud.customEnvironment'); + } + return undefined; + } + authProviderName = customEnv.name; + } else { + authProviderName = environment; } - // Add trailing slash if needed - if (!settingValue.endsWith('/')) { - settingValue += '/'; + const env = Environment.get(authProviderName); + if (!env) { + const res = await vscode.window.showErrorMessage(vscode.l10n.t('The environment `{0}` is not a valid environment.', authProviderName), vscode.l10n.t('Open settings')); + return undefined; } const aadService = new AzureActiveDirectoryService( @@ -42,10 +52,9 @@ async function initMicrosoftSovereignCloudAuthProvider(context: vscode.Extension uriHandler, tokenStorage, telemetryReporter, - settingValue); + env); await aadService.initialize(); - authProviderName ||= uri.authority; const disposable = vscode.authentication.registerAuthenticationProvider('microsoft-sovereign-cloud', authProviderName, { onDidChangeSessions: aadService.onDidChangeSessions, getSessions: (scopes: string[]) => aadService.getSessions(scopes), @@ -108,7 +117,8 @@ export async function activate(context: vscode.ExtensionContext) { context, uriHandler, betterSecretStorage, - telemetryReporter); + telemetryReporter, + Environment.AzureCloud); await loginService.initialize(); context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', { @@ -158,7 +168,7 @@ export async function activate(context: vscode.ExtensionContext) { let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage); context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => { - if (e.affectsConfiguration('microsoft-sovereign-cloud.endpoint')) { + if (e.affectsConfiguration('microsoft-sovereign-cloud')) { microsoftSovereignCloudAuthProviderDisposable?.dispose(); microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage); } diff --git a/extensions/microsoft-authentication/yarn.lock b/extensions/microsoft-authentication/yarn.lock index 5513eb3e992..5c62a11cfa4 100644 --- a/extensions/microsoft-authentication/yarn.lock +++ b/extensions/microsoft-authentication/yarn.lock @@ -55,6 +55,11 @@ dependencies: tslib "^2.2.0" +"@azure/ms-rest-azure-env@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@azure/ms-rest-azure-env/-/ms-rest-azure-env-2.0.0.tgz#45809f89763a480924e21d3c620cd40866771625" + integrity sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw== + "@microsoft/1ds-core-js@3.2.8", "@microsoft/1ds-core-js@^3.2.8": version "3.2.8" resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.8.tgz#1b6b7d9bb858238c818ccf4e4b58ece7aeae5760" @@ -370,11 +375,6 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -querystring-es3@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA== - semver@^5.3.0, semver@^5.4.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"