diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 51b61396ab5..7c2daa082a3 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -17,10 +17,12 @@ import { sha256 } from './env/node/sha256'; import { BetterTokenStorage, IDidChangeInOtherWindowEvent } from './betterSecretStorage'; import { LoopbackAuthServer } from './authServer'; import path = require('path'); +import { URLSearchParams } from 'url'; const localize = nls.loadMessageBundle(); -const redirectUrl = 'https://vscode-redirect.azurewebsites.net/'; +// TODO: Change to stable when it's deployed. +const redirectUrl = 'https://insiders.vscode.dev/redirect'; const loginEndpointUrl = 'https://login.microsoftonline.com/'; const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56'; const DEFAULT_TENANT = 'organizations'; @@ -87,14 +89,6 @@ interface IScopeData { tenant: string; } -function parseQuery(uri: vscode.Uri) { - return uri.query.split('&').reduce((prev: any, current) => { - const queryString = current.split('='); - prev[queryString[0]] = queryString[1]; - return prev; - }, {}); -} - export const onDidChangeSessions = new vscode.EventEmitter(); export const REFRESH_NETWORK_FAILURE = 'Network failure'; @@ -115,7 +109,7 @@ export class AzureActiveDirectoryService { private _uriHandler: UriEventHandler; // Used to keep track of current requests when not using the local server approach. - private _pendingStates = new Map(); + private _pendingNonces = new Map(); private _codeExchangePromises = new Map>(); private _codeVerfifiers = new Map(); @@ -310,7 +304,7 @@ export class AzureActiveDirectoryService { private async createSessionWithLocalServer(scopeData: IScopeData) { const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64')); const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier)); - const qs = querystring.stringify({ + const qs = new URLSearchParams({ response_type: 'code', response_mode: 'query', client_id: scopeData.clientId, @@ -319,11 +313,10 @@ export class AzureActiveDirectoryService { prompt: 'select_account', code_challenge_method: 'S256', code_challenge: codeChallenge, - }); + }).toString(); const loginUrl = `${loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`; const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl); await server.start(); - server.state = `${server.port},${encodeURIComponent(server.nonce)}`; let codeToExchange; try { @@ -347,18 +340,29 @@ export class AzureActiveDirectoryService { } private async createSessionWithoutLocalServer(scopeData: IScopeData): Promise { - const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`)); + let callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`)); const nonce = randomBytes(16).toString('base64'); - const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80); - const callbackEnvironment = AzureActiveDirectoryService.getCallbackEnvironment(callbackUri); - const state = `${callbackEnvironment},${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`; - const signInUrl = `${loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize`; - let uri = vscode.Uri.parse(signInUrl); + const callbackQuery = new URLSearchParams(callbackUri.query); + callbackQuery.set('nonce', encodeURIComponent(nonce)); + callbackUri = callbackUri.with({ + query: callbackQuery.toString() + }); + const state = encodeURIComponent(callbackUri.toString(true)); const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64')); const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier)); - uri = uri.with({ - query: `response_type=code&client_id=${encodeURIComponent(scopeData.clientId)}&response_mode=query&redirect_uri=${redirectUrl}&state=${state}&scope=${scopeData.scopesToSend}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}` + const signInUrl = `${loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize`; + const oauthStartQuery = new URLSearchParams({ + response_type: 'code', + client_id: encodeURIComponent(scopeData.clientId), + response_mode: 'query', + redirect_uri: redirectUrl, + state, + scope: scopeData.scopesToSend, + prompt: 'select_account', + code_challenge_method: 'S256', + code_challenge: codeChallenge, }); + let uri = vscode.Uri.parse(`${signInUrl}?${oauthStartQuery.toString()}`); vscode.env.openExternal(uri); const timeoutPromise = new Promise((_: (value: vscode.AuthenticationSession) => void, reject) => { @@ -368,8 +372,8 @@ export class AzureActiveDirectoryService { }, 1000 * 60 * 5); }); - const existingStates = this._pendingStates.get(scopeData.scopeStr) || []; - this._pendingStates.set(scopeData.scopeStr, [...existingStates, state]); + const existingNonces = this._pendingNonces.get(scopeData.scopeStr) || []; + this._pendingNonces.set(scopeData.scopeStr, [...existingNonces, nonce]); // Register a single listener for the URI callback, in case the user starts the login process multiple times // before completing it. @@ -379,13 +383,13 @@ export class AzureActiveDirectoryService { this._codeExchangePromises.set(scopeData.scopeStr, existingPromise); } - this._codeVerfifiers.set(state, codeVerifier); + this._codeVerfifiers.set(nonce, codeVerifier); return Promise.race([existingPromise, timeoutPromise]) .finally(() => { - this._pendingStates.delete(scopeData.scopeStr); + this._pendingNonces.delete(scopeData.scopeStr); this._codeExchangePromises.delete(scopeData.scopeStr); - this._codeVerfifiers.delete(state); + this._codeVerfifiers.delete(nonce); }); } @@ -630,15 +634,29 @@ export class AzureActiveDirectoryService { return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => { uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => { try { - const query = parseQuery(uri); - const code = query.code; - const acceptedStates = this._pendingStates.get(scopeData.scopeStr) || []; - // Workaround double encoding issues of state in web - if (!acceptedStates.includes(query.state) && !acceptedStates.includes(decodeURIComponent(query.state))) { - throw new Error('State does not match.'); + console.log(uri.query); + const query = querystring.parse(uri.query); + let { code, nonce } = query; + if (Array.isArray(code)) { + code = code[0]; + } + if (!code) { + throw new Error('No code included in query'); + } + if (Array.isArray(nonce)) { + nonce = nonce[0]; + } + if (!nonce) { + throw new Error('No nonce included in query'); } - const verifier = this._codeVerfifiers.get(query.state) ?? this._codeVerfifiers.get(decodeURIComponent(query.state)); + const acceptedStates = this._pendingNonces.get(scopeData.scopeStr) || []; + // Workaround double encoding issues of state in web + if (!acceptedStates.includes(nonce) && !acceptedStates.includes(decodeURIComponent(nonce))) { + throw new Error('Nonce does not match.'); + } + + const verifier = this._codeVerfifiers.get(nonce) ?? this._codeVerfifiers.get(decodeURIComponent(nonce)); if (!verifier) { throw new Error('No available code verifier'); } diff --git a/extensions/microsoft-authentication/src/authServer.ts b/extensions/microsoft-authentication/src/authServer.ts index 158d0db257f..c36e56175de 100644 --- a/extensions/microsoft-authentication/src/authServer.ts +++ b/extensions/microsoft-authentication/src/authServer.ts @@ -155,6 +155,10 @@ export class LoopbackAuthServer implements ILoopbackServer { } clearTimeout(portTimeout); + + // set state which will be used to redirect back to vscode + this.state = `http://127.0.0.1:${this.port}/callback?nonce=${encodeURIComponent(this.nonce)}`; + resolve(this.port); }); this._server.on('error', err => {