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",
|
codex_cli_simplified_flow: "true",
|
||||||
originator: "codex_cli_rs",
|
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: {
|
refresh: {
|
||||||
encoding: "form",
|
encoding: "form",
|
||||||
scope: "openid profile email offline_access",
|
scope: "openid profile email offline_access",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import "open-sse/index.js";
|
|||||||
|
|
||||||
import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb";
|
import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb";
|
||||||
import { getUsageForProvider } from "open-sse/services/usage.js";
|
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 { getExecutor } from "open-sse/executors/index.js";
|
||||||
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
|
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
|
||||||
import { USAGE_APIKEY_PROVIDERS } from "@/shared/constants/providers";
|
import { USAGE_APIKEY_PROVIDERS } from "@/shared/constants/providers";
|
||||||
@@ -21,6 +22,11 @@ function isAuthExpiredMessage(usage) {
|
|||||||
* @returns Promise<{ connection, refreshed: boolean }>
|
* @returns Promise<{ connection, refreshed: boolean }>
|
||||||
*/
|
*/
|
||||||
export async function refreshAndUpdateCredentials(connection, force = false, proxyOptions = null) {
|
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);
|
const executor = getExecutor(connection.provider);
|
||||||
|
|
||||||
// Build credentials object from connection
|
// 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)
|
// Use executor's refreshCredentials method (with optional proxy)
|
||||||
const refreshResult = await executor.refreshCredentials(credentials, console, proxyOptions);
|
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) {
|
if (!refreshResult) {
|
||||||
// Refresh failed but we still have an accessToken — try with existing token
|
// Refresh failed but we still have an accessToken — try with existing token
|
||||||
if (connection.accessToken) {
|
if (connection.accessToken) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Re-export from open-sse with local logger
|
// Re-export from open-sse with local logger
|
||||||
import * as log from "../utils/logger.js";
|
import * as log from "../utils/logger.js";
|
||||||
import { updateProviderConnection } from "../../lib/localDb.js";
|
import { getProviderConnectionById, updateProviderConnection } from "../../lib/localDb.js";
|
||||||
import {
|
import {
|
||||||
getProjectIdForConnection,
|
getProjectIdForConnection,
|
||||||
invalidateProjectId,
|
invalidateProjectId,
|
||||||
@@ -227,6 +227,25 @@ export async function checkAndRefreshToken(provider, credentials, options = {})
|
|||||||
creds.connectionId = creds.id;
|
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;
|
const force = options?.force === true;
|
||||||
|
|
||||||
// ── 1. Regular access-token expiry ────────────────────────────────────────
|
// ── 1. Regular access-token expiry ────────────────────────────────────────
|
||||||
|
|||||||
Reference in new issue
Block a user