mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 04:09:28 +00:00
Support PKCE for GitHub Auth (#265381)
Fixes https://github.com/microsoft/vscode/issues/264795
This commit is contained in:
committed by
GitHub
parent
4407c1e0b3
commit
71b461ab86
@@ -9,6 +9,7 @@ import { Log } from './common/logger';
|
|||||||
import { Config } from './config';
|
import { Config } from './config';
|
||||||
import { UriEventHandler } from './github';
|
import { UriEventHandler } from './github';
|
||||||
import { fetching } from './node/fetch';
|
import { fetching } from './node/fetch';
|
||||||
|
import { crypto } from './node/crypto';
|
||||||
import { LoopbackAuthServer } from './node/authServer';
|
import { LoopbackAuthServer } from './node/authServer';
|
||||||
import { promiseFromEvent } from './common/utils';
|
import { promiseFromEvent } from './common/utils';
|
||||||
import { isHostedGitHubEnterprise } from './common/env';
|
import { isHostedGitHubEnterprise } from './common/env';
|
||||||
@@ -112,11 +113,44 @@ interface IFlow {
|
|||||||
trigger(options: IFlowTriggerOptions): Promise<string>;
|
trigger(options: IFlowTriggerOptions): Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a cryptographically secure random string for PKCE code verifier.
|
||||||
|
* @param length The length of the string to generate
|
||||||
|
* @returns A random hex string
|
||||||
|
*/
|
||||||
|
function generateRandomString(length: number): string {
|
||||||
|
const array = new Uint8Array(length);
|
||||||
|
crypto.getRandomValues(array);
|
||||||
|
return Array.from(array)
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('')
|
||||||
|
.substring(0, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a PKCE code challenge from a code verifier using SHA-256.
|
||||||
|
* @param codeVerifier The code verifier string
|
||||||
|
* @returns A base64url-encoded SHA-256 hash of the code verifier
|
||||||
|
*/
|
||||||
|
async function generateCodeChallenge(codeVerifier: string): Promise<string> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(codeVerifier);
|
||||||
|
const digest = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
|
||||||
|
// Base64url encode the digest
|
||||||
|
const base64String = btoa(String.fromCharCode(...new Uint8Array(digest)));
|
||||||
|
return base64String
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_')
|
||||||
|
.replace(/=+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
async function exchangeCodeForToken(
|
async function exchangeCodeForToken(
|
||||||
logger: Log,
|
logger: Log,
|
||||||
endpointUri: Uri,
|
endpointUri: Uri,
|
||||||
redirectUri: Uri,
|
redirectUri: Uri,
|
||||||
code: string,
|
code: string,
|
||||||
|
codeVerifier: string,
|
||||||
enterpriseUri?: Uri
|
enterpriseUri?: Uri
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
logger.info('Exchanging code for token...');
|
logger.info('Exchanging code for token...');
|
||||||
@@ -130,7 +164,8 @@ async function exchangeCodeForToken(
|
|||||||
['code', code],
|
['code', code],
|
||||||
['client_id', Config.gitHubClientId],
|
['client_id', Config.gitHubClientId],
|
||||||
['redirect_uri', redirectUri.toString(true)],
|
['redirect_uri', redirectUri.toString(true)],
|
||||||
['client_secret', clientSecret]
|
['client_secret', clientSecret],
|
||||||
|
['code_verifier', codeVerifier]
|
||||||
]);
|
]);
|
||||||
if (enterpriseUri) {
|
if (enterpriseUri) {
|
||||||
body.append('github_enterprise', enterpriseUri.toString(true));
|
body.append('github_enterprise', enterpriseUri.toString(true));
|
||||||
@@ -199,13 +234,19 @@ class UrlHandlerFlow implements IFlow {
|
|||||||
}),
|
}),
|
||||||
cancellable: true
|
cancellable: true
|
||||||
}, async (_, token) => {
|
}, async (_, token) => {
|
||||||
|
// Generate PKCE parameters
|
||||||
|
const codeVerifier = generateRandomString(64);
|
||||||
|
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
||||||
|
|
||||||
const promise = uriHandler.waitForCode(logger, scopes, nonce, token);
|
const promise = uriHandler.waitForCode(logger, scopes, nonce, token);
|
||||||
|
|
||||||
const searchParams = new URLSearchParams([
|
const searchParams = new URLSearchParams([
|
||||||
['client_id', Config.gitHubClientId],
|
['client_id', Config.gitHubClientId],
|
||||||
['redirect_uri', redirectUri.toString(true)],
|
['redirect_uri', redirectUri.toString(true)],
|
||||||
['scope', scopes],
|
['scope', scopes],
|
||||||
['state', encodeURIComponent(callbackUri.toString(true))]
|
['state', encodeURIComponent(callbackUri.toString(true))],
|
||||||
|
['code_challenge', codeChallenge],
|
||||||
|
['code_challenge_method', 'S256']
|
||||||
]);
|
]);
|
||||||
if (existingLogin) {
|
if (existingLogin) {
|
||||||
searchParams.append('login', existingLogin);
|
searchParams.append('login', existingLogin);
|
||||||
@@ -236,7 +277,7 @@ class UrlHandlerFlow implements IFlow {
|
|||||||
? Uri.parse(`${proxyEndpoints.github}login/oauth/access_token`)
|
? Uri.parse(`${proxyEndpoints.github}login/oauth/access_token`)
|
||||||
: baseUri.with({ path: '/login/oauth/access_token' });
|
: baseUri.with({ path: '/login/oauth/access_token' });
|
||||||
|
|
||||||
const accessToken = await exchangeCodeForToken(logger, endpointUrl, redirectUri, code, enterpriseUri);
|
const accessToken = await exchangeCodeForToken(logger, endpointUrl, redirectUri, code, codeVerifier, enterpriseUri);
|
||||||
return accessToken;
|
return accessToken;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -283,10 +324,16 @@ class LocalServerFlow implements IFlow {
|
|||||||
}),
|
}),
|
||||||
cancellable: true
|
cancellable: true
|
||||||
}, async (_, token) => {
|
}, async (_, token) => {
|
||||||
|
// Generate PKCE parameters
|
||||||
|
const codeVerifier = generateRandomString(64);
|
||||||
|
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
||||||
|
|
||||||
const searchParams = new URLSearchParams([
|
const searchParams = new URLSearchParams([
|
||||||
['client_id', Config.gitHubClientId],
|
['client_id', Config.gitHubClientId],
|
||||||
['redirect_uri', redirectUri.toString(true)],
|
['redirect_uri', redirectUri.toString(true)],
|
||||||
['scope', scopes],
|
['scope', scopes],
|
||||||
|
['code_challenge', codeChallenge],
|
||||||
|
['code_challenge_method', 'S256']
|
||||||
]);
|
]);
|
||||||
if (existingLogin) {
|
if (existingLogin) {
|
||||||
searchParams.append('login', existingLogin);
|
searchParams.append('login', existingLogin);
|
||||||
@@ -329,6 +376,7 @@ class LocalServerFlow implements IFlow {
|
|||||||
baseUri.with({ path: '/login/oauth/access_token' }),
|
baseUri.with({ path: '/login/oauth/access_token' }),
|
||||||
redirectUri,
|
redirectUri,
|
||||||
codeToExchange,
|
codeToExchange,
|
||||||
|
codeVerifier,
|
||||||
enterpriseUri);
|
enterpriseUri);
|
||||||
return accessToken;
|
return accessToken;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user