@webpieces/gcp-identity 0.4.390 → 0.4.392
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/package.json +3 -3
- package/src/oidc.js +1 -3
- package/src/oidc.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/gcp-identity",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.392",
|
|
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.
|
|
27
|
-
"@webpieces/core-util": "0.4.
|
|
26
|
+
"@webpieces/core-context": "0.4.392",
|
|
27
|
+
"@webpieces/core-util": "0.4.392",
|
|
28
28
|
"google-auth-library": "9.15.1",
|
|
29
29
|
"gcp-metadata": "6.1.1",
|
|
30
30
|
"inversify": "7.10.4",
|
package/src/oidc.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GcpOidc = exports.OidcVerifyResult = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
-
const inversify_1 = require("inversify");
|
|
6
5
|
const google_auth_library_1 = require("google-auth-library");
|
|
7
6
|
const core_context_1 = require("@webpieces/core-context");
|
|
8
7
|
const core_util_1 = require("@webpieces/core-util");
|
|
@@ -196,8 +195,7 @@ let GcpOidc = class GcpOidc {
|
|
|
196
195
|
};
|
|
197
196
|
exports.GcpOidc = GcpOidc;
|
|
198
197
|
exports.GcpOidc = GcpOidc = tslib_1.__decorate([
|
|
199
|
-
(0, core_context_1.provideFrameworkSingleton)()
|
|
200
|
-
(0, inversify_1.injectable)()
|
|
198
|
+
(0, core_context_1.provideFrameworkSingleton)()
|
|
201
199
|
], GcpOidc);
|
|
202
200
|
/** Shape of the decoded dev-oidc token payload. */
|
|
203
201
|
class DevTokenPayload {
|
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":"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","sourcesContent":["import { injectable } from 'inversify';\nimport { 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 @AuthOidc 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()\n@injectable()\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 * @AuthOidc() (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 @AuthOidc 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((m: string) => m === 'allUsers' || m === 'allAuthenticatedUsers'),\n );\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(`Could not read own Cloud Run IAM policy (need run.services.getIamPolicy): ${error.message}`);\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 @AuthOidc 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 * @AuthOidc() (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 @AuthOidc 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((m: string) => m === 'allUsers' || m === 'allAuthenticatedUsers'),\n );\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(`Could not read own Cloud Run IAM policy (need run.services.getIamPolicy): ${error.message}`);\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"]}
|