···
3
-
# Anthropic OAuth client ID
4
-
CLIENT_ID="9d1c250a-e61b-44d9-88ed-5944d1962f5e"
6
-
# Token cache file location
7
-
CACHE_DIR="${HOME}/.config/crush/anthropic"
8
-
CACHE_FILE="${CACHE_DIR}/bearer_token"
9
-
REFRESH_TOKEN_FILE="${CACHE_DIR}/refresh_token"
11
-
# Function to extract expiration from cached token file
12
-
extract_expiration() {
13
-
if [ -f "${CACHE_FILE}.expires" ]; then
14
-
cat "${CACHE_FILE}.expires"
18
-
# Function to check if token is valid
22
-
if [ -z "$expires" ]; then
26
-
local current_time=$(date +%s)
27
-
# Add 60 second buffer before expiration
28
-
local buffer_time=$((expires - 60))
30
-
if [ "$current_time" -lt "$buffer_time" ]; then
37
-
# Function to generate PKCE challenge (requires openssl)
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")
44
-
echo "$verifier|$challenge"
47
-
# Function to exchange refresh token for new access token
48
-
exchange_refresh_token() {
49
-
local refresh_token="$1"
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}\"}")
56
-
# Parse JSON response - try jq first, fallback to sed
57
-
local access_token=""
58
-
local new_refresh_token=""
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')
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')
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))
77
-
# Cache the new tokens
78
-
mkdir -p "$CACHE_DIR"
79
-
echo "$access_token" > "$CACHE_FILE"
80
-
chmod 600 "$CACHE_FILE"
82
-
if [ -n "$new_refresh_token" ]; then
83
-
echo "$new_refresh_token" > "$REFRESH_TOKEN_FILE"
84
-
chmod 600 "$REFRESH_TOKEN_FILE"
87
-
# Store expiration for future reference
88
-
echo "$expires_timestamp" > "${CACHE_FILE}.expires"
89
-
chmod 600 "${CACHE_FILE}.expires"
91
-
echo "$access_token"
98
-
# Function to exchange authorization code for tokens
99
-
exchange_authorization_code() {
100
-
local auth_code="$1"
101
-
local verifier="$2"
103
-
# Split code if it contains state (format: code#state)
104
-
local code=$(echo "$auth_code" | cut -d'#' -f1)
106
-
if echo "$auth_code" | grep -q '#'; then
107
-
state=$(echo "$auth_code" | cut -d'#' -f2)
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}\"}")
116
-
# Parse JSON response - try jq first, fallback to sed
117
-
local access_token=""
118
-
local refresh_token=""
119
-
local expires_in=""
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')
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')
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))
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"
144
-
echo "$access_token"
151
-
# Check for cached bearer token
152
-
if [ -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"
162
-
# Bearer token is expired/missing, try to use cached refresh token
163
-
if [ -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"
176
-
# No valid tokens found, start OAuth flow
177
-
# Check if openssl is available for PKCE
178
-
if ! command -v openssl >/dev/null 2>&1; then
182
-
# Generate PKCE challenge
183
-
PKCE_DATA=$(generate_pkce)
184
-
VERIFIER=$(echo "$PKCE_DATA" | cut -d'|' -f1)
185
-
CHALLENGE=$(echo "$PKCE_DATA" | cut -d'|' -f2)
188
-
AUTH_URL="https://claude.ai/oauth/authorize"
189
-
AUTH_URL="${AUTH_URL}?response_type=code"
190
-
AUTH_URL="${AUTH_URL}&client_id=${CLIENT_ID}"
191
-
AUTH_URL="${AUTH_URL}&redirect_uri=https://console.anthropic.com/oauth/code/callback"
192
-
AUTH_URL="${AUTH_URL}&scope=org:create_api_key%20user:profile%20user:inference"
193
-
AUTH_URL="${AUTH_URL}&code_challenge=${CHALLENGE}"
194
-
AUTH_URL="${AUTH_URL}&code_challenge_method=S256"
195
-
AUTH_URL="${AUTH_URL}&state=${VERIFIER}"
197
-
# Create a temporary HTML file with the authentication form
198
-
TEMP_HTML="/tmp/anthropic_auth_$$.html"
199
-
cat > "$TEMP_HTML" << EOF
203
-
<title>Anthropic Authentication</title>
206
-
box-sizing: border-box;
212
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
213
-
background: linear-gradient(135deg, #1a1a1a 0%, #2d1810 100%);
217
-
align-items: center;
218
-
justify-content: center;
223
-
background: rgba(40, 40, 40, 0.95);
224
-
border: 1px solid #4a4a4a;
225
-
border-radius: 16px;
229
-
text-align: center;
230
-
backdrop-filter: blur(10px);
231
-
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
237
-
margin: 0 auto 24px;
238
-
background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%);
239
-
border-radius: 12px;
241
-
align-items: center;
242
-
justify-content: center;
251
-
margin-bottom: 12px;
257
-
margin-bottom: 32px;
263
-
margin-bottom: 32px;
268
-
display: inline-flex;
269
-
align-items: center;
270
-
justify-content: center;
273
-
background: #ff6b35;
275
-
border-radius: 50%;
278
-
margin-right: 12px;
283
-
margin-bottom: 8px;
287
-
.step-description {
294
-
display: inline-block;
295
-
background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%);
297
-
padding: 16px 32px;
298
-
text-decoration: none;
299
-
border-radius: 12px;
302
-
margin-bottom: 24px;
303
-
transition: all 0.2s ease;
304
-
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
308
-
transform: translateY(-2px);
309
-
box-shadow: 0 8px 20px rgba(255, 107, 53, 0.4);
313
-
margin-bottom: 24px;
319
-
margin-bottom: 8px;
326
-
background: #2a2a2a;
327
-
border: 2px solid #4a4a4a;
328
-
border-radius: 8px;
331
-
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
336
-
transition: border-color 0.2s ease;
341
-
border-color: #ff6b35;
342
-
box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.1);
345
-
textarea::placeholder {
350
-
background: linear-gradient(135deg, #ff6b35 0%, #ff8e53 100%);
353
-
padding: 16px 32px;
354
-
border-radius: 12px;
358
-
transition: all 0.2s ease;
359
-
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
363
-
.submit-btn:hover {
364
-
transform: translateY(-2px);
365
-
box-shadow: 0 8px 20px rgba(255, 107, 53, 0.4);
368
-
.submit-btn:disabled {
370
-
cursor: not-allowed;
377
-
border-radius: 8px;
383
-
background: rgba(52, 168, 83, 0.1);
384
-
border: 1px solid rgba(52, 168, 83, 0.3);
389
-
background: rgba(234, 67, 53, 0.1);
390
-
border: 1px solid rgba(234, 67, 53, 0.3);
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>
402
-
<div class="step-title">
403
-
<span class="step-number">1</span>
404
-
Authorize with Anthropic
406
-
<div class="step-description">
407
-
Click the button below to open the Anthropic authorization page
411
-
<a href="$AUTH_URL" class="button" target="_blank">
412
-
Open Anthropic Authorization
416
-
<div class="step-title">
417
-
<span class="step-number">2</span>
418
-
Paste your authorization token
420
-
<div class="step-description">
421
-
After authorizing, copy the token and paste it below
425
-
<form id="tokenForm">
426
-
<div class="input-group">
427
-
<label for="token">Authorization Token:</label>
431
-
placeholder="Paste your token here..."
435
-
<button type="submit" class="submit-btn" id="submitBtn">
436
-
Complete Authentication
440
-
<div id="status" class="status"></div>
444
-
document.getElementById('tokenForm').addEventListener('submit', function(e) {
445
-
e.preventDefault();
447
-
const token = document.getElementById('token').value.trim();
449
-
showStatus('Please paste your authorization token', 'error');
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
462
-
document.body.removeChild(a); // Clean up
464
-
// Verify file creation
465
-
console.log("Token file created with content length: " + token.length);
467
-
showStatus('Empty token detected, please provide a valid token', 'error');
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');
475
-
// setTimeout(() => {
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';
487
-
// Auto-close after 10 minutes
496
-
# Open the HTML file
497
-
if command -v xdg-open >/dev/null 2>&1; then
498
-
xdg-open "$TEMP_HTML" >/dev/null 2>&1 &
499
-
elif command -v open >/dev/null 2>&1; then
500
-
open "$TEMP_HTML" >/dev/null 2>&1 &
501
-
elif command -v start >/dev/null 2>&1; then
502
-
start "$TEMP_HTML" >/dev/null 2>&1 &
505
-
# Wait for user to download the token file
506
-
TOKEN_FILE="$HOME/Downloads/anthropic_token.txt"
508
-
for 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"
517
-
# Clean up the temporary HTML file
520
-
if [ -z "$AUTH_CODE" ]; then
524
-
# Exchange code for tokens
525
-
ACCESS_TOKEN=$(exchange_authorization_code "$AUTH_CODE" "$VERIFIER")
526
-
if [ -n "$ACCESS_TOKEN" ]; then
527
-
echo "$ACCESS_TOKEN"