Fix OAuth login flow for Claude Max

This commit is contained in:
Zac Gaetano 2026-04-04 23:47:54 -04:00
parent e00b112da1
commit 39c18ac520

View file

@ -487,48 +487,62 @@ _auth_status = "unknown" # unknown | logged_in | logged_out | pending
async def _check_claude_auth() -> str: async def _check_claude_auth() -> str:
"""Check if claude is authenticated by running a quick no-op""" """Check if claude is authenticated by running claude auth status"""
global _auth_status global _auth_status
try: try:
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
"claude", "--version", "claude", "auth", "status",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
) )
await asyncio.wait_for(proc.communicate(), timeout=5) stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10)
# Check for saved credentials file output = stdout.decode(errors="replace").strip()
creds = Path("/root/.claude/.credentials.json") logger.info(f"claude auth status output: {output}")
if creds.exists(): try:
import json as _json import json as _json
data = _json.loads(creds.read_text()) data = _json.loads(output)
if data: if data.get("loggedIn"):
_auth_status = "logged_in" _auth_status = "logged_in"
return "logged_in" return "logged_in"
except Exception:
pass
_auth_status = "logged_out" _auth_status = "logged_out"
return "logged_out" return "logged_out"
except Exception: except Exception as e:
logger.error(f"Auth check failed: {e}")
_auth_status = "unknown" _auth_status = "unknown"
return "unknown" return "unknown"
@app.get("/api/auth/status") @app.get("/api/auth/status")
async def auth_status(): async def auth_status():
"""Check Claude auth status""" """Check Claude auth status using claude auth status JSON"""
global _auth_status, _auth_url global _auth_status, _auth_url
status = await _check_claude_auth()
# Try to get account info if logged in
account = None account = None
auth_method = None
try: try:
creds_file = Path("/root/.claude/.credentials.json") proc = await asyncio.create_subprocess_exec(
if creds_file.exists(): "claude", "auth", "status",
import json as _json stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
creds = _json.loads(creds_file.read_text()) )
account = creds.get("account", {}).get("emailAddress") or creds.get("email") stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10)
except Exception: output = stdout.decode(errors="replace").strip()
import json as _json
data = _json.loads(output)
if data.get("loggedIn"):
_auth_status = "logged_in"
account = data.get("email") or data.get("account", {}).get("emailAddress")
auth_method = data.get("authMethod")
else:
_auth_status = "logged_out"
except Exception as e:
logger.error(f"Auth status check failed: {e}")
# Fall back to existing status
pass pass
return { return {
"status": status, "status": _auth_status,
"account": account, "account": account,
"auth_url": _auth_url if status == "pending" else None "auth_method": auth_method,
"auth_url": _auth_url if _auth_status == "pending" else None
} }
@ -540,41 +554,56 @@ async def auth_login():
_auth_url = None _auth_url = None
try: try:
# Run claude auth login and capture the URL it prints # Run claude auth login --claudeai and capture the OAuth URL
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
"claude", "auth", "login", "claude", "auth", "login", "--claudeai",
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT, stderr=asyncio.subprocess.STDOUT,
stdin=asyncio.subprocess.DEVNULL stdin=asyncio.subprocess.DEVNULL
) )
_auth_process = proc _auth_process = proc
# Read output lines looking for the OAuth URL (30s timeout) # Read output looking for the OAuth URL
url = None url = None
try: try:
async def _read_url(): async def _read_url():
collected = ""
while True: while True:
line = await proc.stdout.readline() line = await proc.stdout.readline()
if not line: if not line:
break break
text = line.decode(errors="replace") text = line.decode(errors="replace")
collected += text
logger.info(f"claude auth: {text.strip()}") logger.info(f"claude auth: {text.strip()}")
# Claude prints something like: "Open this URL: https://..." # The URL contains /oauth/authorize and starts with https://
for part in text.split(): for part in text.split():
if part.startswith("https://claude.ai") or part.startswith("https://anthropic"): cleaned = part.strip().rstrip("\\n")
return part.strip() if cleaned.startswith("https://") and "oauth" in cleaned:
return cleaned
if cleaned.startswith("https://claude.com"):
return cleaned
if cleaned.startswith("https://console.anthropic.com"):
return cleaned
logger.info(f"Full auth output: {collected}")
return None return None
url = await asyncio.wait_for(_read_url(), timeout=30) url = await asyncio.wait_for(_read_url(), timeout=15)
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass logger.warning("Timed out waiting for auth URL")
# Don't wait for the process to finish — it blocks waiting for browser callback
# It will complete in the background when user finishes OAuth in browser
if url: if url:
_auth_url = url _auth_url = url
return {"status": "pending", "auth_url": url, "message": "Open this URL in your browser to log in"} return {"status": "pending", "auth_url": url, "message": "Open this URL in your browser to log in with Claude Max"}
else: else:
return {"status": "error", "message": "Could not extract login URL from claude CLI. Try running 'claude auth login' manually in the container."} return {
"status": "error",
"message": "Could not extract login URL. Try running manually: docker exec -it claude-persistent-agent claude auth login --claudeai"
}
except Exception as e: except Exception as e:
logger.error(f"Auth login failed: {e}")
return {"status": "error", "message": str(e)} return {"status": "error", "message": str(e)}