From b441e44372bc98289408577fb4d6f345c832a1be Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 9 Jan 2026 11:26:11 +0000 Subject: [PATCH] fix: use vscode.env.asExternalUri() for Claude Code OAuth in remote environments Fixes authentication in remote environments (GitHub Codespaces, etc.) by using vscode.env.asExternalUri() to dynamically convert the localhost redirect URI to an externally accessible URL. Changes: - Modified startAuthorizationFlow() to accept vscode.env and vscode.Uri.parse - Uses asExternalUri() to transform localhost:54545/callback to forwarded URL - Updated buildAuthorizationUrl() and exchangeCodeForTokens() to accept custom redirectUri - Added tests for custom redirect URI functionality Fixes #10531 --- src/core/webview/webviewMessageHandler.ts | 4 +- .../claude-code/__tests__/oauth.spec.ts | 25 ++++++++ src/integrations/claude-code/oauth.ts | 57 ++++++++++++++++--- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 0df014b49a..9d4304c531 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2343,7 +2343,9 @@ export const webviewMessageHandler = async ( case "claudeCodeSignIn": { try { const { claudeCodeOAuthManager } = await import("../../integrations/claude-code/oauth") - const authUrl = claudeCodeOAuthManager.startAuthorizationFlow() + // Pass vscode.env and vscode.Uri.parse to support remote environments (GitHub Codespaces, etc.) + // vscode.env.asExternalUri() will convert localhost URLs to forwarded URLs + const authUrl = await claudeCodeOAuthManager.startAuthorizationFlow(vscode.env, vscode.Uri.parse) // Open the authorization URL in the browser await vscode.env.openExternal(vscode.Uri.parse(authUrl)) diff --git a/src/integrations/claude-code/__tests__/oauth.spec.ts b/src/integrations/claude-code/__tests__/oauth.spec.ts index 7de75ec529..66bb19cd7d 100644 --- a/src/integrations/claude-code/__tests__/oauth.spec.ts +++ b/src/integrations/claude-code/__tests__/oauth.spec.ts @@ -137,6 +137,31 @@ describe("Claude Code OAuth", () => { expect(params.get("response_type")).toBe("code") expect(params.get("state")).toBe(state) }) + + test("should use custom redirect URI when provided (for remote environments)", () => { + const codeChallenge = "test-code-challenge" + const state = "test-state" + const customRedirectUri = "https://codespace-name-54545.app.github.dev/callback" + const url = buildAuthorizationUrl(codeChallenge, state, customRedirectUri) + + const parsedUrl = new URL(url) + const params = parsedUrl.searchParams + expect(params.get("redirect_uri")).toBe(customRedirectUri) + // Other parameters should still be correct + expect(params.get("client_id")).toBe(CLAUDE_CODE_OAUTH_CONFIG.clientId) + expect(params.get("code_challenge")).toBe(codeChallenge) + expect(params.get("state")).toBe(state) + }) + + test("should fall back to default redirect URI when custom is undefined", () => { + const codeChallenge = "test-code-challenge" + const state = "test-state" + const url = buildAuthorizationUrl(codeChallenge, state, undefined) + + const parsedUrl = new URL(url) + const params = parsedUrl.searchParams + expect(params.get("redirect_uri")).toBe(CLAUDE_CODE_OAUTH_CONFIG.redirectUri) + }) }) describe("isTokenExpired", () => { diff --git a/src/integrations/claude-code/oauth.ts b/src/integrations/claude-code/oauth.ts index 5d7a929e1c..9596183dd5 100644 --- a/src/integrations/claude-code/oauth.ts +++ b/src/integrations/claude-code/oauth.ts @@ -1,7 +1,7 @@ import * as crypto from "crypto" import * as http from "http" import { URL } from "url" -import type { ExtensionContext } from "vscode" +import type { ExtensionContext, Uri } from "vscode" import { z } from "zod" // OAuth Configuration @@ -147,11 +147,14 @@ export function generateUserId(email?: string): string { /** * Builds the authorization URL for OAuth flow + * @param codeChallenge PKCE code challenge + * @param state CSRF protection state + * @param redirectUri The redirect URI to use (may be externally mapped for remote environments) */ -export function buildAuthorizationUrl(codeChallenge: string, state: string): string { +export function buildAuthorizationUrl(codeChallenge: string, state: string, redirectUri?: string): string { const params = new URLSearchParams({ client_id: CLAUDE_CODE_OAUTH_CONFIG.clientId, - redirect_uri: CLAUDE_CODE_OAUTH_CONFIG.redirectUri, + redirect_uri: redirectUri ?? CLAUDE_CODE_OAUTH_CONFIG.redirectUri, scope: CLAUDE_CODE_OAUTH_CONFIG.scopes, code_challenge: codeChallenge, code_challenge_method: "S256", @@ -164,18 +167,23 @@ export function buildAuthorizationUrl(codeChallenge: string, state: string): str /** * Exchanges the authorization code for tokens + * @param code The authorization code + * @param codeVerifier The PKCE code verifier + * @param state The CSRF state + * @param redirectUri The redirect URI used in the authorization request (must match) */ export async function exchangeCodeForTokens( code: string, codeVerifier: string, state: string, + redirectUri?: string, ): Promise { const body = { code, state, grant_type: "authorization_code", client_id: CLAUDE_CODE_OAUTH_CONFIG.clientId, - redirect_uri: CLAUDE_CODE_OAUTH_CONFIG.redirectUri, + redirect_uri: redirectUri ?? CLAUDE_CODE_OAUTH_CONFIG.redirectUri, code_verifier: codeVerifier, } @@ -277,6 +285,7 @@ export class ClaudeCodeOAuthManager { private pendingAuth: { codeVerifier: string state: string + redirectUri: string server?: http.Server } | null = null @@ -468,9 +477,17 @@ export class ClaudeCodeOAuthManager { /** * Start the OAuth authorization flow - * Returns the authorization URL to open in browser + * Uses vscode.env.asExternalUri() to support remote environments (GitHub Codespaces, etc.) + * @param vscodeEnv The vscode.env object for URI transformation + * @param parseUri Function to parse a string into a Uri (e.g., vscode.Uri.parse) + * @returns The authorization URL to open in browser */ - startAuthorizationFlow(): string { + async startAuthorizationFlow( + vscodeEnv?: { + asExternalUri: (uri: Uri) => Thenable + }, + parseUri?: (value: string) => Uri, + ): Promise { // Cancel any existing authorization flow before starting a new one this.cancelAuthorizationFlow() @@ -478,12 +495,30 @@ export class ClaudeCodeOAuthManager { const codeChallenge = generateCodeChallenge(codeVerifier) const state = generateState() + // Determine the redirect URI + // In remote environments (Codespaces, etc.), use asExternalUri to get the forwarded URL + let resolvedRedirectUri: string = CLAUDE_CODE_OAUTH_CONFIG.redirectUri + + if (vscodeEnv && parseUri) { + try { + // Parse the localhost URI and transform it using asExternalUri + const localUri = parseUri(CLAUDE_CODE_OAUTH_CONFIG.redirectUri) + const externalUri = await vscodeEnv.asExternalUri(localUri) + resolvedRedirectUri = externalUri.toString() + this.log(`[claude-code-oauth] Resolved redirect URI: ${resolvedRedirectUri}`) + } catch (error) { + // Fall back to localhost if asExternalUri fails + this.logError("[claude-code-oauth] Failed to resolve external URI, using localhost:", error) + } + } + this.pendingAuth = { codeVerifier, state, + redirectUri: resolvedRedirectUri, } - return buildAuthorizationUrl(codeChallenge, state) + return buildAuthorizationUrl(codeChallenge, state, resolvedRedirectUri) } /** @@ -545,7 +580,13 @@ export class ClaudeCodeOAuthManager { } try { - const credentials = await exchangeCodeForTokens(code, this.pendingAuth.codeVerifier, state) + // Use the same redirect URI that was used in the authorization request + const credentials = await exchangeCodeForTokens( + code, + this.pendingAuth.codeVerifier, + state, + this.pendingAuth.redirectUri, + ) await this.saveCredentials(credentials)