From 71b461ab86725332849783d7d2aa0093789df77c Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Fri, 5 Sep 2025 07:55:35 -1000 Subject: [PATCH] Support PKCE for GitHub Auth (#265381) Fixes https://github.com/microsoft/vscode/issues/264795 --- extensions/github-authentication/src/flows.ts | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 6e37dfe5dfd..d9d286bcbbc 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -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; } +/** + * 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 { + 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 { 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; });