a789495a98
CI / Quality Guardrails (push) Waiting to run
CI / Build & Test (macos-latest) (push) Waiting to run
CI / Build & Test (ubuntu-latest) (push) Waiting to run
CI / Build & Test (windows-latest) (push) Waiting to run
CI / Format (push) Waiting to run
CI / PowerShell Syntax (push) Waiting to run
CI / Windows Cross-Target Check (Linux) (push) Waiting to run
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
104 lines
3.3 KiB
Python
104 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Helper script to complete Claude OAuth flow with proper PKCE."""
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import secrets
|
|
import sys
|
|
import urllib.parse
|
|
import requests
|
|
|
|
CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
|
AUTHORIZE_URL = "https://claude.ai/oauth/authorize"
|
|
TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"
|
|
REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback"
|
|
SCOPES = "org:create_api_key user:profile user:inference"
|
|
|
|
def generate_pkce():
|
|
"""Generate PKCE verifier and challenge."""
|
|
verifier = secrets.token_urlsafe(48)[:64] # 64 chars
|
|
digest = hashlib.sha256(verifier.encode()).digest()
|
|
challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode()
|
|
return verifier, challenge
|
|
|
|
def generate_state():
|
|
"""Generate random state for CSRF protection."""
|
|
return secrets.token_hex(16)
|
|
|
|
def main():
|
|
if len(sys.argv) > 1 and sys.argv[1] == "--exchange":
|
|
# Exchange mode: read code from stdin or arg
|
|
code = sys.argv[2] if len(sys.argv) > 2 else input("Enter code: ").strip()
|
|
|
|
# Load saved state
|
|
with open("/tmp/claude_oauth_state.json") as f:
|
|
saved = json.load(f)
|
|
|
|
# Exchange code for tokens
|
|
resp = requests.post(TOKEN_URL, data={
|
|
"grant_type": "authorization_code",
|
|
"client_id": CLIENT_ID,
|
|
"code": code,
|
|
"code_verifier": saved["verifier"],
|
|
"redirect_uri": REDIRECT_URI,
|
|
})
|
|
|
|
if resp.status_code != 200:
|
|
print(f"Error: {resp.text}")
|
|
sys.exit(1)
|
|
|
|
tokens = resp.json()
|
|
print(f"\nAccess token: {tokens['access_token'][:50]}...")
|
|
print(f"Refresh token: {tokens['refresh_token'][:50]}...")
|
|
|
|
# Save in Claude Code format
|
|
creds_dir = os.path.expanduser("~/.claude")
|
|
os.makedirs(creds_dir, exist_ok=True)
|
|
|
|
import time
|
|
expires_at = int(time.time() * 1000) + (tokens["expires_in"] * 1000)
|
|
|
|
creds = {
|
|
"claudeAiOauth": {
|
|
"accessToken": tokens["access_token"],
|
|
"refreshToken": tokens["refresh_token"],
|
|
"expiresAt": expires_at,
|
|
}
|
|
}
|
|
|
|
with open(os.path.join(creds_dir, ".credentials.json"), "w") as f:
|
|
json.dump(creds, f, indent=2)
|
|
|
|
print(f"\nCredentials saved to ~/.claude/.credentials.json")
|
|
else:
|
|
# Generate new auth URL
|
|
verifier, challenge = generate_pkce()
|
|
state = generate_state()
|
|
|
|
params = {
|
|
"response_type": "code",
|
|
"client_id": CLIENT_ID,
|
|
"redirect_uri": REDIRECT_URI,
|
|
"scope": SCOPES,
|
|
"code_challenge": challenge,
|
|
"code_challenge_method": "S256",
|
|
"state": state,
|
|
}
|
|
auth_url = f"{AUTHORIZE_URL}?{urllib.parse.urlencode(params)}"
|
|
|
|
with open("/tmp/claude_oauth_state.json", "w") as f:
|
|
json.dump({
|
|
"verifier": verifier,
|
|
"challenge": challenge,
|
|
"state": state,
|
|
"auth_url": auth_url
|
|
}, f)
|
|
|
|
print(f"Auth URL: {auth_url}")
|
|
print(f"\nState saved to /tmp/claude_oauth_state.json")
|
|
print(f"Verifier: {verifier}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|