Commit 9bb2c090 authored by Your Name's avatar Your Name

fix: Use API key from OAuth2 token exchange instead of raw OAuth2 token

Claude Code doesn't use the OAuth2 access token directly for API requests.
Instead, it exchanges the OAuth2 token for an API key via:
  POST https://api.anthropic.com/api/oauth/claude_cli/create_api_key
  Authorization: Bearer {oauth_access_token}

This returns a 'raw_key' which is the actual API key used for API requests.

Changes:
- claude_auth.py: Add create_api_key() and get_api_key() methods
  - create_api_key(): Exchanges OAuth2 token for API key
  - get_api_key(): Gets stored API key or creates one if needed
- providers.py: Update _get_sdk_client() to use API key instead of OAuth2 token

This matches the Claude Code flow in vendors/claude/src/services/oauth/client.ts
parent a303acc3
...@@ -581,6 +581,97 @@ class ClaudeAuth: ...@@ -581,6 +581,97 @@ class ClaudeAuth:
logger.error(f"Token exchange failed after {max_retries} attempts") logger.error(f"Token exchange failed after {max_retries} attempts")
return False return False
def create_api_key(self, access_token: str = None) -> Optional[str]:
"""
Exchange OAuth2 access token for an API key.
This matches the Claude Code flow:
1. Get OAuth2 access token
2. Call create_api_key endpoint to get an API key
3. Use the API key for API requests (not the OAuth2 token)
See: vendors/claude/src/services/oauth/client.ts:createAndStoreApiKey()
Endpoint: https://api.anthropic.com/api/oauth/claude_cli/create_api_key
Args:
access_token: OAuth2 access token (uses current token if not provided)
Returns:
API key string or None if failed
"""
if access_token is None:
if not self.tokens or 'access_token' not in self.tokens:
logger.warning("No access token available")
return None
access_token = self.tokens['access_token']
try:
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
# The endpoint that exchanges OAuth2 token for an API key
api_key_url = "https://api.anthropic.com/api/oauth/claude_cli/create_api_key"
response = self._make_request(
method="POST",
url=api_key_url,
headers=headers,
json_data=None,
timeout=30.0
)
if response.status_code == 200:
data = response.json()
api_key = data.get('raw_key')
if api_key:
# Store the API key in credentials
if self.tokens:
self.tokens['api_key'] = api_key
self._save_credentials(self.tokens)
logger.info("Successfully created and stored API key")
return api_key
else:
logger.warning(f"API key not found in response: {data}")
return None
else:
logger.error(f"API key creation failed: {response.status_code} - {response.text}")
return None
except Exception as e:
logger.error(f"API key creation error: {e}")
return None
def get_api_key(self) -> Optional[str]:
"""
Get a valid API key for API requests.
If we have a stored API key, return it.
If not, create one from the OAuth2 access token.
Returns:
API key string or None if failed
"""
# Check if we have a stored API key
if self.tokens and 'api_key' in self.tokens:
return self.tokens['api_key']
# No API key stored, create one from OAuth2 token
if self.tokens and 'access_token' in self.tokens:
logger.info("No stored API key, creating one from OAuth2 token...")
return self.create_api_key()
# No tokens at all, need to login
logger.info("No tokens available, starting login flow")
self.login()
# Try to create API key after login
if self.tokens and 'access_token' in self.tokens:
return self.create_api_key()
return None
def is_authenticated(self) -> bool: def is_authenticated(self) -> bool:
"""Check if we have valid credentials.""" """Check if we have valid credentials."""
return self.tokens is not None and 'access_token' in self.tokens return self.tokens is not None and 'access_token' in self.tokens
......
...@@ -2350,32 +2350,32 @@ class ClaudeProviderHandler(BaseProviderHandler): ...@@ -2350,32 +2350,32 @@ class ClaudeProviderHandler(BaseProviderHandler):
def _get_sdk_client(self): def _get_sdk_client(self):
""" """
Get or create an Anthropic SDK client configured with OAuth2 token. Get or create an Anthropic SDK client configured with API key.
The SDK handles proper message formatting, retries, and streaming. The SDK handles proper message formatting, retries, and streaming.
We pass the OAuth2 token as the api_key parameter. We use the API key obtained from the OAuth2 token exchange,
matching the Claude Code flow (see createAndStoreApiKey in client.ts).
""" """
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Get valid OAuth2 access token # Get API key from OAuth2 token exchange
access_token = self.auth.get_valid_token() # This matches the Claude Code flow: OAuth2 token → API key → API requests
api_key = self.auth.get_api_key()
if not api_key:
logger.error("ClaudeProviderHandler: Failed to get API key from OAuth2 token")
raise Exception("Failed to get API key. Please re-authenticate with /login")
# Create SDK client with OAuth2 token # Create SDK client with API key
# The SDK uses api_key for authentication - we pass our OAuth2 token
self._sdk_client = Anthropic( self._sdk_client = Anthropic(
api_key=access_token, api_key=api_key,
base_url="https://api.anthropic.com", base_url="https://api.anthropic.com",
max_retries=3, # SDK handles automatic retries max_retries=3, # SDK handles automatic retries
timeout=httpx.Timeout(300.0, connect=30.0), timeout=httpx.Timeout(300.0, connect=30.0),
) )
# Set beta headers for Claude Code compatibility logger.info("ClaudeProviderHandler: Created SDK client with API key")
self._sdk_client._custom_headers = {
'Anthropic-Beta': 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05',
}
logger.info("ClaudeProviderHandler: Created SDK client with OAuth2 token")
return self._sdk_client return self._sdk_client
def _get_auth_headers(self, stream: bool = False): def _get_auth_headers(self, stream: bool = False):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment