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:
@@ -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."
|
||||||
|
|||||||
Reference in New Issue
Block a user