fix(codex): stop refresh-token reuse that logs accounts out on auto-ping
OpenAI rotates the refresh token on every refresh and revokes the whole session on reuse. A 5-day refreshLeadMs (access tokens live ~1h) rotated the token on every call, and three refresh writers (usage poll, auto-ping tick, 5-min background refresher) each held stale snapshots — auto-ping firing at reset time reliably triggered reuse and logged the account out. - registry: refreshLeadMs 5d -> 10min (refresh only near actual expiry) - refreshAndUpdateCredentials: re-read connection from DB before refresh; throw on unrecoverable refresh instead of continuing with a dead token - checkAndRefreshToken: adopt newer DB tokens before refreshing Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
9f41ee754b
commit
0bc7f86e4b
3 files changed
+34
-2
No files matched your search
@@ -103,7 +103,9 @@ export default {
|
||||
codex_cli_simplified_flow: "true",
|
||||
originator: "codex_cli_rs",
|
||||
},
|
||||
refreshLeadMs: 432000000,
|
||||
// Access tokens live ~1h; a 5d lead rotated the refresh token on EVERY call —
|
||||
// reuse of a rotated token revokes the whole OpenAI session (account logout).
|
||||
refreshLeadMs: 600000,
|
||||
refresh: {
|
||||
encoding: "form",
|
||||
scope: "openid profile email offline_access",
|
||||
|
||||
@@ -3,6 +3,7 @@ import "open-sse/index.js";
|
||||
|
||||
import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb";
|
||||
import { getUsageForProvider } from "open-sse/services/usage.js";
|
||||
import { isUnrecoverableRefreshError } from "open-sse/services/tokenRefresh.js";
|
||||
import { getExecutor } from "open-sse/executors/index.js";
|
||||
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
|
||||
import { USAGE_APIKEY_PROVIDERS } from "@/shared/constants/providers";
|
||||
@@ -21,6 +22,11 @@ function isAuthExpiredMessage(usage) {
|
||||
* @returns Promise<{ connection, refreshed: boolean }>
|
||||
*/
|
||||
export async function refreshAndUpdateCredentials(connection, force = false, proxyOptions = null) {
|
||||
// Re-read latest tokens: OpenAI rotates the refresh token on every refresh, and
|
||||
// refreshing with a stale snapshot (reuse) revokes the whole session → account logout.
|
||||
const latest = connection.id ? await getProviderConnectionById(connection.id) : null;
|
||||
if (latest) connection = latest;
|
||||
|
||||
const executor = getExecutor(connection.provider);
|
||||
|
||||
// Build credentials object from connection
|
||||
@@ -47,6 +53,11 @@ export async function refreshAndUpdateCredentials(connection, force = false, pro
|
||||
// Use executor's refreshCredentials method (with optional proxy)
|
||||
const refreshResult = await executor.refreshCredentials(credentials, console, proxyOptions);
|
||||
|
||||
// Refresh token reused/invalidated — token family is revoked; do not continue with the dead token.
|
||||
if (refreshResult && isUnrecoverableRefreshError(refreshResult)) {
|
||||
throw new Error("Refresh token invalid or reused. Please re-authorize the connection.");
|
||||
}
|
||||
|
||||
if (!refreshResult) {
|
||||
// Refresh failed but we still have an accessToken — try with existing token
|
||||
if (connection.accessToken) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Re-export from open-sse with local logger
|
||||
import * as log from "../utils/logger.js";
|
||||
import { updateProviderConnection } from "../../lib/localDb.js";
|
||||
import { getProviderConnectionById, updateProviderConnection } from "../../lib/localDb.js";
|
||||
import {
|
||||
getProjectIdForConnection,
|
||||
invalidateProjectId,
|
||||
@@ -227,6 +227,25 @@ export async function checkAndRefreshToken(provider, credentials, options = {})
|
||||
creds.connectionId = creds.id;
|
||||
}
|
||||
|
||||
// Adopt latest DB tokens: OpenAI rotates the refresh token on every refresh, and
|
||||
// refreshing with a stale snapshot (reuse) revokes the whole session → account logout.
|
||||
if (creds.connectionId) {
|
||||
const latest = await getProviderConnectionById(creds.connectionId).catch(() => null);
|
||||
const latestRefreshMs = Date.parse(latest?.lastRefreshAt || "");
|
||||
const credsRefreshMs = Date.parse(creds.lastRefreshAt || "");
|
||||
const dbIsNewer = Number.isFinite(latestRefreshMs)
|
||||
&& (!Number.isFinite(credsRefreshMs) || latestRefreshMs > credsRefreshMs);
|
||||
if (dbIsNewer && latest.refreshToken && latest.refreshToken !== creds.refreshToken) {
|
||||
creds = {
|
||||
...creds,
|
||||
refreshToken: latest.refreshToken,
|
||||
accessToken: latest.accessToken || creds.accessToken,
|
||||
expiresAt: latest.expiresAt || latest.tokenExpiresAt || creds.expiresAt,
|
||||
lastRefreshAt: latest.lastRefreshAt || creds.lastRefreshAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const force = options?.force === true;
|
||||
|
||||
// ── 1. Regular access-token expiry ────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user