get your claude code tokens here
1#!/bin/sh 2 3# Anthropic OAuth client ID 4CLIENT_ID="9d1c250a-e61b-44d9-88ed-5944d1962f5e" 5 6# Token cache file location 7CACHE_DIR="${HOME}/.config/crush/anthropic" 8CACHE_FILE="${CACHE_DIR}/bearer_token" 9REFRESH_TOKEN_FILE="${CACHE_DIR}/refresh_token" 10 11# Function to extract expiration from cached token file 12extract_expiration() { 13 if [ -f "${CACHE_FILE}.expires" ]; then 14 cat "${CACHE_FILE}.expires" 15 fi 16} 17 18# Function to check if token is valid 19is_token_valid() { 20 local expires="$1" 21 22 if [ -z "$expires" ]; then 23 return 1 24 fi 25 26 local current_time=$(date +%s) 27 # Add 60 second buffer before expiration 28 local buffer_time=$((expires - 60)) 29 30 if [ "$current_time" -lt "$buffer_time" ]; then 31 return 0 32 else 33 return 1 34 fi 35} 36 37# Function to generate PKCE challenge (requires openssl) 38generate_pkce() { 39 # Generate 32 random bytes, base64url encode 40 local verifier=$(openssl rand -base64 32 | tr -d "=" | tr "/" "_" | tr "+" "-" | tr -d "\n") 41 # Create SHA256 hash of verifier, base64url encode 42 local challenge=$(printf '%s' "$verifier" | openssl dgst -sha256 -binary | openssl base64 | tr -d "=" | tr "/" "_" | tr "+" "-" | tr -d "\n") 43 44 echo "$verifier|$challenge" 45} 46 47# Function to exchange refresh token for new access token 48exchange_refresh_token() { 49 local refresh_token="$1" 50 51 local bearer_response=$(curl -s -X POST "https://console.anthropic.com/v1/oauth/token" \ 52 -H "Content-Type: application/json" \ 53 -H "User-Agent: CRUSH/1.0" \ 54 -d "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"${refresh_token}\",\"client_id\":\"${CLIENT_ID}\"}") 55 56 # Parse JSON response - try jq first, fallback to sed 57 local access_token="" 58 local new_refresh_token="" 59 local expires_in="" 60 61 if command -v jq >/dev/null 2>&1; then 62 access_token=$(echo "$bearer_response" | jq -r '.access_token // empty') 63 new_refresh_token=$(echo "$bearer_response" | jq -r '.refresh_token // empty') 64 expires_in=$(echo "$bearer_response" | jq -r '.expires_in // empty') 65 else 66 # Fallback to sed parsing 67 access_token=$(echo "$bearer_response" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') 68 new_refresh_token=$(echo "$bearer_response" | sed -n 's/.*"refresh_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') 69 expires_in=$(echo "$bearer_response" | sed -n 's/.*"expires_in"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p') 70 fi 71 72 if [ -n "$access_token" ] && [ -n "$expires_in" ]; then 73 # Calculate expiration timestamp 74 local current_time=$(date +%s) 75 local expires_timestamp=$((current_time + expires_in)) 76 77 # Cache the new tokens 78 mkdir -p "$CACHE_DIR" 79 echo "$access_token" > "$CACHE_FILE" 80 chmod 600 "$CACHE_FILE" 81 82 if [ -n "$new_refresh_token" ]; then 83 echo "$new_refresh_token" > "$REFRESH_TOKEN_FILE" 84 chmod 600 "$REFRESH_TOKEN_FILE" 85 fi 86 87 # Store expiration for future reference 88 echo "$expires_timestamp" > "${CACHE_FILE}.expires" 89 chmod 600 "${CACHE_FILE}.expires" 90 91 echo "$access_token" 92 return 0 93 fi 94 95 return 1 96} 97 98# Function to exchange authorization code for tokens 99exchange_authorization_code() { 100 local auth_code="$1" 101 local verifier="$2" 102 103 # Split code if it contains state (format: code#state) 104 local code=$(echo "$auth_code" | cut -d'#' -f1) 105 local state="" 106 if echo "$auth_code" | grep -q '#'; then 107 state=$(echo "$auth_code" | cut -d'#' -f2) 108 fi 109 110 # Use the working endpoint 111 local bearer_response=$(curl -s -X POST "https://console.anthropic.com/v1/oauth/token" \ 112 -H "Content-Type: application/json" \ 113 -H "User-Agent: CRUSH/1.0" \ 114 -d "{\"code\":\"${code}\",\"state\":\"${state}\",\"grant_type\":\"authorization_code\",\"client_id\":\"${CLIENT_ID}\",\"redirect_uri\":\"https://console.anthropic.com/oauth/code/callback\",\"code_verifier\":\"${verifier}\"}") 115 116 # Parse JSON response - try jq first, fallback to sed 117 local access_token="" 118 local refresh_token="" 119 local expires_in="" 120 121 if command -v jq >/dev/null 2>&1; then 122 access_token=$(echo "$bearer_response" | jq -r '.access_token // empty') 123 refresh_token=$(echo "$bearer_response" | jq -r '.refresh_token // empty') 124 expires_in=$(echo "$bearer_response" | jq -r '.expires_in // empty') 125 else 126 # Fallback to sed parsing 127 access_token=$(echo "$bearer_response" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') 128 refresh_token=$(echo "$bearer_response" | sed -n 's/.*"refresh_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') 129 expires_in=$(echo "$bearer_response" | sed -n 's/.*"expires_in"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p') 130 fi 131 132 if [ -n "$access_token" ] && [ -n "$refresh_token" ] && [ -n "$expires_in" ]; then 133 # Calculate expiration timestamp 134 local current_time=$(date +%s) 135 local expires_timestamp=$((current_time + expires_in)) 136 137 # Cache the tokens 138 mkdir -p "$CACHE_DIR" 139 echo "$access_token" > "$CACHE_FILE" 140 echo "$refresh_token" > "$REFRESH_TOKEN_FILE" 141 echo "$expires_timestamp" > "${CACHE_FILE}.expires" 142 chmod 600 "$CACHE_FILE" "$REFRESH_TOKEN_FILE" "${CACHE_FILE}.expires" 143 144 echo "$access_token" 145 return 0 146 else 147 return 1 148 fi 149} 150 151# Check for cached bearer token 152if [ -f "$CACHE_FILE" ] && [ -f "${CACHE_FILE}.expires" ]; then 153 CACHED_TOKEN=$(cat "$CACHE_FILE") 154 CACHED_EXPIRES=$(cat "${CACHE_FILE}.expires") 155 if is_token_valid "$CACHED_EXPIRES"; then 156 # Token is still valid, output and exit 157 echo "$CACHED_TOKEN" 158 exit 0 159 fi 160fi 161 162# Bearer token is expired/missing, try to use cached refresh token 163if [ -f "$REFRESH_TOKEN_FILE" ]; then 164 REFRESH_TOKEN=$(cat "$REFRESH_TOKEN_FILE") 165 if [ -n "$REFRESH_TOKEN" ]; then 166 # Try to exchange refresh token for new bearer token 167 BEARER_TOKEN=$(exchange_refresh_token "$REFRESH_TOKEN") 168 if [ -n "$BEARER_TOKEN" ]; then 169 # Successfully got new bearer token, output and exit 170 echo "$BEARER_TOKEN" 171 exit 0 172 fi 173 fi 174fi 175 176# No valid tokens found, start OAuth flow 177# Check if openssl is available for PKCE 178if ! command -v openssl >/dev/null 2>&1; then 179 exit 1 180fi 181 182# Generate PKCE challenge 183PKCE_DATA=$(generate_pkce) 184VERIFIER=$(echo "$PKCE_DATA" | cut -d'|' -f1) 185CHALLENGE=$(echo "$PKCE_DATA" | cut -d'|' -f2) 186 187# Build OAuth URL 188AUTH_URL="https://claude.ai/oauth/authorize" 189AUTH_URL="${AUTH_URL}?response_type=code" 190AUTH_URL="${AUTH_URL}&client_id=${CLIENT_ID}" 191AUTH_URL="${AUTH_URL}&redirect_uri=https://console.anthropic.com/oauth/code/callback" 192AUTH_URL="${AUTH_URL}&scope=org:create_api_key%20user:profile%20user:inference" 193AUTH_URL="${AUTH_URL}&code_challenge=${CHALLENGE}" 194AUTH_URL="${AUTH_URL}&code_challenge_method=S256" 195AUTH_URL="${AUTH_URL}&state=${VERIFIER}" 196 197# Create a temporary HTML file with the authentication form 198TEMP_HTML="/tmp/anthropic_auth_$$.html" 199cat > "$TEMP_HTML" << EOF 200<!DOCTYPE html> 201<html> 202<head> 203 <title>Anthropic Authentication</title> 204 <style> 205 * { 206 box-sizing: border-box; 207 margin: 0; 208 padding: 0; 209 } 210 211 body { 212 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 213 background: linear-gradient(135deg, #1a1a1a 0%, #2d1810 100%); 214 color: #ffffff; 215 min-height: 100vh; 216 display: flex; 217 align-items: center; 218 justify-content: center; 219 padding: 20px; 220 } 221 222 .container { 223 background: rgba(40, 40, 40, 0.95); 224 border: 1px solid #4a4a4a; 225 border-radius: 16px; 226 padding: 48px; 227 max-width: 480px; 228 width: 100%; 229 text-align: center; 230 backdrop-filter: blur(10px); 231 box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); 232 } 233 234 .logo { 235 width: 48px; 236 height: 48px; 237 margin: 0 auto 24px; 238 background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%); 239 border-radius: 12px; 240 display: flex; 241 align-items: center; 242 justify-content: center; 243 font-weight: bold; 244 font-size: 24px; 245 color: white; 246 } 247 248 h1 { 249 font-size: 28px; 250 font-weight: 600; 251 margin-bottom: 12px; 252 color: #ffffff; 253 } 254 255 .subtitle { 256 color: #a0a0a0; 257 margin-bottom: 32px; 258 font-size: 16px; 259 line-height: 1.5; 260 } 261 262 .step { 263 margin-bottom: 32px; 264 text-align: left; 265 } 266 267 .step-number { 268 display: inline-flex; 269 align-items: center; 270 justify-content: center; 271 width: 24px; 272 height: 24px; 273 background: #ff6b35; 274 color: white; 275 border-radius: 50%; 276 font-size: 14px; 277 font-weight: 600; 278 margin-right: 12px; 279 } 280 281 .step-title { 282 font-weight: 600; 283 margin-bottom: 8px; 284 color: #ffffff; 285 } 286 287 .step-description { 288 color: #a0a0a0; 289 font-size: 14px; 290 margin-left: 36px; 291 } 292 293 .button { 294 display: inline-block; 295 background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%); 296 color: white; 297 padding: 16px 32px; 298 text-decoration: none; 299 border-radius: 12px; 300 font-weight: 600; 301 font-size: 16px; 302 margin-bottom: 24px; 303 transition: all 0.2s ease; 304 box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3); 305 } 306 307 .button:hover { 308 transform: translateY(-2px); 309 box-shadow: 0 8px 20px rgba(255, 107, 53, 0.4); 310 } 311 312 .input-group { 313 margin-bottom: 24px; 314 text-align: left; 315 } 316 317 label { 318 display: block; 319 margin-bottom: 8px; 320 font-weight: 500; 321 color: #ffffff; 322 } 323 324 textarea { 325 width: 100%; 326 background: #2a2a2a; 327 border: 2px solid #4a4a4a; 328 border-radius: 8px; 329 padding: 16px; 330 color: #ffffff; 331 font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; 332 font-size: 14px; 333 line-height: 1.4; 334 resize: vertical; 335 min-height: 120px; 336 transition: border-color 0.2s ease; 337 } 338 339 textarea:focus { 340 outline: none; 341 border-color: #ff6b35; 342 box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.1); 343 } 344 345 textarea::placeholder { 346 color: #666; 347 } 348 349 .submit-btn { 350 background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%); 351 color: white; 352 border: none; 353 padding: 16px 32px; 354 border-radius: 12px; 355 font-weight: 600; 356 font-size: 16px; 357 cursor: pointer; 358 transition: all 0.2s ease; 359 box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3); 360 width: 100%; 361 } 362 363 .submit-btn:hover { 364 transform: translateY(-2px); 365 box-shadow: 0 8px 20px rgba(255, 107, 53, 0.4); 366 } 367 368 .submit-btn:disabled { 369 opacity: 0.6; 370 cursor: not-allowed; 371 transform: none; 372 } 373 374 .status { 375 margin-top: 16px; 376 padding: 12px; 377 border-radius: 8px; 378 font-size: 14px; 379 display: none; 380 } 381 382 .status.success { 383 background: rgba(52, 168, 83, 0.1); 384 border: 1px solid rgba(52, 168, 83, 0.3); 385 color: #34a853; 386 } 387 388 .status.error { 389 background: rgba(234, 67, 53, 0.1); 390 border: 1px solid rgba(234, 67, 53, 0.3); 391 color: #ea4335; 392 } 393 </style> 394</head> 395<body> 396 <div class="container"> 397 <div class="logo">A</div> 398 <h1>Anthropic Authentication</h1> 399 <p class="subtitle">Connect your Anthropic account to continue</p> 400 401 <div class="step"> 402 <div class="step-title"> 403 <span class="step-number">1</span> 404 Authorize with Anthropic 405 </div> 406 <div class="step-description"> 407 Click the button below to open the Anthropic authorization page 408 </div> 409 </div> 410 411 <a href="$AUTH_URL" class="button" target="_blank"> 412 Open Anthropic Authorization 413 </a> 414 415 <div class="step"> 416 <div class="step-title"> 417 <span class="step-number">2</span> 418 Paste your authorization token 419 </div> 420 <div class="step-description"> 421 After authorizing, copy the token and paste it below 422 </div> 423 </div> 424 425 <form id="tokenForm"> 426 <div class="input-group"> 427 <label for="token">Authorization Token:</label> 428 <textarea 429 id="token" 430 name="token" 431 placeholder="Paste your token here..." 432 required 433 ></textarea> 434 </div> 435 <button type="submit" class="submit-btn" id="submitBtn"> 436 Complete Authentication 437 </button> 438 </form> 439 440 <div id="status" class="status"></div> 441 </div> 442 443 <script> 444 document.getElementById('tokenForm').addEventListener('submit', function(e) { 445 e.preventDefault(); 446 447 const token = document.getElementById('token').value.trim(); 448 if (!token) { 449 showStatus('Please paste your authorization token', 'error'); 450 return; 451 } 452 453 // Ensure token has content before creating file 454 if (token.length > 0) { 455 // Save the token as a downloadable file 456 const blob = new Blob([token], { type: 'text/plain' }); 457 const a = document.createElement('a'); 458 a.href = URL.createObjectURL(blob); 459 a.download = "anthropic_token.txt"; 460 document.body.appendChild(a); // Append to body to ensure it works in all browsers 461 a.click(); 462 document.body.removeChild(a); // Clean up 463 464 // Verify file creation 465 console.log("Token file created with content length: " + token.length); 466 } else { 467 showStatus('Empty token detected, please provide a valid token', 'error'); 468 return; 469 } 470 471 document.getElementById('submitBtn').disabled = true; 472 document.getElementById('submitBtn').textContent = "Token saved, you may close this tab."; 473 showStatus('Token file downloaded! You can close this window.', 'success'); 474 475 // setTimeout(() => { 476 // window.close(); 477 // }, 2000); 478 }); 479 480 function showStatus(message, type) { 481 const status = document.getElementById('status'); 482 status.textContent = message; 483 status.className = 'status ' + type; 484 status.style.display = 'block'; 485 } 486 487 // Auto-close after 10 minutes 488 setTimeout(() => { 489 window.close(); 490 }, 600000); 491 </script> 492</body> 493</html> 494EOF 495 496# Open the HTML file 497if command -v xdg-open >/dev/null 2>&1; then 498 xdg-open "$TEMP_HTML" >/dev/null 2>&1 & 499elif command -v open >/dev/null 2>&1; then 500 open "$TEMP_HTML" >/dev/null 2>&1 & 501elif command -v start >/dev/null 2>&1; then 502 start "$TEMP_HTML" >/dev/null 2>&1 & 503fi 504 505# Wait for user to download the token file 506TOKEN_FILE="$HOME/Downloads/anthropic_token.txt" 507 508for i in $(seq 1 60); do 509 if [ -f "$TOKEN_FILE" ]; then 510 AUTH_CODE=$(cat "$TOKEN_FILE" | tr -d '\r\n') 511 rm -f "$TOKEN_FILE" 512 break 513 fi 514 sleep 2 515done 516 517# Clean up the temporary HTML file 518rm -f "$TEMP_HTML" 519 520if [ -z "$AUTH_CODE" ]; then 521 exit 1 522fi 523 524# Exchange code for tokens 525ACCESS_TOKEN=$(exchange_authorization_code "$AUTH_CODE" "$VERIFIER") 526if [ -n "$ACCESS_TOKEN" ]; then 527 echo "$ACCESS_TOKEN" 528 exit 0 529else 530 exit 1 531fi