import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { Buffer } from "node:buffer"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import type { FetchImpl } from "../../types"; import { __resetVertexTokenCache, getVertexAccessToken } from "../google-auth"; const CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; const JWT_BEARER_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer"; /** Generate a real RS256 private key so signJwtRs256 / pemToPkcs8 run for real. */ async function generateServiceAccountPem(): Promise { const keyPair = (await globalThis.crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, true, ["sign", "verify"], )) as CryptoKeyPair; const pkcs8 = new Uint8Array(await globalThis.crypto.subtle.exportKey("pkcs8", keyPair.privateKey)); const body = ( Buffer.from(pkcs8) .toString("base64") .match(/.{1,64}/g) ?? [] ).join("\n"); return `-----BEGIN PRIVATE KEY-----\n${body}\n-----END PRIVATE KEY-----\n`; } function urlOf(input: string | URL | Request): string { if (typeof input === "string") return input; if (input instanceof URL) return input.toString(); return input.url; } describe("getVertexAccessToken impersonated_service_account ADC", () => { let tmpDir: string; let originalGac: string | undefined; beforeEach(async () => { __resetVertexTokenCache(); originalGac = Bun.env.GOOGLE_APPLICATION_CREDENTIALS; tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-vertex-adc-")); }); afterEach(async () => { __resetVertexTokenCache(); if (originalGac === undefined) delete Bun.env.GOOGLE_APPLICATION_CREDENTIALS; else Bun.env.GOOGLE_APPLICATION_CREDENTIALS = originalGac; await fs.rm(tmpDir, { recursive: true, force: true }); }); it("rejects a malformed service_account_impersonation_url before any network call", async () => { const adcPath = path.join(tmpDir, "impersonated-bad-url.json"); await Bun.write( adcPath, JSON.stringify({ type: "impersonated_service_account", // Missing the trailing ":generateAccessToken" the principal parser requires. service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com", source_credentials: { type: "authorized_user", client_id: "client-id", client_secret: "client-secret", refresh_token: "refresh-token", }, }), ); Bun.env.GOOGLE_APPLICATION_CREDENTIALS = adcPath; const calls: string[] = []; const fetchImpl: FetchImpl = async input => { calls.push(urlOf(input)); return new Response("{}"); }; // The principal is parsed before the source exchange, so a bad URL must fail // up front rather than after burning a source-token round trip. await expect(getVertexAccessToken({ fetch: fetchImpl })).rejects.toBeInstanceOf(RangeError); expect(calls).toEqual([]); }); it("signs an RS256 JWT for a service_account source and reconstructs the IAM URL", async () => { const pem = await generateServiceAccountPem(); const adcPath = path.join(tmpDir, "impersonated-sa.json"); await Bun.write( adcPath, JSON.stringify({ type: "impersonated_service_account", // Non-canonical project segment proves the request URL is rebuilt, not echoed. service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/explicit-proj/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken", source_credentials: { type: "service_account", client_email: "source@project.iam.gserviceaccount.com", private_key: pem, private_key_id: "key-1", }, // delegates intentionally omitted — the IAM body must default to []. }), ); Bun.env.GOOGLE_APPLICATION_CREDENTIALS = adcPath; const calls: { url: string; init?: RequestInit }[] = []; const fetchImpl: FetchImpl = async (input, init) => { const url = urlOf(input); calls.push({ url, init }); if (url === "https://oauth2.googleapis.com/token") { return new Response(JSON.stringify({ access_token: "sa-source-token", expires_in: 3600 })); } if (url.startsWith("https://iamcredentials.googleapis.com/")) { return new Response( JSON.stringify({ accessToken: "impersonated-token", expireTime: new Date(Date.now() + 3_600_000).toISOString(), }), ); } return new Response("unexpected", { status: 404 }); }; const token = await getVertexAccessToken({ fetch: fetchImpl }); expect(token).toBe("impersonated-token"); // Source JWT exchange happens first, then the impersonation exchange. expect(calls.map(c => c.url)).toEqual([ "https://oauth2.googleapis.com/token", "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken", ]); // Source credential is exchanged via a signed JWT bearer assertion, not a refresh grant. const sourceBody = new URLSearchParams(String(calls[0].init?.body)); expect(sourceBody.get("grant_type")).toBe(JWT_BEARER_GRANT); expect((sourceBody.get("assertion") ?? "").split(".")).toHaveLength(3); // The IAM call carries the source-derived bearer token and defaults delegates to []. const iamHeaders = calls[1].init?.headers as Record; expect(iamHeaders.Authorization).toBe("Bearer sa-source-token"); expect(JSON.parse(String(calls[1].init?.body))).toEqual({ delegates: [], scope: [CLOUD_PLATFORM_SCOPE], lifetime: "3600s", }); }); });