fix: implement two-step ChatGPT auth (session cookie → access token)

The __Secure-next-auth.session-token cannot be used directly as a Bearer
token. It must first be exchanged via GET /api/auth/session (with the token
sent as a Cookie) to obtain a short-lived accessToken. This accessToken is
then used as the Authorization: Bearer header for all backend-api calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
JesseMarkowitz
2026-02-27 23:37:42 -05:00
parent b41634d892
commit 6a33de682a

View File

@@ -4,19 +4,23 @@ import logging
import os import os
from typing import Any from typing import Any
from src.providers.base import BaseProvider, ProviderError from src.providers.base import BaseProvider, ProviderError, REQUEST_TIMEOUT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
BASE_URL = "https://chatgpt.com/backend-api" BASE_URL = "https://chatgpt.com/backend-api"
AUTH_SESSION_URL = "https://chatgpt.com/api/auth/session"
class ChatGPTProvider(BaseProvider): class ChatGPTProvider(BaseProvider):
"""Provider for ChatGPT conversations via the internal web API. """Provider for ChatGPT conversations via the internal web API.
Authentication: Authorization: Bearer <CHATGPT_SESSION_TOKEN> Authentication is a two-step process:
Token: __Secure-next-auth.session-token cookie value (a JWT). 1. Send __Secure-next-auth.session-token as a Cookie header to
Typical validity: ~7 days. /api/auth/session to obtain a short-lived accessToken.
2. Use that accessToken as the Bearer token for all backend-api calls.
Token: __Secure-next-auth.session-token cookie (~7 day lifetime).
""" """
provider_name = "chatgpt" provider_name = "chatgpt"
@@ -33,20 +37,60 @@ class ChatGPTProvider(BaseProvider):
"Run 'python -m src.main auth' to configure it." "Run 'python -m src.main auth' to configure it."
), ),
) )
# Never log the token value self._session_token = token
self._session.headers.update( self._session.headers.update(
{ {
"Authorization": f"Bearer {token}",
"Referer": "https://chatgpt.com/", "Referer": "https://chatgpt.com/",
"Origin": "https://chatgpt.com", "Origin": "https://chatgpt.com",
} }
) )
logger.debug("[chatgpt] Session initialised (token: [REDACTED])") # Exchange the session cookie for an access token immediately
self._access_token: str = self._fetch_access_token(token)
self._session.headers["Authorization"] = f"Bearer {self._access_token}"
logger.debug("[chatgpt] Session initialised — access token obtained (token: [REDACTED])")
def _fetch_access_token(self, session_token: str) -> str:
"""Exchange the session cookie for a Bearer access token.
Calls GET /api/auth/session with the session cookie, which returns
{"accessToken": "...", "user": {...}}.
"""
logger.debug("[chatgpt] Fetching access token from %s", AUTH_SESSION_URL)
try:
resp = self._session.get(
AUTH_SESSION_URL,
headers={"Cookie": f"__Secure-next-auth.session-token={session_token}"},
timeout=REQUEST_TIMEOUT,
)
resp.raise_for_status()
data = resp.json()
except Exception as e:
raise ProviderError(
self.provider_name,
"fetch_access_token",
RuntimeError(
f"Could not exchange session token for access token: {e}. "
"Check that your CHATGPT_SESSION_TOKEN is current."
),
) from e
access_token = data.get("accessToken")
if not access_token:
raise ProviderError(
self.provider_name,
"fetch_access_token",
RuntimeError(
"No accessToken in /api/auth/session response. "
"Your session token may be expired — run 'python -m src.main auth' to refresh."
),
)
return access_token
def _handle_401(self) -> None: def _handle_401(self) -> None:
msg = ( msg = (
"[chatgpt] Authentication failed (401 Unauthorized). " "[chatgpt] Authentication failed (401 Unauthorized). "
"Your __Secure-next-auth.session-token has likely expired (~7 day lifetime). " "Your __Secure-next-auth.session-token has likely expired (~7 day lifetime). "
"The session token is used to obtain a short-lived access token via /api/auth/session. "
"To refresh: open chatgpt.com in Chrome → F12 → Application → Cookies " "To refresh: open chatgpt.com in Chrome → F12 → Application → Cookies "
"→ find '__Secure-next-auth.session-token' → copy the value. " "→ find '__Secure-next-auth.session-token' → copy the value. "
"Then run 'python -m src.main auth' or update CHATGPT_SESSION_TOKEN in .env." "Then run 'python -m src.main auth' or update CHATGPT_SESSION_TOKEN in .env."