|
#!/usr/bin/env node |
|
/** |
|
* Get OAuth tokens from your Claude Pro/Max subscription |
|
* Works with: node, bun, deno |
|
* Usage: node oauth-claude.ts |
|
*/ |
|
|
|
import { createHash, randomBytes } from 'node:crypto'; |
|
|
|
const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e'; |
|
const AUTH_URL = 'https://claude.ai/oauth/authorize'; |
|
const TOKEN_URL = 'https://console.anthropic.com/v1/oauth/token'; |
|
const REDIRECT_URI = 'https://console.anthropic.com/oauth/code/callback'; |
|
const SCOPES = 'org:create_api_key user:profile user:inference'; |
|
|
|
// Generate PKCE challenge |
|
function generatePKCE() { |
|
const verifier = randomBytes(32).toString('base64url'); |
|
const challenge = createHash('sha256').update(verifier).digest('base64url'); |
|
return { verifier, challenge }; |
|
} |
|
|
|
async function main() { |
|
const { verifier, challenge } = generatePKCE(); |
|
const state = randomBytes(16).toString('hex'); |
|
|
|
// Build auth URL |
|
const authUrl = new URL(AUTH_URL); |
|
authUrl.searchParams.set('response_type', 'code'); |
|
authUrl.searchParams.set('client_id', CLIENT_ID); |
|
authUrl.searchParams.set('redirect_uri', REDIRECT_URI); |
|
authUrl.searchParams.set('scope', SCOPES); |
|
authUrl.searchParams.set('state', state); |
|
authUrl.searchParams.set('code_challenge', challenge); |
|
authUrl.searchParams.set('code_challenge_method', 'S256'); |
|
|
|
console.log('\n1. Open this URL in your browser:\n'); |
|
console.log(authUrl.toString()); |
|
console.log('\n2. Sign in with your Claude Pro/Max account'); |
|
console.log('\n3. After authorization, copy the COMPLETE URL from the redirect page'); |
|
console.log(' (it looks like https://console.anthropic.com/oauth/code/callback?code=...&state=...)'); |
|
|
|
// Helper to read input |
|
const readInput = () => |
|
new Promise<string>((resolve) => { |
|
process.stdout.write('\n> '); |
|
process.stdin.once('data', (data) => { |
|
resolve(data.toString().trim()); |
|
}); |
|
}); |
|
|
|
// Loop until valid input |
|
let code: string; |
|
|
|
while (true) { |
|
console.log('\n4. Paste the complete URL here:'); |
|
const input = await readInput(); |
|
|
|
if (!input) { |
|
console.error('❌ You didn\'t paste anything! Try again.'); |
|
continue; |
|
} |
|
|
|
if (!input.startsWith('http')) { |
|
console.error('❌ Paste the COMPLETE URL, not just the code!'); |
|
console.error(' The URL should start with https://console.anthropic.com/...'); |
|
continue; |
|
} |
|
|
|
try { |
|
const url = new URL(input); |
|
code = url.searchParams.get('code') || ''; |
|
const returnedState = url.searchParams.get('state') || ''; |
|
|
|
if (!code) { |
|
console.error('❌ No "code" in the URL. Did you authorize the app?'); |
|
continue; |
|
} |
|
|
|
if (returnedState !== state) { |
|
console.error('❌ State mismatch. Please restart the script.'); |
|
process.exit(1); |
|
} |
|
|
|
break; // Valid input, exit loop |
|
} catch { |
|
console.error('❌ Invalid URL. Paste the complete URL from your address bar.'); |
|
continue; |
|
} |
|
} |
|
|
|
console.log('\n✓ Code extracted:', code.substring(0, 20) + '...'); |
|
console.log('✓ State verified'); |
|
|
|
console.log('\nExchanging code for access token...'); |
|
|
|
// Exchange code for token |
|
const response = await fetch(TOKEN_URL, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
grant_type: 'authorization_code', |
|
client_id: CLIENT_ID, |
|
code, |
|
state, |
|
redirect_uri: REDIRECT_URI, |
|
code_verifier: verifier, |
|
}), |
|
}); |
|
|
|
if (!response.ok) { |
|
const error = await response.text(); |
|
console.error('\nError:', error); |
|
process.exit(1); |
|
} |
|
|
|
const tokens = await response.json() as { |
|
access_token: string; |
|
refresh_token: string; |
|
expires_in: number; |
|
}; |
|
|
|
console.log('\n✅ Tokens retrieved!\n'); |
|
console.log('─'.repeat(60)); |
|
console.log('REFRESH TOKEN (recommended - auto-refreshes, never expires):'); |
|
console.log('─'.repeat(60)); |
|
console.log(tokens.refresh_token); |
|
console.log('─'.repeat(60)); |
|
console.log('\nACCESS TOKEN (expires in ~8 hours):'); |
|
console.log(tokens.access_token); |
|
console.log('─'.repeat(60)); |
|
|
|
process.exit(0); |
|
} |
|
|
|
main().catch((err) => { |
|
console.error(err); |
|
process.exit(1); |
|
}); |