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