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 { 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;
}); });