Add auth code input flow for headless OAuth
This commit is contained in:
parent
91cedaed5c
commit
68968fe7f4
1 changed files with 69 additions and 13 deletions
|
|
@ -550,16 +550,24 @@ async def auth_status():
|
||||||
async def auth_login():
|
async def auth_login():
|
||||||
"""Initiate Claude OAuth login - returns the URL to open in browser"""
|
"""Initiate Claude OAuth login - returns the URL to open in browser"""
|
||||||
global _auth_process, _auth_url, _auth_status
|
global _auth_process, _auth_url, _auth_status
|
||||||
|
|
||||||
|
# Kill any existing auth process
|
||||||
|
if _auth_process and _auth_process.returncode is None:
|
||||||
|
try:
|
||||||
|
_auth_process.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
_auth_status = "pending"
|
_auth_status = "pending"
|
||||||
_auth_url = None
|
_auth_url = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Run claude auth login --claudeai and capture the OAuth URL
|
# Run claude auth login --claudeai with stdin PIPE so we can send the code later
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"claude", "auth", "login", "--claudeai",
|
"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.PIPE
|
||||||
)
|
)
|
||||||
_auth_process = proc
|
_auth_process = proc
|
||||||
|
|
||||||
|
|
@ -575,14 +583,9 @@ async def auth_login():
|
||||||
text = line.decode(errors="replace")
|
text = line.decode(errors="replace")
|
||||||
collected += text
|
collected += text
|
||||||
logger.info(f"claude auth: {text.strip()}")
|
logger.info(f"claude auth: {text.strip()}")
|
||||||
# The URL contains /oauth/authorize and starts with https://
|
|
||||||
for part in text.split():
|
for part in text.split():
|
||||||
cleaned = part.strip().rstrip("\\n")
|
cleaned = part.strip().rstrip("\\n")
|
||||||
if cleaned.startswith("https://") and "oauth" in cleaned:
|
if cleaned.startswith("https://") and ("oauth" in cleaned or "claude.com" in cleaned):
|
||||||
return cleaned
|
|
||||||
if cleaned.startswith("https://claude.com"):
|
|
||||||
return cleaned
|
|
||||||
if cleaned.startswith("https://console.anthropic.com"):
|
|
||||||
return cleaned
|
return cleaned
|
||||||
logger.info(f"Full auth output: {collected}")
|
logger.info(f"Full auth output: {collected}")
|
||||||
return None
|
return None
|
||||||
|
|
@ -590,16 +593,17 @@ async def auth_login():
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.warning("Timed out waiting for auth URL")
|
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 with Claude Max"}
|
return {
|
||||||
|
"status": "pending",
|
||||||
|
"auth_url": url,
|
||||||
|
"message": "Open this URL, authorize, then paste the code you receive back here."
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"message": "Could not extract login URL. Try running manually: docker exec -it claude-persistent-agent claude auth login --claudeai"
|
"message": "Could not extract login URL. Try: docker exec -it claude-persistent-agent claude auth login --claudeai"
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -607,6 +611,58 @@ async def auth_login():
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
class AuthCode(BaseModel):
|
||||||
|
code: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/auth/code")
|
||||||
|
async def auth_submit_code(payload: AuthCode):
|
||||||
|
"""Submit the OAuth authorization code from the browser callback"""
|
||||||
|
global _auth_process, _auth_status
|
||||||
|
|
||||||
|
if not _auth_process or _auth_process.returncode is not None:
|
||||||
|
return {"status": "error", "message": "No active login session. Click 'Login' first."}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Send the code to the waiting claude auth login process via stdin
|
||||||
|
code = payload.code.strip() + "\n"
|
||||||
|
_auth_process.stdin.write(code.encode())
|
||||||
|
await _auth_process.stdin.drain()
|
||||||
|
_auth_process.stdin.close()
|
||||||
|
|
||||||
|
# Wait for the process to finish
|
||||||
|
try:
|
||||||
|
stdout, _ = await asyncio.wait_for(_auth_process.communicate(), timeout=30)
|
||||||
|
output = stdout.decode(errors="replace") if stdout else ""
|
||||||
|
logger.info(f"Auth code result: {output}")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.warning("Auth process timed out after submitting code")
|
||||||
|
|
||||||
|
# Check if login succeeded
|
||||||
|
check_proc = await asyncio.create_subprocess_exec(
|
||||||
|
"claude", "auth", "status",
|
||||||
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
check_stdout, _ = await asyncio.wait_for(check_proc.communicate(), timeout=10)
|
||||||
|
check_output = check_stdout.decode(errors="replace").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json as _json
|
||||||
|
data = _json.loads(check_output)
|
||||||
|
if data.get("loggedIn"):
|
||||||
|
_auth_status = "logged_in"
|
||||||
|
return {"status": "logged_in", "message": "Successfully logged in!", "account": data.get("email")}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
_auth_status = "logged_out"
|
||||||
|
return {"status": "error", "message": f"Login may have failed. Auth status: {check_output}"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Auth code submission failed: {e}")
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/auth/logout")
|
@app.post("/api/auth/logout")
|
||||||
async def auth_logout():
|
async def auth_logout():
|
||||||
"""Log out of Claude"""
|
"""Log out of Claude"""
|
||||||
|
|
|
||||||
Reference in a new issue