Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
25 changes: 25 additions & 0 deletions src/integrations/claude-code/__tests__/oauth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
57 changes: 49 additions & 8 deletions src/integrations/claude-code/oauth.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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<ClaudeCodeCredentials> {
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,
}

Expand Down Expand Up @@ -277,6 +285,7 @@ export class ClaudeCodeOAuthManager {
private pendingAuth: {
codeVerifier: string
state: string
redirectUri: string
server?: http.Server
} | null = null

Expand Down Expand Up @@ -468,22 +477,48 @@ 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<Uri>
},
parseUri?: (value: string) => Uri,
): Promise<string> {
// Cancel any existing authorization flow before starting a new one
this.cancelAuthorizationFlow()

const codeVerifier = generateCodeVerifier()
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)
}

/**
Expand Down Expand Up @@ -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)

Expand Down
Loading