Skip to content

Auth chain breaks for local dev: hardcoded SA + double-impersonation #3

Description

@robin-morton

Summary

fireway (this fork on the fix-auth-firebase branch — the version AS8 consumes via yarn.lock) hardcodes a service-account name in its impersonation chain and double-impersonates when ADC is already configured for impersonation. The combination makes it work in CI (which uses an OIDC-derived external_account credential) but fail locally (where engineers authenticate via gcloud auth application-default login). We need it to work in both contexts so engineers can run migrations from their laptops.

What the code does today

src/index.js:307-313 (this fork):

const client = await auth.getClient();

targetClient = new Impersonated({
    sourceClient: client,
    targetPrincipal: `main-service-account@${projectId}.iam.gserviceaccount.com`,
    lifetime: 60 * 15,
    delegates: [],
    targetScopes: ['https://www.googleapis.com/auth/cloud-platform'],
});

Two issues here:

  1. Hardcoded SA name. main-service-account@${projectId}.iam.gserviceaccount.com is an Akeneo-AS8 naming convention. Consumers in other projects (or future AS8 envs that don't follow this convention) can't use this fork.
  2. Forced impersonation regardless of ADC. If ADC is already configured to impersonate the same SA (which is what gcloud auth application-default login --impersonate-service-account=… produces locally for engineers in the as8-squad group), the chain becomes "main-SA impersonating main-SA", which the IAM Credentials API rejects with INVALID_ARGUMENT: unable to impersonate: Request contains an invalid argument.

What goes wrong locally — three scenarios we hit

Reproduced while building https://github.com/akeneo/as8/pull/6411 (a local-deploy script):

Local ADC Outcome
Plain user (gcloud auth application-default login) auth.getClient() returns user; fireway tries user → main-SA; user is in as8-squad (which has tokenCreator); should work, but…
Plain user + stale nested @google-cloud/firestore@5.0.2 under node_modules/fireway/node_modules/ Firestore@5.0.2 doesn't fully honour authClient: targetClient — calls go out as user → PERMISSION_DENIED / UNAUTHENTICATED. The fix from a fresh install (8.x) works. But it's a footgun: anyone with stale node_modules from before this fork bumped its firestore dep hits this.
Impersonated (gcloud auth application-default login --impersonate-service-account=main-service-account@...) auth.getClient() returns a client whose tokens are already main-SA tokens. fireway's wrapper then asks IAM to "impersonate main-SA using main-SA token" → INVALID_ARGUMENT: unable to impersonate.

In CI, an external_account credentials file is set up by the OIDC orb. auth.getClient() returns an external_account client whose token-fetch path goes through STS+impersonation transparently, and fireway's wrapper happens to layer cleanly. So CI works, locally we're stuck.

Proposed fixes

A safe, minimal approach:

  1. Make the impersonation target configurable — read FIREWAY_IMPERSONATE_SERVICE_ACCOUNT env var (or --impersonate-service-account= CLI flag), default to no impersonation. Keep the current AS8 default behind that env var if you want zero-config for the AS8 use case.
  2. Skip the explicit Impersonated wrap when not needed. If FIREWAY_IMPERSONATE_SERVICE_ACCOUNT is unset (or matches the SA already in service_account_impersonation_url of ADC), use auth.getClient() directly — let the underlying ADC handle impersonation. This eliminates the double-impersonation case for engineers using gcloud auth application-default login --impersonate-service-account=….
  3. Document the auth contract in the README so consumers know whether to set the env var or use ADC's impersonation flag.

A version that's even smaller: just stop wrapping in Impersonated at all. Let consumers configure ADC how they want. Both CI's external_account creds file and engineers' gcloud auth application-default login --impersonate-service-account=… produce ADC that already mints the right token; fireway doesn't need to add another layer.

Side issue: stale nested deps

Even with the above fixed, this fork's node_modules/fireway/node_modules/@google-cloud/firestore can drift to an older version (we saw 5.0.2) for engineers whose node_modules predates a dep bump. v5 doesn't fully honour authClient, breaking auth in subtle ways. Worth either:

  • Pinning peerDependencies more tightly so yarn doesn't keep nested copies around, or
  • Documenting "if local migrations fail with UNAUTHENTICATED, run rm -rf node_modules/fireway/node_modules && yarn install".

Repro

In a workspace with stale @google-cloud/firestore@5.0.2 under node_modules/fireway/node_modules/:

gcloud auth application-default login   # plain user ADC, no --impersonate flag
fireway --path migrations migrate --projectId=<your-gcp-project>
# → ERROR: 16 UNAUTHENTICATED: Request is missing required authentication credential.

After cleaning the stale nested dep:

rm -rf node_modules/fireway/node_modules/@google-cloud
yarn install --check-files
fireway --path migrations migrate --projectId=<your-gcp-project>
# → succeeds (because root @google-cloud/firestore@8.3.0 honours authClient)

With ADC impersonating the same SA fireway hardcodes:

gcloud auth application-default login --impersonate-service-account=main-service-account@<project>.iam.gserviceaccount.com
fireway --path migrations migrate --projectId=<your-gcp-project>
# → ERROR: INVALID_ARGUMENT: unable to impersonate: Request contains an invalid argument.

Why this matters

The AS8 team is moving sandbox/dev deploys from CircleCI onto engineers' laptops (akeneo/as8#6411 / #6413), partly to stop CI from holding "incomplete" pipelines open behind never-clicked approval gates and partly to recover ~293k CircleCI credits / month wasted on builds that never deploy. Local migrations are part of that flow. While gcp-projects PR akeneo/gcp-projects#2105 grants impersonation to the as8-squad group, fireway's auth chain still doesn't compose cleanly with engineer ADC. Fixing this here unblocks the local path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions