@webpieces/gcp-identity 0.4.862 → 0.4.864

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ tests) every call falls back to a deterministic localhost value so no GCP is nee
12
12
  - `GcpOidc.mintIdToken(audience)` — Google-signed OIDC ID token (a `dev-oidc.*` token off-GCP)
13
13
  - `GcpOidc.verifyFromCallers(idToken, callers)` — verify + allow-list the caller SA
14
14
 
15
- Underpins the `@WpAuthOidc` service-to-service credential contract used by
15
+ Underpins the `oidc(...)` service-to-service credential contract used by
16
16
  `@webpieces/http-client-node` (RPC) and `@webpieces/cloudtasks-client`, with supplementary
17
17
  server checks dispatched by `AuthFilter`.
18
18
 
@@ -20,7 +20,7 @@ server checks dispatched by `AuthFilter`.
20
20
 
21
21
  The protected Cloud Run edge verifies the invocation ID token and enforces `roles/run.invoker`
22
22
  before the container receives it. The annotation primarily tells clients to generate the
23
- correct destination-audience token. A bare `@WpAuthOidc()` delegates caller authorization to
23
+ correct destination-audience token. A bare `@WpAuth([oidc()])` delegates caller authorization to
24
24
  that edge; explicit callers add an application allow-list. Real tokens are also checked by the
25
25
  application verifier, without its own audience constraint.
26
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/gcp-identity",
3
- "version": "0.4.862",
3
+ "version": "0.4.864",
4
4
  "description": "GCP runtime identity: project/region metadata, Cloud Run URLs, OIDC mint/verify",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -23,8 +23,8 @@
23
23
  "access": "public"
24
24
  },
25
25
  "dependencies": {
26
- "@webpieces/core-context": "0.4.862",
27
- "@webpieces/core-util": "0.4.862",
26
+ "@webpieces/core-context": "0.4.864",
27
+ "@webpieces/core-util": "0.4.864",
28
28
  "google-auth-library": "9.15.1",
29
29
  "gcp-metadata": "6.1.1",
30
30
  "inversify": "7.10.4",
package/src/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * all read from the metadata server / ADC at runtime with deterministic localhost
6
6
  * fallbacks off-GCP so local dev and tests never touch GCP.
7
7
  *
8
- * Underpins the @WpAuthOidc service-to-service auth mode (enforced by the framework AuthFilter
8
+ * Underpins the oidc(...) service-to-service auth mode (enforced by the framework AuthFilter
9
9
  * via an app-bound AuthConfig) and the RPC + Cloud Tasks clients.
10
10
  *
11
11
  * URL RESOLUTION lives in core-util's browser-safe `ClientRegistry`, NOT here; this package
package/src/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * all read from the metadata server / ADC at runtime with deterministic localhost
7
7
  * fallbacks off-GCP so local dev and tests never touch GCP.
8
8
  *
9
- * Underpins the @WpAuthOidc service-to-service auth mode (enforced by the framework AuthFilter
9
+ * Underpins the oidc(...) service-to-service auth mode (enforced by the framework AuthFilter
10
10
  * via an app-bound AuthConfig) and the RPC + Cloud Tasks clients.
11
11
  *
12
12
  * URL RESOLUTION lives in core-util's browser-safe `ClientRegistry`, NOT here; this package
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/cloud/gcp-identity/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAEH,uCAA4D;AAAnD,mGAAA,OAAO,OAAA;AAAE,iHAAA,qBAAqB,OAAA;AACvC,+BAOgB;AANZ,sGAAA,cAAc,OAAA;AACd,oGAAA,YAAY,OAAA;AACZ,iGAAA,SAAS,OAAA;AACT,qHAAA,6BAA6B,OAAA;AAC7B,0GAAA,kBAAkB,OAAA;AAClB,mHAAA,2BAA2B,OAAA;AAE/B,2DAA6E;AAApE,wHAAA,kBAAkB,OAAA;AAAE,uHAAA,iBAAiB,OAAA;AAC9C,+BAAmD;AAA1C,+FAAA,OAAO,OAAA;AAAE,wGAAA,gBAAgB,OAAA","sourcesContent":["/**\n * @webpieces/gcp-identity\n *\n * GCP runtime identity (Node-only). Metadata, Cloud Run URLs, and OIDC mint/verify,\n * all read from the metadata server / ADC at runtime with deterministic localhost\n * fallbacks off-GCP so local dev and tests never touch GCP.\n *\n * Underpins the @WpAuthOidc service-to-service auth mode (enforced by the framework AuthFilter\n * via an app-bound AuthConfig) and the RPC + Cloud Tasks clients.\n *\n * URL RESOLUTION lives in core-util's browser-safe `ClientRegistry`, NOT here; this package\n * contributes only the GCP half of it — {@link gcpCloudRunDeriver}, which an app installs with\n * `ClientRegistry.setDeriver(...)` at startup. That is the seam that lets a non-GCP deployment use\n * `templateDeriver` (or plain mappings) and never pull gcp-metadata onto the URL path.\n */\n\nexport { isOnGcp, resetMetadataForTests } from './metadata';\nexport {\n getServiceName,\n getProjectId,\n getRegion,\n getRuntimeServiceAccountEmail,\n getSelfCloudRunUrl,\n LOCAL_SERVICE_ACCOUNT_EMAIL,\n} from './urls';\nexport { gcpCloudRunDeriver, GcpCloudRunTarget } from './gcpCloudRunDeriver';\nexport { GcpOidc, OidcVerifyResult } from './oidc';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/cloud/gcp-identity/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAEH,uCAA4D;AAAnD,mGAAA,OAAO,OAAA;AAAE,iHAAA,qBAAqB,OAAA;AACvC,+BAOgB;AANZ,sGAAA,cAAc,OAAA;AACd,oGAAA,YAAY,OAAA;AACZ,iGAAA,SAAS,OAAA;AACT,qHAAA,6BAA6B,OAAA;AAC7B,0GAAA,kBAAkB,OAAA;AAClB,mHAAA,2BAA2B,OAAA;AAE/B,2DAA6E;AAApE,wHAAA,kBAAkB,OAAA;AAAE,uHAAA,iBAAiB,OAAA;AAC9C,+BAAmD;AAA1C,+FAAA,OAAO,OAAA;AAAE,wGAAA,gBAAgB,OAAA","sourcesContent":["/**\n * @webpieces/gcp-identity\n *\n * GCP runtime identity (Node-only). Metadata, Cloud Run URLs, and OIDC mint/verify,\n * all read from the metadata server / ADC at runtime with deterministic localhost\n * fallbacks off-GCP so local dev and tests never touch GCP.\n *\n * Underpins the oidc(...) service-to-service auth mode (enforced by the framework AuthFilter\n * via an app-bound AuthConfig) and the RPC + Cloud Tasks clients.\n *\n * URL RESOLUTION lives in core-util's browser-safe `ClientRegistry`, NOT here; this package\n * contributes only the GCP half of it — {@link gcpCloudRunDeriver}, which an app installs with\n * `ClientRegistry.setDeriver(...)` at startup. That is the seam that lets a non-GCP deployment use\n * `templateDeriver` (or plain mappings) and never pull gcp-metadata onto the URL path.\n */\n\nexport { isOnGcp, resetMetadataForTests } from './metadata';\nexport {\n getServiceName,\n getProjectId,\n getRegion,\n getRuntimeServiceAccountEmail,\n getSelfCloudRunUrl,\n LOCAL_SERVICE_ACCOUNT_EMAIL,\n} from './urls';\nexport { gcpCloudRunDeriver, GcpCloudRunTarget } from './gcpCloudRunDeriver';\nexport { GcpOidc, OidcVerifyResult } from './oidc';\n"]}
package/src/oidc.d.ts CHANGED
@@ -38,7 +38,7 @@ export declare class GcpOidc {
38
38
  */
39
39
  verifyFromCallers(idToken: string, callers: string[]): Promise<OidcVerifyResult>;
40
40
  /**
41
- * @WpAuthOidc() (trust-the-edge) is only secure when the Cloud Run service is PRIVATE — the edge
41
+ * @WpAuth([oidc()]) (trust-the-edge) is only secure when the Cloud Run service is PRIVATE — the edge
42
42
  * enforces run.invoker. If it is actually PUBLIC, warn LOUDLY (once): we still admit only
43
43
  * Google-signed callers, but the edge is not filtering WHO. Never fails — this is advisory.
44
44
  */
package/src/oidc.js CHANGED
@@ -12,7 +12,7 @@ const log = core_util_1.LogManager.getLogger('gcp-identity-oidc');
12
12
  /**
13
13
  * Prefix marking a deterministic local (off-GCP) OIDC token. Real Google-signed
14
14
  * tokens never start with this, so verify can tell them apart. This is what keeps
15
- * the @WpAuthOidc code path fully exercised in tests without any GCP round-trip.
15
+ * the oidc(...) code path fully exercised in tests without any GCP round-trip.
16
16
  */
17
17
  const DEV_TOKEN_PREFIX = 'dev-oidc.';
18
18
  /** Outcome of verifying an inbound OIDC token against an allow-list of callers. */
@@ -85,7 +85,7 @@ let GcpOidc = class GcpOidc {
85
85
  return new OidcVerifyResult(true, email);
86
86
  }
87
87
  /**
88
- * @WpAuthOidc() (trust-the-edge) is only secure when the Cloud Run service is PRIVATE — the edge
88
+ * @WpAuth([oidc()]) (trust-the-edge) is only secure when the Cloud Run service is PRIVATE — the edge
89
89
  * enforces run.invoker. If it is actually PUBLIC, warn LOUDLY (once): we still admit only
90
90
  * Google-signed callers, but the edge is not filtering WHO. Never fails — this is advisory.
91
91
  */
@@ -98,7 +98,7 @@ let GcpOidc = class GcpOidc {
98
98
  return; // off-GCP: there is no Cloud Run edge; public/private does not apply.
99
99
  }
100
100
  if (await this.isServicePublic()) {
101
- log.error('This endpoint is currently public but marked @WpAuthOidc which requires the service to be ' +
101
+ log.error('This endpoint is currently public but marked oidc(...) which requires the service to be ' +
102
102
  'private. You are currently running in a very insecure mode; however, we are only allowing ' +
103
103
  'google-signed callers in. The edge will validate callers are allowed in (make the Cloud Run ' +
104
104
  'service private — remove the allUsers run.invoker binding) to reduce your attack surface.');
package/src/oidc.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"oidc.js","sourceRoot":"","sources":["../../../../../packages/cloud/gcp-identity/src/oidc.ts"],"names":[],"mappings":";;;;AAAA,6DAA+D;AAC/D,0DAAoE;AACpE,oDAAkD;AAClD,oDAA+C;AAC/C,yCAAqC;AACrC,iCAAgF;AAEhF,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;AAEtD;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAErC,mFAAmF;AACnF,MAAa,gBAAgB;IACzB,iEAAiE;IACjE,EAAE,CAAU;IACZ,mDAAmD;IACnD,KAAK,CAAU;IACf,kEAAkE;IAClE,MAAM,CAAU;IAEhB,YAAY,EAAW,EAAE,KAAc,EAAE,MAAe;QACpD,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;CACJ;AAbD,4CAaC;AAED;;;;;;;;GAQG;AAEI,IAAM,OAAO,GAAb,MAAM,OAAO;IACC,IAAI,GAAG,IAAI,gCAAU,EAAE,CAAC;IACxB,QAAQ,GAAG,IAAI,kCAAY,EAAE,CAAC;IACvC,oBAAoB,GAAG,KAAK,CAAC;IAErC;;;;OAIG;IACH,KAAK,CAAC,WAAW,CAAC,QAAgB;QAC9B,IAAI,CAAC,CAAC,MAAM,IAAA,kBAAO,GAAE,CAAC,EAAE,CAAC;YACrB,MAAM,KAAK,GAAG,MAAM,IAAA,oCAA6B,GAAE,CAAC;YACpD,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAC9C,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAC1D,OAAO,MAAM,CAAC,eAAe,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IACzD,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,iBAAiB,CAAC,OAAe,EAAE,OAAiB;QACtD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;QACvD,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,OAAO,IAAI,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,sCAAsC,CAAC,CAAC;QAC1F,CAAC;QACD,6FAA6F;QAC7F,gGAAgG;QAChG,gGAAgG;QAChG,8FAA8F;QAC9F,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC9B,OAAO,IAAI,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC7C,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QACnD,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,IAAI,gBAAgB,CACvB,KAAK,EACL,KAAK,EACL,WAAW,KAAK,+BAA+B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACvE,CAAC;QACN,CAAC;QACD,OAAO,IAAI,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC7C,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,gBAAgB;QAC1B,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC5B,OAAO;QACX,CAAC;QACD,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC,wDAAwD;QAC1F,IAAI,CAAC,CAAC,MAAM,IAAA,kBAAO,GAAE,CAAC,EAAE,CAAC;YACrB,OAAO,CAAC,sEAAsE;QAClF,CAAC;QACD,IAAI,MAAM,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;YAC/B,GAAG,CAAC,KAAK,CACL,4FAA4F;gBACxF,4FAA4F;gBAC5F,8FAA8F;gBAC9F,2FAA2F,CAClG,CAAC;QACN,CAAC;IACL,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,eAAe;QACzB,kIAAkI;QAClI,IAAI,CAAC;YACD,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAA,mBAAY,GAAE,EAAE,IAAA,gBAAS,GAAE,CAAC,CAAC,CAAC;YAC3E,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;YAC/C,MAAM,GAAG,GAAG,0CAA0C,OAAO,cAAc,MAAM,aAAa,OAAO,eAAe,CAAC;YACrH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YAC3C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,OAAO,CAAY,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;YAC1D,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,CACjC,CAAC,OAAmB,EAAE,EAAE,CACpB,OAAO,CAAC,IAAI,KAAK,mBAAmB;gBACpC,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CACxB,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,KAAK,UAAU,IAAI,CAAC,KAAK,uBAAuB,CACnE,CACR,CAAC;QACN,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,GAAG,CAAC,KAAK,CACL,6EAA6E,KAAK,CAAC,OAAO,EAAE,CAC/F,CAAC;YACF,OAAO,KAAK,CAAC;QACjB,CAAC;IACL,CAAC;IAEO,YAAY,CAAC,KAAa,EAAE,QAAgB;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QAChE,OAAO,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACjF,CAAC;IAEO,KAAK,CAAC,oBAAoB,CAAC,OAAe;QAC9C,IAAI,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACvC,OAAO,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAC7C,CAAC;QACD,2KAA2K;QAC3K,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;YACvE,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;YACpC,OAAO,OAAO,EAAE,KAAK,IAAI,SAAS,CAAC;QACvC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,EAAE,CAAC;gBACpC,sFAAsF;gBACtF,qFAAqF;gBACrF,qDAAqD;gBACrD,MAAM,KAAK,CAAC;YAChB,CAAC;YACD,wFAAwF;YACxF,GAAG,CAAC,KAAK,CAAC,0CAA0C,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YACrE,OAAO,SAAS,CAAC;QACrB,CAAC;IACL,CAAC;IAED;;;;OAIG;IACK,qBAAqB,CAAC,KAAY;QACtC,MAAM,IAAI,GAAI,KAAuB,CAAC,IAAI,CAAC;QAC3C,MAAM,YAAY,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;QAC3F,OAAO,CACH,CAAC,IAAI,KAAK,SAAS,IAAI,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YACnD,KAAK,CAAC,IAAI,KAAK,aAAa;YAC5B,KAAK,CAAC,IAAI,KAAK,YAAY,CAC9B,CAAC;IACN,CAAC;IAEO,mBAAmB,CAAC,OAAe;QACvC,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAChE,2HAA2H;QAC3H,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAoB,CAAC;YACnD,OAAO,MAAM,CAAC,KAAK,IAAI,SAAS,CAAC;QACrC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,GAAG,CAAC,KAAK,CAAC,iCAAiC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YAC5D,OAAO,SAAS,CAAC;QACrB,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,cAAc,CAAC,OAAiB;QAC1C,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAc,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACpF,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,MAAc;QACtC,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACpB,OAAO,IAAA,oCAA6B,GAAE,CAAC;QAC3C,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,MAAM,CAAC;QAClB,CAAC;QACD,MAAM,SAAS,GAAG,MAAM,IAAA,mBAAY,GAAE,CAAC;QACvC,OAAO,GAAG,MAAM,IAAI,SAAS,0BAA0B,CAAC;IAC5D,CAAC;CACJ,CAAA;AA/KY,0BAAO;kBAAP,OAAO;IADnB,IAAA,wCAAyB,GAAE;GACf,OAAO,CA+KnB;AAWD,mDAAmD;AACnD,MAAM,eAAe;IACjB,KAAK,CAAU;IACf,GAAG,CAAU;CAChB","sourcesContent":["import { GoogleAuth, OAuth2Client } from 'google-auth-library';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\nimport { toError } from '@webpieces/core-util';\nimport { isOnGcp } from './metadata';\nimport { getProjectId, getRegion, getRuntimeServiceAccountEmail } from './urls';\n\nconst log = LogManager.getLogger('gcp-identity-oidc');\n\n/**\n * Prefix marking a deterministic local (off-GCP) OIDC token. Real Google-signed\n * tokens never start with this, so verify can tell them apart. This is what keeps\n * the @WpAuthOidc code path fully exercised in tests without any GCP round-trip.\n */\nconst DEV_TOKEN_PREFIX = 'dev-oidc.';\n\n/** Outcome of verifying an inbound OIDC token against an allow-list of callers. */\nexport class OidcVerifyResult {\n /** True when the token is valid AND its caller SA is allowed. */\n ok: boolean;\n /** The verified caller email (present when ok). */\n email?: string;\n /** Human-readable reason when not ok (for logs / 401 message). */\n reason?: string;\n\n constructor(ok: boolean, email?: string, reason?: string) {\n this.ok = ok;\n this.email = email;\n this.reason = reason;\n }\n}\n\n/**\n * GcpOidc - Google OIDC service-to-service tokens: MINT an outbound token as this service's runtime\n * SA, and VERIFY an inbound token against a caller allow-list. Injected by type (@provideSingleton /\n * inject-by-type) into the RPC + Cloud Tasks clients (mint) and the framework auth verifier (verify),\n * so no DI-managed code calls gcp-identity as a free function.\n *\n * Holds the reusable google-auth-library clients as instance state (one per process, since this is a\n * framework singleton) plus the \"warned about public posture once\" flag.\n */\n@provideFrameworkSingleton()\nexport class GcpOidc {\n private readonly auth = new GoogleAuth();\n private readonly verifier = new OAuth2Client();\n private publicPostureChecked = false;\n\n /**\n * Mint a Google-signed OIDC ID token for `audience` (the callee's base URL), as this service's\n * runtime SA. Off-GCP returns a self-describing `dev-oidc.*` token that {@link verifyFromCallers}\n * accepts locally.\n */\n async mintIdToken(audience: string): Promise<string> {\n if (!(await isOnGcp())) {\n const email = await getRuntimeServiceAccountEmail();\n return this.makeDevToken(email, audience);\n }\n const client = await this.auth.getIdTokenClient(audience);\n return client.idTokenProvider.fetchIdToken(audience);\n }\n\n /**\n * Verify an inbound OIDC token and require its caller SA to be in the allow-list. Audience is\n * deliberately NOT gated (mirrors the platform's cross-service model — the callee's own `aud`\n * passes). Never throws — an invalid token or a disallowed caller comes back as `ok:false` so\n * callers map it to a 401 without a try/catch.\n *\n * `callers` entries resolve as: 'self' → this service's runtime SA; a bare id →\n * `<id>@<project>.iam.gserviceaccount.com`; anything containing '@' → verbatim.\n */\n async verifyFromCallers(idToken: string, callers: string[]): Promise<OidcVerifyResult> {\n const email = await this.extractVerifiedEmail(idToken);\n if (!email) {\n return new OidcVerifyResult(false, undefined, 'token invalid or missing email claim');\n }\n // EMPTY allow-list = TRUST THE EDGE: accept any genuine Google-signed OIDC caller, because a\n // PRIVATE Cloud Run service's edge already gated WHO via run.invoker IAM (managed in terraform,\n // one source of truth). Warn loudly (once) if the service is actually PUBLIC — then the edge is\n // NOT filtering callers and this is insecure. Non-empty list = explicit app-level allow-list.\n if (callers.length === 0) {\n await this.warnIfPublicOnce();\n return new OidcVerifyResult(true, email);\n }\n const allowed = await this.resolveCallers(callers);\n if (!allowed.includes(email)) {\n return new OidcVerifyResult(\n false,\n email,\n `caller '${email}' is not in the allow-list [${allowed.join(', ')}]`,\n );\n }\n return new OidcVerifyResult(true, email);\n }\n\n /**\n * @WpAuthOidc() (trust-the-edge) is only secure when the Cloud Run service is PRIVATE — the edge\n * enforces run.invoker. If it is actually PUBLIC, warn LOUDLY (once): we still admit only\n * Google-signed callers, but the edge is not filtering WHO. Never fails — this is advisory.\n */\n private async warnIfPublicOnce(): Promise<void> {\n if (this.publicPostureChecked) {\n return;\n }\n this.publicPostureChecked = true; // attempt at most once, even if the check itself errors\n if (!(await isOnGcp())) {\n return; // off-GCP: there is no Cloud Run edge; public/private does not apply.\n }\n if (await this.isServicePublic()) {\n log.error(\n 'This endpoint is currently public but marked @WpAuthOidc which requires the service to be ' +\n 'private. You are currently running in a very insecure mode; however, we are only allowing ' +\n 'google-signed callers in. The edge will validate callers are allowed in (make the Cloud Run ' +\n 'service private — remove the allUsers run.invoker binding) to reduce your attack surface.',\n );\n }\n }\n\n /**\n * True when THIS Cloud Run service grants run.invoker to allUsers/allAuthenticatedUsers (public).\n * Reads the service's OWN IAM policy via the Run Admin API (needs run.services.getIamPolicy on the\n * runtime SA). On any failure we cannot tell → false (no false alarm).\n */\n private async isServicePublic(): Promise<boolean> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a failed self-IAM read just means \"posture unknown\" → no warning\n try {\n const [project, region] = await Promise.all([getProjectId(), getRegion()]);\n const service = process.env['K_SERVICE'] ?? '';\n const url = `https://run.googleapis.com/v2/projects/${project}/locations/${region}/services/${service}:getIamPolicy`;\n const client = await this.auth.getClient();\n const res = await client.request<IamPolicy>({ url: url });\n return (res.data.bindings ?? []).some(\n (binding: IamBinding) =>\n binding.role === 'roles/run.invoker' &&\n (binding.members ?? []).some(\n (m: string) => m === 'allUsers' || m === 'allAuthenticatedUsers',\n ),\n );\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(\n `Could not read own Cloud Run IAM policy (need run.services.getIamPolicy): ${error.message}`,\n );\n return false;\n }\n }\n\n private makeDevToken(email: string, audience: string): string {\n const payload = JSON.stringify({ email: email, aud: audience });\n return DEV_TOKEN_PREFIX + Buffer.from(payload, 'utf8').toString('base64url');\n }\n\n private async extractVerifiedEmail(idToken: string): Promise<string | undefined> {\n if (idToken.startsWith(DEV_TOKEN_PREFIX)) {\n return this.decodeDevTokenEmail(idToken);\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a genuinely unverifiable token is unauthenticated (→ undefined → 401); infra failures are re-thrown below\n try {\n const ticket = await this.verifier.verifyIdToken({ idToken: idToken });\n const payload = ticket.getPayload();\n return payload?.email ?? undefined;\n } catch (err: unknown) {\n const error = toError(err);\n if (this.isInfrastructureError(error)) {\n // A network/system failure fetching Google's certs (or a bug) is NOT an auth failure.\n // Do NOT mask it as a 401 — re-throw so it surfaces as a 5xx (alertable / retryable)\n // instead of looking like a caller sent a bad token.\n throw error;\n }\n // Bad signature / expired / malformed token → genuinely unauthenticated → 401 upstream.\n log.debug(`OIDC token rejected (unauthenticated): ${error.message}`);\n return undefined;\n }\n }\n\n /**\n * True when an error thrown while verifying a token is INFRASTRUCTURE (network/system), not a bad\n * token — e.g. the metadata/cert fetch failed. These must NOT be swallowed as a 401 (that would\n * both mislead the caller and hide real outages/bugs behind a client-error status).\n */\n private isInfrastructureError(error: Error): boolean {\n const code = (error as ErrorWithCode).code;\n const networkCodes = ['ENOTFOUND', 'ETIMEDOUT', 'ECONNREFUSED', 'ECONNRESET', 'EAI_AGAIN'];\n return (\n (code !== undefined && networkCodes.includes(code)) ||\n error.name === 'GaxiosError' ||\n error.name === 'FetchError'\n );\n }\n\n private decodeDevTokenEmail(idToken: string): string | undefined {\n const encoded = idToken.substring(DEV_TOKEN_PREFIX.length);\n const json = Buffer.from(encoded, 'base64url').toString('utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a malformed dev token is just unauthenticated → undefined\n try {\n const parsed = JSON.parse(json) as DevTokenPayload;\n return parsed.email ?? undefined;\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(`dev-oidc token decode failed: ${error.message}`);\n return undefined;\n }\n }\n\n private async resolveCallers(callers: string[]): Promise<string[]> {\n return Promise.all(callers.map((caller: string) => this.resolveCaller(caller)));\n }\n\n private async resolveCaller(caller: string): Promise<string> {\n if (caller === 'self') {\n return getRuntimeServiceAccountEmail();\n }\n if (caller.includes('@')) {\n return caller;\n }\n const projectId = await getProjectId();\n return `${caller}@${projectId}.iam.gserviceaccount.com`;\n }\n}\n\n/** Minimal shape of a Cloud Run getIamPolicy response. */\ninterface IamBinding {\n role: string;\n members?: string[];\n}\ninterface IamPolicy {\n bindings?: IamBinding[];\n}\n\n/** Shape of the decoded dev-oidc token payload. */\nclass DevTokenPayload {\n email!: string;\n aud!: string;\n}\n\n/** A thrown error carrying an optional Node/system error code (e.g. 'ETIMEDOUT'). */\ninterface ErrorWithCode {\n code?: string;\n}\n"]}
1
+ {"version":3,"file":"oidc.js","sourceRoot":"","sources":["../../../../../packages/cloud/gcp-identity/src/oidc.ts"],"names":[],"mappings":"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","sourcesContent":["import { GoogleAuth, OAuth2Client } from 'google-auth-library';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\nimport { toError } from '@webpieces/core-util';\nimport { isOnGcp } from './metadata';\nimport { getProjectId, getRegion, getRuntimeServiceAccountEmail } from './urls';\n\nconst log = LogManager.getLogger('gcp-identity-oidc');\n\n/**\n * Prefix marking a deterministic local (off-GCP) OIDC token. Real Google-signed\n * tokens never start with this, so verify can tell them apart. This is what keeps\n * the oidc(...) code path fully exercised in tests without any GCP round-trip.\n */\nconst DEV_TOKEN_PREFIX = 'dev-oidc.';\n\n/** Outcome of verifying an inbound OIDC token against an allow-list of callers. */\nexport class OidcVerifyResult {\n /** True when the token is valid AND its caller SA is allowed. */\n ok: boolean;\n /** The verified caller email (present when ok). */\n email?: string;\n /** Human-readable reason when not ok (for logs / 401 message). */\n reason?: string;\n\n constructor(ok: boolean, email?: string, reason?: string) {\n this.ok = ok;\n this.email = email;\n this.reason = reason;\n }\n}\n\n/**\n * GcpOidc - Google OIDC service-to-service tokens: MINT an outbound token as this service's runtime\n * SA, and VERIFY an inbound token against a caller allow-list. Injected by type (@provideSingleton /\n * inject-by-type) into the RPC + Cloud Tasks clients (mint) and the framework auth verifier (verify),\n * so no DI-managed code calls gcp-identity as a free function.\n *\n * Holds the reusable google-auth-library clients as instance state (one per process, since this is a\n * framework singleton) plus the \"warned about public posture once\" flag.\n */\n@provideFrameworkSingleton()\nexport class GcpOidc {\n private readonly auth = new GoogleAuth();\n private readonly verifier = new OAuth2Client();\n private publicPostureChecked = false;\n\n /**\n * Mint a Google-signed OIDC ID token for `audience` (the callee's base URL), as this service's\n * runtime SA. Off-GCP returns a self-describing `dev-oidc.*` token that {@link verifyFromCallers}\n * accepts locally.\n */\n async mintIdToken(audience: string): Promise<string> {\n if (!(await isOnGcp())) {\n const email = await getRuntimeServiceAccountEmail();\n return this.makeDevToken(email, audience);\n }\n const client = await this.auth.getIdTokenClient(audience);\n return client.idTokenProvider.fetchIdToken(audience);\n }\n\n /**\n * Verify an inbound OIDC token and require its caller SA to be in the allow-list. Audience is\n * deliberately NOT gated (mirrors the platform's cross-service model — the callee's own `aud`\n * passes). Never throws — an invalid token or a disallowed caller comes back as `ok:false` so\n * callers map it to a 401 without a try/catch.\n *\n * `callers` entries resolve as: 'self' → this service's runtime SA; a bare id →\n * `<id>@<project>.iam.gserviceaccount.com`; anything containing '@' → verbatim.\n */\n async verifyFromCallers(idToken: string, callers: string[]): Promise<OidcVerifyResult> {\n const email = await this.extractVerifiedEmail(idToken);\n if (!email) {\n return new OidcVerifyResult(false, undefined, 'token invalid or missing email claim');\n }\n // EMPTY allow-list = TRUST THE EDGE: accept any genuine Google-signed OIDC caller, because a\n // PRIVATE Cloud Run service's edge already gated WHO via run.invoker IAM (managed in terraform,\n // one source of truth). Warn loudly (once) if the service is actually PUBLIC — then the edge is\n // NOT filtering callers and this is insecure. Non-empty list = explicit app-level allow-list.\n if (callers.length === 0) {\n await this.warnIfPublicOnce();\n return new OidcVerifyResult(true, email);\n }\n const allowed = await this.resolveCallers(callers);\n if (!allowed.includes(email)) {\n return new OidcVerifyResult(\n false,\n email,\n `caller '${email}' is not in the allow-list [${allowed.join(', ')}]`,\n );\n }\n return new OidcVerifyResult(true, email);\n }\n\n /**\n * @WpAuth([oidc()]) (trust-the-edge) is only secure when the Cloud Run service is PRIVATE — the edge\n * enforces run.invoker. If it is actually PUBLIC, warn LOUDLY (once): we still admit only\n * Google-signed callers, but the edge is not filtering WHO. Never fails — this is advisory.\n */\n private async warnIfPublicOnce(): Promise<void> {\n if (this.publicPostureChecked) {\n return;\n }\n this.publicPostureChecked = true; // attempt at most once, even if the check itself errors\n if (!(await isOnGcp())) {\n return; // off-GCP: there is no Cloud Run edge; public/private does not apply.\n }\n if (await this.isServicePublic()) {\n log.error(\n 'This endpoint is currently public but marked oidc(...) which requires the service to be ' +\n 'private. You are currently running in a very insecure mode; however, we are only allowing ' +\n 'google-signed callers in. The edge will validate callers are allowed in (make the Cloud Run ' +\n 'service private — remove the allUsers run.invoker binding) to reduce your attack surface.',\n );\n }\n }\n\n /**\n * True when THIS Cloud Run service grants run.invoker to allUsers/allAuthenticatedUsers (public).\n * Reads the service's OWN IAM policy via the Run Admin API (needs run.services.getIamPolicy on the\n * runtime SA). On any failure we cannot tell → false (no false alarm).\n */\n private async isServicePublic(): Promise<boolean> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a failed self-IAM read just means \"posture unknown\" → no warning\n try {\n const [project, region] = await Promise.all([getProjectId(), getRegion()]);\n const service = process.env['K_SERVICE'] ?? '';\n const url = `https://run.googleapis.com/v2/projects/${project}/locations/${region}/services/${service}:getIamPolicy`;\n const client = await this.auth.getClient();\n const res = await client.request<IamPolicy>({ url: url });\n return (res.data.bindings ?? []).some(\n (binding: IamBinding) =>\n binding.role === 'roles/run.invoker' &&\n (binding.members ?? []).some(\n (m: string) => m === 'allUsers' || m === 'allAuthenticatedUsers',\n ),\n );\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(\n `Could not read own Cloud Run IAM policy (need run.services.getIamPolicy): ${error.message}`,\n );\n return false;\n }\n }\n\n private makeDevToken(email: string, audience: string): string {\n const payload = JSON.stringify({ email: email, aud: audience });\n return DEV_TOKEN_PREFIX + Buffer.from(payload, 'utf8').toString('base64url');\n }\n\n private async extractVerifiedEmail(idToken: string): Promise<string | undefined> {\n if (idToken.startsWith(DEV_TOKEN_PREFIX)) {\n return this.decodeDevTokenEmail(idToken);\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a genuinely unverifiable token is unauthenticated (→ undefined → 401); infra failures are re-thrown below\n try {\n const ticket = await this.verifier.verifyIdToken({ idToken: idToken });\n const payload = ticket.getPayload();\n return payload?.email ?? undefined;\n } catch (err: unknown) {\n const error = toError(err);\n if (this.isInfrastructureError(error)) {\n // A network/system failure fetching Google's certs (or a bug) is NOT an auth failure.\n // Do NOT mask it as a 401 — re-throw so it surfaces as a 5xx (alertable / retryable)\n // instead of looking like a caller sent a bad token.\n throw error;\n }\n // Bad signature / expired / malformed token → genuinely unauthenticated → 401 upstream.\n log.debug(`OIDC token rejected (unauthenticated): ${error.message}`);\n return undefined;\n }\n }\n\n /**\n * True when an error thrown while verifying a token is INFRASTRUCTURE (network/system), not a bad\n * token — e.g. the metadata/cert fetch failed. These must NOT be swallowed as a 401 (that would\n * both mislead the caller and hide real outages/bugs behind a client-error status).\n */\n private isInfrastructureError(error: Error): boolean {\n const code = (error as ErrorWithCode).code;\n const networkCodes = ['ENOTFOUND', 'ETIMEDOUT', 'ECONNREFUSED', 'ECONNRESET', 'EAI_AGAIN'];\n return (\n (code !== undefined && networkCodes.includes(code)) ||\n error.name === 'GaxiosError' ||\n error.name === 'FetchError'\n );\n }\n\n private decodeDevTokenEmail(idToken: string): string | undefined {\n const encoded = idToken.substring(DEV_TOKEN_PREFIX.length);\n const json = Buffer.from(encoded, 'base64url').toString('utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a malformed dev token is just unauthenticated → undefined\n try {\n const parsed = JSON.parse(json) as DevTokenPayload;\n return parsed.email ?? undefined;\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(`dev-oidc token decode failed: ${error.message}`);\n return undefined;\n }\n }\n\n private async resolveCallers(callers: string[]): Promise<string[]> {\n return Promise.all(callers.map((caller: string) => this.resolveCaller(caller)));\n }\n\n private async resolveCaller(caller: string): Promise<string> {\n if (caller === 'self') {\n return getRuntimeServiceAccountEmail();\n }\n if (caller.includes('@')) {\n return caller;\n }\n const projectId = await getProjectId();\n return `${caller}@${projectId}.iam.gserviceaccount.com`;\n }\n}\n\n/** Minimal shape of a Cloud Run getIamPolicy response. */\ninterface IamBinding {\n role: string;\n members?: string[];\n}\ninterface IamPolicy {\n bindings?: IamBinding[];\n}\n\n/** Shape of the decoded dev-oidc token payload. */\nclass DevTokenPayload {\n email!: string;\n aud!: string;\n}\n\n/** A thrown error carrying an optional Node/system error code (e.g. 'ETIMEDOUT'). */\ninterface ErrorWithCode {\n code?: string;\n}\n"]}