Support PKCE for GitHub Auth (#265381)

Fixes https://github.com/microsoft/vscode/issues/264795
This commit is contained in:
Tyler James Leonhardt
2025-09-05 07:55:35 -10:00
committed by GitHub
parent 4407c1e0b3
commit 71b461ab86

View File

@@ -9,6 +9,7 @@ import { Log } from './common/logger';
import { Config } from './config';
import { UriEventHandler } from './github';
import { fetching } from './node/fetch';
import { crypto } from './node/crypto';
import { LoopbackAuthServer } from './node/authServer';
import { promiseFromEvent } from './common/utils';
import { isHostedGitHubEnterprise } from './common/env';
@@ -112,11 +113,44 @@ interface IFlow {
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(
logger: Log,
endpointUri: Uri,
redirectUri: Uri,
code: string,
codeVerifier: string,
enterpriseUri?: Uri
): Promise<string> {
logger.info('Exchanging code for token...');
@@ -130,7 +164,8 @@ async function exchangeCodeForToken(
['code', code],
['client_id', Config.gitHubClientId],
['redirect_uri', redirectUri.toString(true)],
['client_secret', clientSecret]
['client_secret', clientSecret],
['code_verifier', codeVerifier]
]);
if (enterpriseUri) {
body.append('github_enterprise', enterpriseUri.toString(true));
@@ -199,13 +234,19 @@ class UrlHandlerFlow implements IFlow {
}),
cancellable: true
}, async (_, token) => {
// Generate PKCE parameters
const codeVerifier = generateRandomString(64);
const codeChallenge = await generateCodeChallenge(codeVerifier);
const promise = uriHandler.waitForCode(logger, scopes, nonce, token);
const searchParams = new URLSearchParams([
['client_id', Config.gitHubClientId],
['redirect_uri', redirectUri.toString(true)],
['scope', scopes],
['state', encodeURIComponent(callbackUri.toString(true))]
['state', encodeURIComponent(callbackUri.toString(true))],
['code_challenge', codeChallenge],
['code_challenge_method', 'S256']
]);
if (existingLogin) {
searchParams.append('login', existingLogin);
@@ -236,7 +277,7 @@ class UrlHandlerFlow implements IFlow {
? Uri.parse(`${proxyEndpoints.github}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;
});
}
@@ -283,10 +324,16 @@ class LocalServerFlow implements IFlow {
}),
cancellable: true
}, async (_, token) => {
// Generate PKCE parameters
const codeVerifier = generateRandomString(64);
const codeChallenge = await generateCodeChallenge(codeVerifier);
const searchParams = new URLSearchParams([
['client_id', Config.gitHubClientId],
['redirect_uri', redirectUri.toString(true)],
['scope', scopes],
['code_challenge', codeChallenge],
['code_challenge_method', 'S256']
]);
if (existingLogin) {
searchParams.append('login', existingLogin);
@@ -329,6 +376,7 @@ class LocalServerFlow implements IFlow {
baseUri.with({ path: '/login/oauth/access_token' }),
redirectUri,
codeToExchange,
codeVerifier,
enterpriseUri);
return accessToken;
});