fix(auth): fall through to env/fallback when OAuth credential has no registered provider (#2097)

Fixes #2083

When an OpenRouter API key is stored in auth.json as type:"oauth" (instead
of type:"api_key"), getApiKey() calls getOAuthProvider("openrouter") which
returns undefined — OpenRouter is not a registered OAuth provider. Previously,
resolveCredentialApiKey returned undefined and getApiKey returned that directly,
never reaching the env-var or fallback-resolver paths.

Now, when resolveCredentialApiKey returns undefined, getApiKey falls through
to OPENROUTER_API_KEY env var and the fallback resolver instead of silently
failing with "Authentication failed."

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-23 12:03:05 -04:00 committed by GitHub
parent f4ee51017a
commit 297845f10c
2 changed files with 73 additions and 2 deletions

View file

@ -263,6 +263,74 @@ describe("AuthStorage — areAllCredentialsBackedOff", () => {
});
});
// ─── mismatched oauth credential for non-OAuth provider (#2083) ───────────────
describe("AuthStorage — oauth credential for non-OAuth provider (#2083)", () => {
it("returns undefined when openrouter has type:oauth (no registered OAuth provider)", async () => {
// Simulates the bug: OpenRouter credential stored as type:"oauth"
// but OpenRouter is not a registered OAuth provider.
const storage = inMemory({
openrouter: {
type: "oauth",
access_token: "sk-or-v1-fake",
refresh_token: "rt-fake",
expires: Date.now() + 3_600_000,
},
});
// Before the fix, getApiKey returns undefined because
// resolveCredentialApiKey calls getOAuthProvider("openrouter") → null → undefined.
// The key in the oauth credential is never extracted.
const key = await storage.getApiKey("openrouter");
// After the fix, the oauth credential with an unrecognised provider
// should be skipped, and getApiKey should fall through to env / fallback.
assert.equal(key, undefined);
});
it("falls through to env var when openrouter has type:oauth credential", async () => {
const storage = inMemory({
openrouter: {
type: "oauth",
access_token: "sk-or-v1-fake",
refresh_token: "rt-fake",
expires: Date.now() + 3_600_000,
},
});
// Simulate OPENROUTER_API_KEY being set via env
const origEnv = process.env.OPENROUTER_API_KEY;
try {
process.env.OPENROUTER_API_KEY = "sk-or-v1-env-key";
const key = await storage.getApiKey("openrouter");
assert.equal(key, "sk-or-v1-env-key");
} finally {
if (origEnv === undefined) {
delete process.env.OPENROUTER_API_KEY;
} else {
process.env.OPENROUTER_API_KEY = origEnv;
}
}
});
it("falls through to fallback resolver when openrouter has type:oauth credential", async () => {
const storage = inMemory({
openrouter: {
type: "oauth",
access_token: "sk-or-v1-fake",
refresh_token: "rt-fake",
expires: Date.now() + 3_600_000,
},
});
storage.setFallbackResolver((provider) =>
provider === "openrouter" ? "sk-or-v1-fallback" : undefined,
);
const key = await storage.getApiKey("openrouter");
assert.equal(key, "sk-or-v1-fallback");
});
});
// ─── getAll truncation ────────────────────────────────────────────────────────
describe("AuthStorage — getAll()", () => {

View file

@ -756,9 +756,12 @@ export class AuthStorage {
if (credentials.length > 0) {
const index = this.selectCredentialIndex(providerId, credentials, sessionId);
if (index >= 0) {
return this.resolveCredentialApiKey(providerId, credentials[index]);
const resolved = await this.resolveCredentialApiKey(providerId, credentials[index]);
if (resolved) return resolved;
// Credential unresolvable (e.g. type:"oauth" for a non-OAuth provider) —
// fall through to env / fallback instead of returning undefined (#2083)
}
// All credentials backed off - fall through to env/fallback
// All credentials backed off or unresolvable - fall through to env/fallback
}
// Fall back to environment variable