mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 18:49:00 +01:00
depend on redirect route within Microsoft auth (#145263)
* depend on redirect service * better server query parameter handling
This commit is contained in:
committed by
GitHub
parent
429a9d8ff1
commit
00888f774d
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user