Fix OAuth login flow for Claude Max
This commit is contained in:
parent
e00b112da1
commit
39c18ac520
1 changed files with 59 additions and 30 deletions
|
|
@ -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)}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Reference in a new issue