Skip to content

Instantly share code, notes, and snippets.

@max-lt
Last active January 8, 2026 15:43
Show Gist options
  • Select an option

  • Save max-lt/9a500c715f891574aa44652fe2a8ddf9 to your computer and use it in GitHub Desktop.

Select an option

Save max-lt/9a500c715f891574aa44652fe2a8ddf9 to your computer and use it in GitHub Desktop.
Claude OAuth Token Generator

Claude OAuth Token Generator

Get OAuth tokens from your Claude Pro/Max subscription to use with third-party apps.

⚠️ Disclaimer

This script exploits Claude Code's OAuth flow. It's unofficial and unsupported by Anthropic. Use at your own risk — it may break at any time.

For production use, prefer official API keys.

Requirements

  • Node.js, Bun, or Deno
  • Claude Pro or Max subscription

Usage

# With Bun
bun oauth-claude.ts

# With Node
node oauth-claude.ts

# With Deno
deno run -A oauth-claude.ts

Follow the prompts:

  1. Open the generated URL in your browser
  2. Sign in with your Claude account
  3. Authorize the app
  4. Copy the complete redirect URL from your address bar
  5. Paste it back in the terminal

Token Types

Token Prefix Lifetime Notes
Refresh Token sk-ant-ort... Never expires ✅ Recommended. Use this one.
Access Token sk-ant-oat... ~8 hours Short-lived, needs refresh.

How it works

This script uses the same OAuth flow as Claude Code (Anthropic's official CLI). It generates a PKCE challenge, redirects you to authorize, then exchanges the code for tokens.

The refresh token can be used to get new access tokens indefinitely, as long as your Claude subscription is active.

#!/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);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment