Skip to content

Add signjwt credential mode for auto-managed direct Cloud Run IAP - #5

Merged
knwoop merged 3 commits into
mainfrom
signjwt-mode
Jul 27, 2026
Merged

Add signjwt credential mode for auto-managed direct Cloud Run IAP#5
knwoop merged 3 commits into
mainfrom
signjwt-mode

Conversation

@knwoop

@knwoop knwoop commented Jul 27, 2026

Copy link
Copy Markdown
Owner

What

Local MCP clients can now reach Cloud Run services whose IAP was enabled the modern default way (gcloud beta run services update --iap, auto-managed OAuth client). Live testing showed that setup rejects every Google-issued OIDC ID token the proxy could previously mint — so all existing modes (adc/impersonate/oauth) fail against it with Invalid IAP credentials: Invalid JWT audience, regardless of audience format. The new --credentials=signjwt mode signs a self-signed service-account JWT via the IAM Credentials signJwt API, which such IAP accepts, unblocking the full MCP handshake end to end.

The four OIDC modes are unchanged and still documented for custom-OAuth-client / LB-backed IAP.

Why

The IAP OAuth Admin API was shut down (March 2026), so new IAP deployments are forced onto the auto-managed client. Google's documented programmatic-access path for it is a self-signed SA JWT, not an ID token — a token type the proxy simply couldn't produce, making it unusable against any freshly-created direct Cloud Run IAP. This was found by testing against a real IAP-protected Cloud Run service: same audience, OIDC rejected but a hand-signed SA JWT accepted, so the blocker is the token type, not the audience.

Notes for reviewers

  • signjwt reuses --impersonate-service-account as the target SA (signed-as, via ADC as base identity). Caller needs roles/iam.serviceAccountTokenCreator on the SA; the SA needs roles/iap.httpsResourceAccessor on the IAP resource.
  • Audience defaulting is now mode-aware: signjwt defaults to the upstream origin plus /* (the canonical run.app URL; origin-only and the project-number URL are rejected), other modes keep origin-only. Overridable with --audience.
  • Self-signed JWTs are ordinary 3-part JWTs with an exp claim, so the existing Cached/tokenExpiry refresh path works unchanged (covered by a test).

Verification

  • go test -race ./..., go vet ./..., golangci-lint run — all pass. New unit tests: signjwt claim construction, exp-parses-in-cache, and mode-aware audience defaulting.
  • CLI smoke: signjwt without an SA errors cleanly (exit 2); audience defaulting logs https://svc-xxxx-an.a.run.app/*.
  • Not runnable in this environment: the live end-to-end run against the real IAP-protected Cloud Run service (no GCP ADC / service here). It was verified by hand during the investigation that produced this change — initialize/tools/list/tools/call succeed through IAP with a self-signed SA JWT — and the claim shape and audience default here match exactly what worked.

🤖 Generated with Claude Code

@knwoop knwoop self-assigned this Jul 27, 2026
@knwoop
knwoop marked this pull request as ready for review July 27, 2026 12:57
@knwoop
knwoop merged commit bd22fcb into main Jul 27, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant