fix(google): reject unsafe vertex adc lifetimes

This commit is contained in:
Peter Steinberger
2026-05-29 13:57:34 -04:00
parent b9d7dd4a84
commit e3be541a6c
2 changed files with 60 additions and 5 deletions

View File

@@ -880,6 +880,48 @@ describe("google transport stream", () => {
expect(result.content).toEqual([{ type: "text", text: "ok" }]);
});
it("does not reuse authorized_user ADC tokens with unsafe expiry lifetimes", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-unsafe-adc-"));
const credentialsPath = path.join(tempDir, "application_default_credentials.json");
await writeFile(
credentialsPath,
JSON.stringify({
type: "authorized_user",
client_id: "client-id",
client_secret: "client-secret",
refresh_token: "refresh-token",
}),
"utf8",
);
vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", credentialsPath);
const tokenFetchMock = vi
.fn()
.mockResolvedValueOnce(
new Response(
JSON.stringify({
access_token: "ya29.unsafe-token",
expires_in: Number.MAX_SAFE_INTEGER,
}),
{ status: 200, headers: { "content-type": "application/json" } },
),
)
.mockResolvedValueOnce(
new Response(JSON.stringify({ access_token: "ya29.fresh-token", expires_in: 3600 }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).resolves.toEqual({
Authorization: "Bearer ya29.unsafe-token",
});
await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).resolves.toEqual({
Authorization: "Bearer ya29.fresh-token",
});
expect(tokenFetchMock).toHaveBeenCalledTimes(2);
});
it("refreshes authorized_user ADC from the Windows APPDATA fallback for Google Vertex requests", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-appdata-adc-"));
const homeDir = path.join(tempDir, "home");

View File

@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
import { readFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { resolveExpiresAtMsFromDurationSeconds } from "openclaw/plugin-sdk/number-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
type GoogleAuthorizedUserCredentials = {
@@ -30,6 +31,7 @@ const GOOGLE_VERTEX_OAUTH_SCOPE = "https://www.googleapis.com/auth/cloud-platfor
// is a 60s buffer) so we don't ship a request that's already revoked when it
// leaves the gateway.
const GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS = 60_000;
const GOOGLE_VERTEX_DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
let cachedGoogleVertexAuthorizedUserToken: GoogleVertexAuthorizedUserToken | undefined;
let cachedGoogleAuthClient:
@@ -41,6 +43,20 @@ let cachedGoogleAuthClient:
| undefined;
let cachedGoogleVertexAdcToken: GoogleVertexAdcToken | undefined;
function resolveAuthorizedUserTokenExpiresAtMs(value: unknown, nowMs: number): number {
if (typeof value === "number" && Number.isFinite(value)) {
return (
resolveExpiresAtMsFromDurationSeconds(Math.max(1, value), { nowMs }) ??
nowMs - GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS
);
}
return (
resolveExpiresAtMsFromDurationSeconds(GOOGLE_VERTEX_DEFAULT_TOKEN_LIFETIME_SECONDS, {
nowMs,
}) ?? nowMs - GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS
);
}
export function resetGoogleVertexAuthorizedUserTokenCacheForTest(): void {
cachedGoogleVertexAuthorizedUserToken = undefined;
cachedGoogleAuthClient = undefined;
@@ -191,13 +207,10 @@ async function refreshGoogleVertexAuthorizedUserAccessToken(params: {
if (!token) {
throw new Error("Google Vertex ADC token refresh response did not include an access_token.");
}
const expiresInSeconds =
typeof payload?.expires_in === "number" && Number.isFinite(payload.expires_in)
? payload.expires_in
: 3600;
const nowMs = Date.now();
cachedGoogleVertexAuthorizedUserToken = {
token,
expiresAtMs: Date.now() + Math.max(1, expiresInSeconds) * 1000,
expiresAtMs: resolveAuthorizedUserTokenExpiresAtMs(payload?.expires_in, nowMs),
credentialsPath: params.credentialsPath,
refreshToken,
};