depend on redirect route within Microsoft auth (#145263)

* depend on redirect service

* better server query parameter handling
This commit is contained in:
Tyler James Leonhardt
2022-03-16 13:41:44 -07:00
committed by GitHub
parent 429a9d8ff1
commit 00888f774d
2 changed files with 55 additions and 33 deletions

View File

@@ -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<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
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<string, string[]>();
private _pendingNonces = new Map<string, string[]>();
private _codeExchangePromises = new Map<string, Promise<vscode.AuthenticationSession>>();
private _codeVerfifiers = new Map<string, string>();
@@ -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<vscode.AuthenticationSession> {
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');
}

View File

@@ -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 => {