@my-f-startup/google-internal-auth 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 My F* Startup
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,105 @@
1
- # Temporary Holding Version
1
+ # @my-f-startup/google-internal-auth
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Authentication of internal calls between Google Cloud services.
4
+
5
+ A Cloud Run service often has routes that only another Google service may call: a job triggered by Cloud Scheduler, a Pub/Sub push subscription, another service. Those callers send an ID token issued by Google for the service account they run as, with the URL of the target service as audience. This package accepts a request only when that token is valid for the audience of the service and its verified email is in the list of allowed callers.
6
+
7
+ ---
8
+
9
+ ## Why this exists
10
+
11
+ Cloud Run services that sit behind a gateway such as Firebase Hosting must accept requests at the platform level, so the platform does not check who calls an internal route. Each service checks it itself. The Google auth library verifies the signature and the audience of the token; the rest is the same in every service: read the bearer token, require the verified email of an allowed caller, reject everything when nothing is configured, read the configuration from the environment, and skip the check against the emulators, where no Google token exists. This package holds that once.
12
+
13
+ ---
14
+
15
+ ## Features
16
+
17
+ - ✅ **Signature and audience** – verified by the Google auth library
18
+ - ✅ **Allowed callers** – the verified email of the token must be in the list
19
+ - ✅ **Closed by default** – without an audience or allowed callers, every request is rejected
20
+ - ✅ **Environment configuration** – `INTERNAL_ALLOWED_CALLERS` and `INTERNAL_AUDIENCE`
21
+ - ✅ **Local development** – the check is skipped when an emulator host is configured
22
+ - ✅ **Two shapes** – a function that answers `true` or `false`, usable as an OpenAPI security handler, and an Express style middleware
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install @my-f-startup/google-internal-auth
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Quick Start
35
+
36
+ As the security handler of an OpenAPI validator (for example `express-openapi-validator`):
37
+
38
+ ```ts
39
+ import {
40
+ createInternalAuthHandler,
41
+ internalAuthOptionsFromEnvironment,
42
+ } from "@my-f-startup/google-internal-auth";
43
+
44
+ const validateSecurity = {
45
+ handlers: {
46
+ internalAuth: createInternalAuthHandler(internalAuthOptionsFromEnvironment()),
47
+ },
48
+ };
49
+ ```
50
+
51
+ As an Express middleware:
52
+
53
+ ```ts
54
+ import { internalAuthMiddleware, internalAuthOptionsFromEnvironment } from "@my-f-startup/google-internal-auth";
55
+
56
+ app.post("/internal/jobs/publish-outbox", internalAuthMiddleware(internalAuthOptionsFromEnvironment()), handler);
57
+ ```
58
+
59
+ ---
60
+
61
+ ## API
62
+
63
+ ### `createInternalAuthHandler(options)`
64
+
65
+ Returns `(req) => Promise<boolean>`. A request is accepted when it has `Authorization: Bearer <token>`, the token is valid for `audience`, and its email is verified and listed in `allowedCallers`.
66
+
67
+ | Option | Type | Description |
68
+ | --- | --- | --- |
69
+ | `allowedCallers` | `string[]` | Emails of the service accounts allowed to call. Empty rejects every request |
70
+ | `audience` | `string` | Audience the token must be issued for: the URL of the service. Missing rejects every request |
71
+ | `skip` | `boolean` | Accept every request, for local development |
72
+ | `verifier` | `IdTokenVerifier` | Verifies the token. Defaults to the `OAuth2Client` of the Google auth library; tests pass a fake |
73
+
74
+ ### `internalAuthOptionsFromEnvironment(environment?)`
75
+
76
+ Reads `INTERNAL_ALLOWED_CALLERS` (comma separated emails) and `INTERNAL_AUDIENCE`. `skip` is `true` when `FIRESTORE_EMULATOR_HOST`, `FIREBASE_DATABASE_EMULATOR_HOST`, `FIREBASE_AUTH_EMULATOR_HOST` or `PUBSUB_EMULATOR_HOST` is set.
77
+
78
+ ### `internalAuthMiddleware(options)`
79
+
80
+ Calls `next()` for an accepted request and answers 401 otherwise.
81
+
82
+ ---
83
+
84
+ ## Tests
85
+
86
+ - Unit: `npm run test:unit`
87
+ - All tests: `npm test`
88
+
89
+ The check against a real token issued by Google runs only in a deployed environment.
90
+
91
+ ---
92
+
93
+ ## Compatibility
94
+
95
+ - Node.js: >= 20
96
+
97
+ ---
98
+
99
+ ## Changelog
100
+
101
+ The [CHANGELOG.md](CHANGELOG.md) file is **automatically generated** during pull requests from Conventional Commits (`feat`, `fix`, `refactor`, `perf`). Do not edit it manually.
102
+
103
+ ---
104
+
105
+ Made with ❤️ by My F* Startup
@@ -0,0 +1,3 @@
1
+ export { createInternalAuthHandler, internalAuthMiddleware, internalAuthOptionsFromEnvironment } from "./internalAuth";
2
+ export type { IdTokenVerifier, InternalAuthOptions, RequestWithHeaders } from "./internalAuth";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,kCAAkC,EAAE,MAAM,gBAAgB,CAAC;AAEvH,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.internalAuthOptionsFromEnvironment = exports.internalAuthMiddleware = exports.createInternalAuthHandler = void 0;
4
+ var internalAuth_1 = require("./internalAuth");
5
+ Object.defineProperty(exports, "createInternalAuthHandler", { enumerable: true, get: function () { return internalAuth_1.createInternalAuthHandler; } });
6
+ Object.defineProperty(exports, "internalAuthMiddleware", { enumerable: true, get: function () { return internalAuth_1.internalAuthMiddleware; } });
7
+ Object.defineProperty(exports, "internalAuthOptionsFromEnvironment", { enumerable: true, get: function () { return internalAuth_1.internalAuthOptionsFromEnvironment; } });
@@ -0,0 +1,52 @@
1
+ export type IdTokenVerifier = {
2
+ verifyIdToken: (options: {
3
+ idToken: string;
4
+ audience: string;
5
+ }) => Promise<{
6
+ getPayload: () => {
7
+ email?: string;
8
+ email_verified?: boolean;
9
+ } | undefined;
10
+ }>;
11
+ };
12
+ export type InternalAuthOptions = {
13
+ /** Emails of the service accounts allowed to call the internal routes. */
14
+ allowedCallers: readonly string[];
15
+ /** Audience the token must be issued for: the URL of the service. */
16
+ audience?: string;
17
+ /** Accept every request, for local development against emulators. */
18
+ skip?: boolean;
19
+ /** Verifies the token. Defaults to the Google auth library, which checks the signature and the audience. */
20
+ verifier?: IdTokenVerifier;
21
+ };
22
+ export type RequestWithHeaders = {
23
+ headers: {
24
+ authorization?: string;
25
+ };
26
+ };
27
+ /**
28
+ * Checks a request to an internal route, such as a job called by Cloud Scheduler: it must carry a
29
+ * bearer ID token issued by Google for `audience`, with the verified email of one of the
30
+ * `allowedCallers`. Without an audience or allowed callers every request is rejected.
31
+ *
32
+ * The returned function answers `true` or `false`, so it can be used as the security handler of an
33
+ * OpenAPI validator, or wrapped in a middleware with `internalAuthMiddleware`.
34
+ */
35
+ export declare const createInternalAuthHandler: (options: InternalAuthOptions) => (req: RequestWithHeaders) => Promise<boolean>;
36
+ /**
37
+ * Options from `INTERNAL_ALLOWED_CALLERS` (comma separated emails) and `INTERNAL_AUDIENCE`. The check
38
+ * is skipped when an emulator host is configured, since there is no Google issued token against the
39
+ * emulators.
40
+ */
41
+ export declare const internalAuthOptionsFromEnvironment: (environment?: NodeJS.ProcessEnv) => InternalAuthOptions;
42
+ type MiddlewareResponse = {
43
+ status: (code: number) => {
44
+ end: () => void;
45
+ };
46
+ };
47
+ /**
48
+ * Express style middleware: calls `next()` for an accepted request and answers 401 otherwise.
49
+ */
50
+ export declare const internalAuthMiddleware: (options: InternalAuthOptions) => (req: RequestWithHeaders, res: MiddlewareResponse, next: (error?: unknown) => void) => void;
51
+ export {};
52
+ //# sourceMappingURL=internalAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internalAuth.d.ts","sourceRoot":"","sources":["../src/internalAuth.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,GAAG;IAC5B,aAAa,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC;QACzE,UAAU,EAAE,MAAM;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,cAAc,CAAC,EAAE,OAAO,CAAA;SAAE,GAAG,SAAS,CAAC;KAC5E,CAAC,CAAC;CACJ,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,0EAA0E;IAC1E,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,4GAA4G;IAC5G,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC,CAAC;AASF;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,mBAAmB,MAGtD,KAAK,kBAAkB,KAAG,OAAO,CAAC,OAAO,CAqBxD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,GAC7C,cAAa,MAAM,CAAC,UAAwB,KAC3C,mBAOD,CAAC;AAEH,KAAK,kBAAkB,GAAG;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;CAC/C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,mBAAmB,MAGzD,KAAK,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,KAAG,IAM7F,CAAC"}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.internalAuthMiddleware = exports.internalAuthOptionsFromEnvironment = exports.createInternalAuthHandler = void 0;
4
+ const google_auth_library_1 = require("google-auth-library");
5
+ const EMULATOR_VARIABLES = [
6
+ "FIRESTORE_EMULATOR_HOST",
7
+ "FIREBASE_DATABASE_EMULATOR_HOST",
8
+ "FIREBASE_AUTH_EMULATOR_HOST",
9
+ "PUBSUB_EMULATOR_HOST",
10
+ ];
11
+ /**
12
+ * Checks a request to an internal route, such as a job called by Cloud Scheduler: it must carry a
13
+ * bearer ID token issued by Google for `audience`, with the verified email of one of the
14
+ * `allowedCallers`. Without an audience or allowed callers every request is rejected.
15
+ *
16
+ * The returned function answers `true` or `false`, so it can be used as the security handler of an
17
+ * OpenAPI validator, or wrapped in a middleware with `internalAuthMiddleware`.
18
+ */
19
+ const createInternalAuthHandler = (options) => {
20
+ const verifier = options.verifier ?? new google_auth_library_1.OAuth2Client();
21
+ return async (req) => {
22
+ if (options.skip)
23
+ return true;
24
+ const { audience, allowedCallers } = options;
25
+ const header = req.headers.authorization;
26
+ if (!audience || allowedCallers.length === 0 || !header?.startsWith("Bearer ")) {
27
+ return false;
28
+ }
29
+ try {
30
+ const ticket = await verifier.verifyIdToken({
31
+ idToken: header.substring("Bearer ".length),
32
+ audience,
33
+ });
34
+ const payload = ticket.getPayload();
35
+ return Boolean(payload?.email_verified && payload.email && allowedCallers.includes(payload.email));
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ };
41
+ };
42
+ exports.createInternalAuthHandler = createInternalAuthHandler;
43
+ /**
44
+ * Options from `INTERNAL_ALLOWED_CALLERS` (comma separated emails) and `INTERNAL_AUDIENCE`. The check
45
+ * is skipped when an emulator host is configured, since there is no Google issued token against the
46
+ * emulators.
47
+ */
48
+ const internalAuthOptionsFromEnvironment = (environment = process.env) => ({
49
+ allowedCallers: (environment.INTERNAL_ALLOWED_CALLERS ?? "")
50
+ .split(",")
51
+ .map((caller) => caller.trim())
52
+ .filter(Boolean),
53
+ audience: environment.INTERNAL_AUDIENCE,
54
+ skip: EMULATOR_VARIABLES.some((name) => Boolean(environment[name])),
55
+ });
56
+ exports.internalAuthOptionsFromEnvironment = internalAuthOptionsFromEnvironment;
57
+ /**
58
+ * Express style middleware: calls `next()` for an accepted request and answers 401 otherwise.
59
+ */
60
+ const internalAuthMiddleware = (options) => {
61
+ const isAllowed = (0, exports.createInternalAuthHandler)(options);
62
+ return (req, res, next) => {
63
+ isAllowed(req).then((allowed) => (allowed ? next() : res.status(401).end()), (error) => next(error));
64
+ };
65
+ };
66
+ exports.internalAuthMiddleware = internalAuthMiddleware;
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@my-f-startup/google-internal-auth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Authentication of internal calls between Google Cloud services: accepts a request only with an ID token issued by Google for the audience of the service and the verified email of an allowed caller.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.build.json",
9
+ "build:ts": "tsc",
10
+ "typecheck": "tsc --noEmit",
11
+ "lint": "eslint .",
12
+ "lint:fix": "eslint . --fix",
13
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
14
+ "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
15
+ "test": "tsx --test --test-concurrency=1 test/**/*.spec.ts",
16
+ "test:unit": "tsx --test --test-concurrency=1 test/*.unit.spec.ts"
17
+ },
18
+ "keywords": [
19
+ "google-cloud",
20
+ "cloud-run",
21
+ "cloud-scheduler",
22
+ "authentication",
23
+ "id-token",
24
+ "oidc",
25
+ "service-to-service"
26
+ ],
27
+ "author": "My Fucking Startup",
28
+ "license": "MIT",
29
+ "type": "commonjs",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/my-f-startup/google-internal-auth.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/my-f-startup/google-internal-auth/issues"
36
+ },
37
+ "homepage": "https://github.com/my-f-startup/google-internal-auth#readme",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "default": "./dist/index.js"
42
+ }
43
+ },
44
+ "files": [
45
+ "dist"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "engines": {
51
+ "node": ">=20"
52
+ },
53
+ "dependencies": {
54
+ "google-auth-library": "^9.15.1"
55
+ },
56
+ "devDependencies": {
57
+ "@eslint/js": "^9.39.5",
58
+ "@types/node": "^25.9.9",
59
+ "@typescript-eslint/eslint-plugin": "^8.72.1",
60
+ "@typescript-eslint/parser": "^8.72.1",
61
+ "eslint": "^9.39.5",
62
+ "eslint-config-prettier": "^9.1.2",
63
+ "eslint-plugin-prettier": "^5.5.7",
64
+ "globals": "^17.14.0",
65
+ "prettier": "^3.9.10",
66
+ "tsx": "^4.23.15",
67
+ "typescript": "^5.9.3"
68
+ }
69
+ }