Skip to content

Commit dd2441f

Browse files
Polish Sovereign Cloud support (microsoft#184634)
* Use `@azure/ms-rest-azure-env` as official reference of endpoints * Allow better configuration of custom clouds (these are new so it is ok to change the settings without migration) Also clean up: * querystring -> URLSearchParams (getting rid of a package dependency in the web) * handle `workbench.getCodeExchangeProxyEndpoints` in one place
1 parent 1ea4242 commit dd2441f

File tree

6 files changed

+143
-81
lines changed

6 files changed

+143
-81
lines changed

extensions/microsoft-authentication/extension-browser.webpack.config.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ module.exports = withBrowserDefaults({
2424
'keytar': 'commonjs keytar',
2525
},
2626
resolve: {
27-
fallback: {
28-
'querystring': require.resolve('querystring-es3')
29-
},
3027
alias: {
3128
'./node/crypto': path.resolve(__dirname, 'src/browser/crypto'),
3229
'./node/authServer': path.resolve(__dirname, 'src/browser/authServer'),

extensions/microsoft-authentication/package.json

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,58 @@
4141
{
4242
"title": "Microsoft Sovereign Cloud",
4343
"properties": {
44-
"microsoft-sovereign-cloud.endpoint": {
45-
"anyOf": [
46-
{
47-
"type": "string"
44+
"microsoft-sovereign-cloud.environment": {
45+
"type": "string",
46+
"markdownDescription": "%microsoft-sovereign-cloud.environment.description%",
47+
"enum": [
48+
"ChinaCloud",
49+
"USGovernment",
50+
"custom"
51+
],
52+
"enumDescriptions": [
53+
"%microsoft-sovereign-cloud.environment.enumDescriptions.AzureChinaCloud%",
54+
"%microsoft-sovereign-cloud.environment.enumDescriptions.AzureUSGovernment%",
55+
"%microsoft-sovereign-cloud.environment.enumDescriptions.custom%"
56+
]
57+
},
58+
"microsoft-sovereign-cloud.customEnvironment": {
59+
"type": "object",
60+
"additionalProperties": true,
61+
"markdownDescription": "%microsoft-sovereign-cloud.customEnvironment.description%",
62+
"properties": {
63+
"name": {
64+
"type": "string",
65+
"description": "%microsoft-sovereign-cloud.customEnvironment.name.description%"
66+
},
67+
"portalUrl": {
68+
"type": "string",
69+
"description": "%microsoft-sovereign-cloud.customEnvironment.portalUrl.description%"
70+
},
71+
"managementEndpointUrl": {
72+
"type": "string",
73+
"description": "%microsoft-sovereign-cloud.customEnvironment.managementEndpointUrl.description%"
4874
},
49-
{
75+
"resourceManagerEndpointUrl": {
5076
"type": "string",
51-
"enum": [
52-
"Azure China",
53-
"Azure US Government"
54-
]
77+
"description": "%microsoft-sovereign-cloud.customEnvironment.resourceManagerEndpointUrl.description%"
78+
},
79+
"activeDirectoryEndpointUrl": {
80+
"type": "string",
81+
"description": "%microsoft-sovereign-cloud.customEnvironment.activeDirectoryEndpointUrl.description%"
82+
},
83+
"activeDirectoryResourceId": {
84+
"type": "string",
85+
"description": "%microsoft-sovereign-cloud.customEnvironment.activeDirectoryResourceId.description%"
5586
}
56-
],
57-
"description": "%microsoft-sovereign-cloud.endpoint.description%"
87+
},
88+
"required": [
89+
"name",
90+
"portalUrl",
91+
"managementEndpointUrl",
92+
"resourceManagerEndpointUrl",
93+
"activeDirectoryEndpointUrl",
94+
"activeDirectoryResourceId"
95+
]
5896
}
5997
}
6098
}
@@ -75,11 +113,11 @@
75113
"@types/node-fetch": "^2.5.7",
76114
"@types/randombytes": "^2.0.0",
77115
"@types/sha.js": "^2.4.0",
78-
"@types/uuid": "8.0.0",
79-
"querystring-es3": "^0.2.1"
116+
"@types/uuid": "8.0.0"
80117
},
81118
"dependencies": {
82119
"node-fetch": "2.6.7",
120+
"@azure/ms-rest-azure-env": "^2.0.0",
83121
"@vscode/extension-telemetry": "0.7.5"
84122
},
85123
"repository": {

extensions/microsoft-authentication/package.nls.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,27 @@
33
"description": "Microsoft authentication provider",
44
"signIn": "Sign In",
55
"signOut": "Sign Out",
6-
"microsoft-sovereign-cloud.endpoint.description": "Login endpoint for Azure authentication. Select a national cloud or enter the login URL for a custom Azure cloud."
6+
"microsoft-sovereign-cloud.environment.description": {
7+
"message": "The Sovereign Cloud to use for authentication. If you select `custom`, you must also set the `#microsoft-sovereign-cloud.customEnvironment#` setting.",
8+
"comment": [
9+
"{Locked='`#microsoft-sovereign-cloud.customEnvironment#`'}",
10+
"The `#microsoft-sovereign-cloud.customEnvironment#` syntax will turn into a link. Do not translate it."
11+
]
12+
},
13+
"microsoft-sovereign-cloud.environment.enumDescriptions.AzureChinaCloud": "Azure China",
14+
"microsoft-sovereign-cloud.environment.enumDescriptions.AzureUSGovernment": "Azure US Government",
15+
"microsoft-sovereign-cloud.environment.enumDescriptions.custom": "A custom Microsoft Sovereign Cloud",
16+
"microsoft-sovereign-cloud.customEnvironment.description": {
17+
"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.",
18+
"comment": [
19+
"{Locked='`#microsoft-sovereign-cloud.environment#`'}",
20+
"The `#microsoft-sovereign-cloud.environment#` syntax will turn into a link. Do not translate it."
21+
]
22+
},
23+
"microsoft-sovereign-cloud.customEnvironment.name.description": "The name of the custom Sovereign Cloud.",
24+
"microsoft-sovereign-cloud.customEnvironment.portalUrl.description": "The portal URL for the custom Sovereign Cloud.",
25+
"microsoft-sovereign-cloud.customEnvironment.managementEndpointUrl.description": "The management endpoint for the custom Sovereign Cloud.",
26+
"microsoft-sovereign-cloud.customEnvironment.resourceManagerEndpointUrl.description": "The resource manager endpoint for the custom Sovereign Cloud.",
27+
"microsoft-sovereign-cloud.customEnvironment.activeDirectoryEndpointUrl.description": "The Active Directory endpoint for the custom Sovereign Cloud.",
28+
"microsoft-sovereign-cloud.customEnvironment.activeDirectoryResourceId.description": "The Active Directory resource ID for the custom Sovereign Cloud."
729
}

extensions/microsoft-authentication/src/AADHelper.ts

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import * as querystring from 'querystring';
87
import * as path from 'path';
98
import { isSupportedEnvironment } from './utils';
109
import { generateCodeChallenge, generateCodeVerifier, randomUUID } from './cryptoUtils';
@@ -14,9 +13,10 @@ import { base64Decode } from './node/buffer';
1413
import { fetching } from './node/fetch';
1514
import { UriEventHandler } from './UriEventHandler';
1615
import TelemetryReporter from '@vscode/extension-telemetry';
16+
import { Environment } from '@azure/ms-rest-azure-env';
1717

1818
const redirectUrl = 'https://vscode.dev/redirect';
19-
const defaultLoginEndpointUrl = 'https://login.microsoftonline.com/';
19+
const defaultActiveDirectoryEndpointUrl = Environment.AzureCloud.activeDirectoryEndpointUrl;
2020
const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56';
2121
const DEFAULT_TENANT = 'organizations';
2222
const MSA_TID = '9188040d-6c67-4c5b-b112-36a304b66dad';
@@ -102,7 +102,7 @@ export class AzureActiveDirectoryService {
102102
private readonly _uriHandler: UriEventHandler,
103103
private readonly _tokenStorage: BetterTokenStorage<IStoredSession>,
104104
private readonly _telemetryReporter: TelemetryReporter,
105-
private readonly _loginEndpointUrl: string = defaultLoginEndpointUrl
105+
private readonly _env: Environment
106106
) {
107107
_context.subscriptions.push(this._tokenStorage.onDidChangeInOtherWindow((e) => this.checkForUpdates(e)));
108108
}
@@ -301,7 +301,7 @@ export class AzureActiveDirectoryService {
301301
const runsRemote = vscode.env.remoteName !== undefined;
302302
const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web;
303303

304-
if (runsServerless && this._loginEndpointUrl !== defaultLoginEndpointUrl) {
304+
if (runsServerless && this._env.activeDirectoryEndpointUrl !== defaultActiveDirectoryEndpointUrl) {
305305
throw new Error('Sign in to non-public clouds is not supported on the web.');
306306
}
307307

@@ -338,7 +338,7 @@ export class AzureActiveDirectoryService {
338338
code_challenge_method: 'S256',
339339
code_challenge: codeChallenge,
340340
}).toString();
341-
const loginUrl = `${this._loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`;
341+
const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`, this._env.activeDirectoryEndpointUrl).toString();
342342
const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl);
343343
await server.start();
344344

@@ -368,8 +368,8 @@ export class AzureActiveDirectoryService {
368368
const state = encodeURIComponent(callbackUri.toString(true));
369369
const codeVerifier = generateCodeVerifier();
370370
const codeChallenge = await generateCodeChallenge(codeVerifier);
371-
const signInUrl = `${this._loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize`;
372-
const oauthStartQuery = new URLSearchParams({
371+
const signInUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize`, this._env.activeDirectoryEndpointUrl);
372+
signInUrl.search = new URLSearchParams({
373373
response_type: 'code',
374374
client_id: encodeURIComponent(scopeData.clientId),
375375
response_mode: 'query',
@@ -379,8 +379,8 @@ export class AzureActiveDirectoryService {
379379
prompt: 'select_account',
380380
code_challenge_method: 'S256',
381381
code_challenge: codeChallenge,
382-
});
383-
const uri = vscode.Uri.parse(`${signInUrl}?${oauthStartQuery.toString()}`);
382+
}).toString();
383+
const uri = vscode.Uri.parse(signInUrl.toString());
384384
vscode.env.openExternal(uri);
385385

386386
let inputBox: vscode.InputBox | undefined;
@@ -601,19 +601,15 @@ export class AzureActiveDirectoryService {
601601

602602
private async doRefreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise<IToken> {
603603
this._logger.info(`Refreshing token for scopes: ${scopeData.scopeStr}`);
604-
const postData = querystring.stringify({
604+
const postData = new URLSearchParams({
605605
refresh_token: refreshToken,
606606
client_id: scopeData.clientId,
607607
grant_type: 'refresh_token',
608608
scope: scopeData.scopesToSend
609-
});
610-
611-
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
612-
const endpointUrl = proxyEndpoints?.microsoft || this._loginEndpointUrl;
613-
const endpoint = `${endpointUrl}${scopeData.tenant}/oauth2/v2.0/token`;
609+
}).toString();
614610

615611
try {
616-
const json = await this.fetchTokenResponse(endpoint, postData, scopeData);
612+
const json = await this.fetchTokenResponse(postData, scopeData);
617613
const token = this.convertToTokenSync(json, scopeData, sessionId);
618614
if (token.expiresIn) {
619615
this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER);
@@ -666,8 +662,9 @@ export class AzureActiveDirectoryService {
666662
return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => {
667663
uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => {
668664
try {
669-
const query = querystring.parse(uri.query);
670-
let { code, nonce } = query;
665+
const query = new URLSearchParams(uri.query);
666+
let code = query.get('code');
667+
let nonce = query.get('nonce');
671668
if (Array.isArray(code)) {
672669
code = code[0];
673670
}
@@ -735,28 +732,16 @@ export class AzureActiveDirectoryService {
735732
this._logger.info(`Exchanging login code for token for scopes: ${scopeData.scopeStr}`);
736733
let token: IToken | undefined;
737734
try {
738-
const postData = querystring.stringify({
735+
const postData = new URLSearchParams({
739736
grant_type: 'authorization_code',
740737
code: code,
741738
client_id: scopeData.clientId,
742739
scope: scopeData.scopesToSend,
743740
code_verifier: codeVerifier,
744741
redirect_uri: redirectUrl
745-
});
746-
747-
let endpointUrl: string;
742+
}).toString();
748743

749-
if (this._loginEndpointUrl !== defaultLoginEndpointUrl) {
750-
// If this is for sovereign clouds, don't try using the proxy endpoint, which supports only public cloud
751-
endpointUrl = this._loginEndpointUrl;
752-
} else {
753-
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
754-
endpointUrl = proxyEndpoints?.microsoft || this._loginEndpointUrl;
755-
}
756-
757-
const endpoint = `${endpointUrl}${scopeData.tenant}/oauth2/v2.0/token`;
758-
759-
const json = await this.fetchTokenResponse(endpoint, postData, scopeData);
744+
const json = await this.fetchTokenResponse(postData, scopeData);
760745
this._logger.info(`Exchanging login code for token (for scopes: ${scopeData.scopeStr}) succeeded!`);
761746
token = this.convertToTokenSync(json, scopeData);
762747
} catch (e) {
@@ -772,7 +757,17 @@ export class AzureActiveDirectoryService {
772757
return await this.convertToSession(token, scopeData);
773758
}
774759

775-
private async fetchTokenResponse(endpoint: string, postData: string, scopeData: IScopeData): Promise<ITokenResponse> {
760+
private async fetchTokenResponse(postData: string, scopeData: IScopeData): Promise<ITokenResponse> {
761+
let endpointUrl: string;
762+
if (this._env.activeDirectoryEndpointUrl !== defaultActiveDirectoryEndpointUrl) {
763+
// If this is for sovereign clouds, don't try using the proxy endpoint, which supports only public cloud
764+
endpointUrl = this._env.activeDirectoryEndpointUrl;
765+
} else {
766+
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
767+
endpointUrl = proxyEndpoints?.microsoft || this._env.activeDirectoryEndpointUrl;
768+
}
769+
const endpoint = new URL(`${scopeData.tenant}/oauth2/v2.0/token`, endpointUrl);
770+
776771
let attempts = 0;
777772
while (attempts <= 3) {
778773
attempts++;
@@ -869,7 +864,7 @@ export class AzureActiveDirectoryService {
869864
refreshToken: token.refreshToken,
870865
scope: token.scope,
871866
account: token.account,
872-
endpoint: this._loginEndpointUrl,
867+
endpoint: this._env.activeDirectoryEndpointUrl,
873868
});
874869
this._logger.info(`Stored token for scopes: ${scopeData.scopeStr}`);
875870
}
@@ -933,9 +928,9 @@ export class AzureActiveDirectoryService {
933928

934929
private sessionMatchesEndpoint(session: IStoredSession): boolean {
935930
// For older sessions with no endpoint set, it can be assumed to be the default endpoint
936-
session.endpoint ||= defaultLoginEndpointUrl;
931+
session.endpoint ||= defaultActiveDirectoryEndpointUrl;
937932

938-
return session.endpoint === this._loginEndpointUrl;
933+
return session.endpoint === this._env.activeDirectoryEndpointUrl;
939934
}
940935

941936
//#endregion

extensions/microsoft-authentication/src/extension.ts

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,46 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7+
import { Environment, EnvironmentParameters } from '@azure/ms-rest-azure-env';
78
import { AzureActiveDirectoryService, IStoredSession } from './AADHelper';
89
import { BetterTokenStorage } from './betterSecretStorage';
910
import { UriEventHandler } from './UriEventHandler';
1011
import TelemetryReporter from '@vscode/extension-telemetry';
1112

1213
async function initMicrosoftSovereignCloudAuthProvider(context: vscode.ExtensionContext, telemetryReporter: TelemetryReporter, uriHandler: UriEventHandler, tokenStorage: BetterTokenStorage<IStoredSession>): Promise<vscode.Disposable | undefined> {
13-
let settingValue = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get<string | undefined>('endpoint');
14+
const environment = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get<string | undefined>('environment');
1415
let authProviderName: string | undefined;
15-
if (!settingValue) {
16+
if (!environment) {
1617
return undefined;
17-
} else if (settingValue === 'Azure China') {
18-
authProviderName = settingValue;
19-
settingValue = 'https://login.chinacloudapi.cn/';
20-
} else if (settingValue === 'Azure US Government') {
21-
authProviderName = settingValue;
22-
settingValue = 'https://login.microsoftonline.us/';
2318
}
2419

25-
// validate user value
26-
let uri: vscode.Uri;
27-
try {
28-
uri = vscode.Uri.parse(settingValue, true);
29-
} catch (e) {
30-
vscode.window.showErrorMessage(vscode.l10n.t('Microsoft Sovereign Cloud login URI is not a valid URI: {0}', e.message ?? e));
31-
return;
20+
if (environment === 'custom') {
21+
const customEnv = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get<EnvironmentParameters>('customEnvironment');
22+
if (!customEnv) {
23+
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'));
24+
if (res) {
25+
await vscode.commands.executeCommand('workbench.action.openSettingsJson', 'microsoft-sovereign-cloud.customEnvironment');
26+
}
27+
return undefined;
28+
}
29+
try {
30+
Environment.add(customEnv);
31+
} catch (e) {
32+
const res = await vscode.window.showErrorMessage(vscode.l10n.t('Error validating custom environment setting: {0}', e.message), vscode.l10n.t('Open settings'));
33+
if (res) {
34+
await vscode.commands.executeCommand('workbench.action.openSettings', 'microsoft-sovereign-cloud.customEnvironment');
35+
}
36+
return undefined;
37+
}
38+
authProviderName = customEnv.name;
39+
} else {
40+
authProviderName = environment;
3241
}
3342

34-
// Add trailing slash if needed
35-
if (!settingValue.endsWith('/')) {
36-
settingValue += '/';
43+
const env = Environment.get(authProviderName);
44+
if (!env) {
45+
const res = await vscode.window.showErrorMessage(vscode.l10n.t('The environment `{0}` is not a valid environment.', authProviderName), vscode.l10n.t('Open settings'));
46+
return undefined;
3747
}
3848

3949
const aadService = new AzureActiveDirectoryService(
@@ -42,10 +52,9 @@ async function initMicrosoftSovereignCloudAuthProvider(context: vscode.Extension
4252
uriHandler,
4353
tokenStorage,
4454
telemetryReporter,
45-
settingValue);
55+
env);
4656
await aadService.initialize();
4757

48-
authProviderName ||= uri.authority;
4958
const disposable = vscode.authentication.registerAuthenticationProvider('microsoft-sovereign-cloud', authProviderName, {
5059
onDidChangeSessions: aadService.onDidChangeSessions,
5160
getSessions: (scopes: string[]) => aadService.getSessions(scopes),
@@ -108,7 +117,8 @@ export async function activate(context: vscode.ExtensionContext) {
108117
context,
109118
uriHandler,
110119
betterSecretStorage,
111-
telemetryReporter);
120+
telemetryReporter,
121+
Environment.AzureCloud);
112122
await loginService.initialize();
113123

114124
context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', {
@@ -158,7 +168,7 @@ export async function activate(context: vscode.ExtensionContext) {
158168
let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage);
159169

160170
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => {
161-
if (e.affectsConfiguration('microsoft-sovereign-cloud.endpoint')) {
171+
if (e.affectsConfiguration('microsoft-sovereign-cloud')) {
162172
microsoftSovereignCloudAuthProviderDisposable?.dispose();
163173
microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage);
164174
}

0 commit comments

Comments
 (0)