mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(google): reject unsafe vertex adc lifetimes
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user