Skip to content

Impersonated credentials from google-auth-library v10 don't work with getSignedUrl()Β #2691

@instilled

Description

@instilled

Please make sure you have searched for information in the following guides.

A screenshot that you have tested with "Try this API".

The api is not the problem.

Link to the code that reproduces this issue. A link to a public Github Repository or gist with a minimal reproduction.

https://gist.github.com/instilled/0d98c90cd59cf625f496ddebbf0d522d

A step-by-step description of how to reproduce the issue, based on the linked reproduction.

  import { Storage } from "@google-cloud/storage";
  import { GoogleAuth, Impersonated } from "google-auth-library";

  const auth = new GoogleAuth({
    scopes: ["https://www.googleapis.com/auth/cloud-platform"],
  });
  const sourceClient = await auth.getClient();

  const impersonatedCreds = new Impersonated({
    sourceClient,
    targetPrincipal: "my-sa@project.iam.gserviceaccount.com",
    targetScopes: ["https://www.googleapis.com/auth/devstorage.read_write"],
    lifetime: 3600,
  });

  const storage = new Storage({
    authClient: impersonatedCreds,
    projectId: "my-project",
  });

  // This fails with multiple errors
  const [signedUrl] = await storage
    .bucket("my-bucket")
    .file("test.txt")
    .getSignedUrl({
      version: "v4",
      action: "write",
      expires: Date.now() + 3600000,
    });

A clear and concise description of what the bug is, and what you expected to happen.

Environment

  • @google-cloud/storage version: 7.18.0
  • google-auth-library version: 10.5.0
  • Node.js version: 24.x (Bun 1.3.5)
  • OS: macOS

Problem

When using Impersonated credentials from google-auth-library@10.x with @google-cloud/storage@7.x, calling getSignedUrl() fails. There are three separate issues:

Issue 1: authClient option is ignored

The Storage constructor ignores the authClient option and creates its own GoogleAuth instance internally:

 const storage = new Storage({
   authClient: impersonatedCreds,  // This is ignored!
   projectId: "my-project",
 });

 console.log(storage.authClient === impersonatedCreds); // false - Storage creates its own GoogleAuth

Issue 2: Impersonated class lacks getCredentials() method

The URLSigner class calls this.auth.getCredentials() at https://github.com/googleapis/nodejs-storage/blob/main/src/signer.ts#L168, but the Impersonated class from google-auth-library@10.x doesn't implement this method.

Issue 3: Impersonated.sign() returns object instead of string

The URLSigner expects sign() to return a base64 string, but Impersonated.sign() returns { keyId: string, signedBlob: string }:

  // Storage expects:
  const signature: string = await auth.sign(blobToSign);

  // Impersonated returns:
  const result = await impersonatedCreds.sign(data);
  // result = { keyId: "...", signedBlob: "..." }

  This causes: SigningError: The first argument must be of type string, Buffer, ArrayBuffer...

A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. **

This is expected google best practices to issue signed url with least privilege, e.g. as per https://docs.cloud.google.com/storage/docs/access-control/best-practices-access-control, https://docs.cloud.google.com/iam/docs/service-account-impersonation

There's somehow related but not identical issues #2427 #2381

An alternative would likely be to use the API, but this is an incomplete solution due to additional roundtrip costs incurred.

Workaround

import { Storage } from "@google-cloud/storage";

import { GoogleAuth, Impersonated } from "google-auth-library";

const SA_EMAIL = "some-sa@my-project.iam.gserviceaccount.com";

const auth = new GoogleAuth({
  scopes: ["https://www.googleapis.com/auth/cloud-platform"],
});

const sourceClient = await auth.getClient();

const impersonatedCreds = new Impersonated({
  sourceClient,
  targetPrincipal: SA_EMAIL,
  delegates: [],
  targetScopes: ["https://www.googleapis.com/auth/devstorage.read_write"],
  lifetime: 3600,
});

// Patch 1: Add missing getCredentials method (Storage SDK requires this)
(impersonatedCreds as any).getCredentials = async () => ({ client_email: SA_EMAIL });

// Patch 2: Wrap sign method to return just signedBlob string (Storage expects string, not object)
const originalSign = impersonatedCreds.sign.bind(impersonatedCreds);
(impersonatedCreds as any).sign = async (data: string) => {
  const result = await originalSign(data);
  return result.signedBlob;
};

const storage = new Storage({
  authClient: impersonatedCreds as any,
  projectId: "my-project",
});

// Patch 3: Force override authClient after construction
storage.authClient = impersonatedCreds as any;

const [signedUrl] = await storage
  .bucket("my-bucket")
  .file("test.mp4")
  .getSignedUrl({
    version: "v4",
    action: "resumable",
    contentType: "text/plain",
    expires: new Date(Date.now() + 60 * 60 * 1000),
  });

console.log(signedUrl);

Metadata

Metadata

Assignees

No one assigned

    Labels

    api: storageIssues related to the googleapis/nodejs-storage API.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions