Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions open-sse/services/tokenRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/token-refresh-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down
Loading