diff --git a/open-sse/services/tokenRefresh.ts b/open-sse/services/tokenRefresh.ts index 34eac23a74..507e9ff27b 100755 --- a/open-sse/services/tokenRefresh.ts +++ b/open-sse/services/tokenRefresh.ts @@ -1063,6 +1063,26 @@ export async function refreshCodexToken(refreshToken, log, proxyConfig: unknown return { error: "unrecoverable_refresh_error", code: errorCode }; } + // Defense-in-depth (port from decolua/9router#1821): any 401 from OpenAI's + // OAuth token endpoint means the refresh credential itself was rejected + // (e.g. rotated away, or a payload variant whose code we do not yet + // recognize — OpenAI has shipped both `token_expired` and the bare + // "Could not validate your token" message). Retrying with the same dead + // refresh token will never succeed; surface re-auth instead of looping. + // 429 / 5xx remain transient and fall through to the retryable branch. + if (response.status === 401) { + const code = errorCode || "unauthorized"; + log?.error?.( + "TOKEN_REFRESH", + "Codex OAuth token endpoint returned 401. Re-authentication required.", + { + status: response.status, + errorCode: code, + } + ); + return { error: "unrecoverable_refresh_error", code }; + } + log?.error?.("TOKEN_REFRESH", "Failed to refresh Codex token", { status: response.status, error: errorText, diff --git a/tests/unit/token-refresh-service.test.ts b/tests/unit/token-refresh-service.test.ts index 1574a79626..8b2f3e09d0 100644 --- a/tests/unit/token-refresh-service.test.ts +++ b/tests/unit/token-refresh-service.test.ts @@ -438,6 +438,37 @@ test("refreshCodexToken recognizes refresh_token_reused responses", async () => ); }); +// Port from decolua/9router#1821 (sacwooky): a 401 from OpenAI's OAuth token +// endpoint means the refresh credential itself was rejected (e.g. rotated away +// or a payload whose error code we do not yet recognize). Retrying with the +// same refresh token will never succeed — surface re-auth, do not loop. +test("refreshCodexToken treats any 401 from the token endpoint as unrecoverable", async () => { + const log = createLog(); + + await withMockedFetch( + async () => + textResponse( + JSON.stringify({ + error: { + // A payload variant whose code/type are NOT in the existing + // unrecoverable set — only the 401 status proves the token is dead. + message: "Could not validate your token. Please try signing in again.", + type: "invalid_request_error", + }, + }), + 401 + ), + async () => { + const result = await refreshCodexToken("codex-refresh", log); + assert.equal( + result?.error, + "unrecoverable_refresh_error", + "401 from OpenAI token endpoint must surface re-auth instead of returning null (which triggers retry)" + ); + } + ); +}); + test("refreshKiroToken uses the AWS OIDC flow when client credentials are present", async () => { const log = createLog(); const calls: any[] = [];