@sebspark/gcp-iam 3.0.5 → 3.0.7
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/dist/{index.d.ts → index.d.mts} +9 -4
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +152 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +5 -5
- package/dist/index.js +0 -156
- package/dist/index.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//#region src/apiGatewayToken.d.ts
|
|
1
2
|
/**
|
|
2
3
|
* Generate a system token for the API Gateway.
|
|
3
4
|
* This is intended to be run under the context of the service account signing the JWT.
|
|
@@ -6,9 +7,12 @@
|
|
|
6
7
|
* @param logger An optional logger to use for logging.
|
|
7
8
|
* @returns A JWT.
|
|
8
9
|
*/
|
|
9
|
-
declare const getApiGatewayTokenByUrl: ({
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
declare const getApiGatewayTokenByUrl: ({
|
|
11
|
+
apiURL,
|
|
12
|
+
key
|
|
13
|
+
}: {
|
|
14
|
+
apiURL: string;
|
|
15
|
+
key?: string;
|
|
12
16
|
}) => Promise<string>;
|
|
13
17
|
/**
|
|
14
18
|
*
|
|
@@ -21,5 +25,6 @@ declare const clearCache: (key: string) => Promise<void>;
|
|
|
21
25
|
* @returns ID Token.
|
|
22
26
|
*/
|
|
23
27
|
declare const getApiGatewayTokenByClientId: (clientId: string) => Promise<string>;
|
|
24
|
-
|
|
28
|
+
//#endregion
|
|
25
29
|
export { clearCache, getApiGatewayTokenByClientId, getApiGatewayTokenByUrl };
|
|
30
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/apiGatewayToken.ts"],"sourcesContent":[],"mappings":";;AA2GA;;;;;AAiBA;AAsDA;cAvEa;;;;;;MAMT;;;;;cAWS,6BAA+B;;;;;;cAsD/B,oDAEV"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { IAMCredentialsClient } from "@google-cloud/iam-credentials";
|
|
2
|
+
import { getLogger } from "@sebspark/otel";
|
|
3
|
+
import { GoogleAuth } from "google-auth-library";
|
|
4
|
+
|
|
5
|
+
//#region src/lruCache.ts
|
|
6
|
+
var LruCache = class {
|
|
7
|
+
values = /* @__PURE__ */ new Map();
|
|
8
|
+
maxEntries = 1e4;
|
|
9
|
+
defaultTTL = 1e3 * 10;
|
|
10
|
+
constructor(props) {
|
|
11
|
+
this.defaultTTL = props?.ttl ?? this.defaultTTL;
|
|
12
|
+
this.maxEntries = props?.maxEntries ?? this.maxEntries;
|
|
13
|
+
}
|
|
14
|
+
get(key) {
|
|
15
|
+
if (this.values.has(key)) {
|
|
16
|
+
const entry = this.values.get(key);
|
|
17
|
+
if (Date.now() - entry.timestamp > entry.ttl) {
|
|
18
|
+
this.values.delete(key);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.values.delete(key);
|
|
22
|
+
this.values.set(key, entry);
|
|
23
|
+
return entry.data;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
put(key, value, ttl) {
|
|
27
|
+
if (this.values.size >= this.maxEntries) {
|
|
28
|
+
const keyToDelete = this.values.keys().next().value;
|
|
29
|
+
this.values.delete(keyToDelete);
|
|
30
|
+
}
|
|
31
|
+
this.values.set(key, {
|
|
32
|
+
data: value,
|
|
33
|
+
timestamp: Date.now(),
|
|
34
|
+
ttl: ttl || this.defaultTTL
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
clear(key) {
|
|
38
|
+
this.values.delete(key);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/apiGatewayToken.ts
|
|
44
|
+
const expInSeconds = 3600;
|
|
45
|
+
const apiGatewayJwtCache = new LruCache();
|
|
46
|
+
const logger = getLogger("gcp-iam");
|
|
47
|
+
const generateTokenByUrl = async ({ apiURL, key }) => {
|
|
48
|
+
try {
|
|
49
|
+
const iamClient = new IAMCredentialsClient();
|
|
50
|
+
const serviceAccountEmail = (await new GoogleAuth().getCredentials()).client_email;
|
|
51
|
+
if (!serviceAccountEmail) throw new Error("No service account e-mail could be found.");
|
|
52
|
+
logger.info(`Service account e-mail being used: ${serviceAccountEmail}`);
|
|
53
|
+
const headerBase64 = Buffer.from(JSON.stringify({
|
|
54
|
+
alg: "RS256",
|
|
55
|
+
typ: "JWT"
|
|
56
|
+
})).toString("base64");
|
|
57
|
+
/**
|
|
58
|
+
* JWT Payload.
|
|
59
|
+
*/
|
|
60
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
61
|
+
const payload = {
|
|
62
|
+
iss: serviceAccountEmail,
|
|
63
|
+
sub: serviceAccountEmail,
|
|
64
|
+
aud: apiURL,
|
|
65
|
+
iat: now,
|
|
66
|
+
exp: now + expInSeconds
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* JWT Signature.
|
|
70
|
+
*/
|
|
71
|
+
const unsignedJWT = `${headerBase64}.${Buffer.from(JSON.stringify(payload)).toString("base64")}`;
|
|
72
|
+
const [response] = await iamClient.signBlob({
|
|
73
|
+
delegates: [serviceAccountEmail],
|
|
74
|
+
name: `projects/-/serviceAccounts/${serviceAccountEmail}`,
|
|
75
|
+
payload: new Uint8Array(Buffer.from(unsignedJWT))
|
|
76
|
+
});
|
|
77
|
+
if (!response.signedBlob) throw new Error("signBlob(...) returned an empty response. Cannot sign JWT.");
|
|
78
|
+
logger.debug(`New JWT for ${key || apiURL} created. Signed with ${response.keyId}.`);
|
|
79
|
+
return `${unsignedJWT}.${Buffer.from(response.signedBlob).toString("base64")}`;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (process.env.GCP_IAM_SOFT_FAIL === "true") {
|
|
82
|
+
logger.info("Soft fail enabled, returning empty JWT");
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
logger.error("Error generating system JWT", error);
|
|
86
|
+
throw new Error(`Error generating system JWT: ${JSON.stringify(error)}`);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Generate a system token for the API Gateway.
|
|
91
|
+
* This is intended to be run under the context of the service account signing the JWT.
|
|
92
|
+
* @param apiUrl The URL of the API Gateway including the path of the specific API to be accessed using the token.
|
|
93
|
+
* @param serviceAccountEmail The email of the service account to be used.
|
|
94
|
+
* @param logger An optional logger to use for logging.
|
|
95
|
+
* @returns A JWT.
|
|
96
|
+
*/
|
|
97
|
+
const getApiGatewayTokenByUrl = async ({ apiURL, key }) => {
|
|
98
|
+
return checkCache({
|
|
99
|
+
cacheKey: key || apiURL,
|
|
100
|
+
generate: () => generateTokenByUrl({
|
|
101
|
+
apiURL,
|
|
102
|
+
key
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
*
|
|
108
|
+
* @param key Clears a cached JWT by key.
|
|
109
|
+
*/
|
|
110
|
+
const clearCache = async (key) => {
|
|
111
|
+
apiGatewayJwtCache.clear(key);
|
|
112
|
+
};
|
|
113
|
+
const checkCache = ({ cacheKey, generate }) => {
|
|
114
|
+
/**
|
|
115
|
+
* Check if there is a cached JWT
|
|
116
|
+
*/
|
|
117
|
+
const cachedJwt = apiGatewayJwtCache.get(cacheKey);
|
|
118
|
+
if (cachedJwt) {
|
|
119
|
+
logger.debug(`JWT for ${cacheKey} found in cache.`);
|
|
120
|
+
return cachedJwt;
|
|
121
|
+
}
|
|
122
|
+
const jwtPromise = generate();
|
|
123
|
+
apiGatewayJwtCache.put(cacheKey, jwtPromise, expInSeconds / 2 * 1e3);
|
|
124
|
+
return jwtPromise;
|
|
125
|
+
};
|
|
126
|
+
const generateTokenByClientId = async (clientId) => {
|
|
127
|
+
try {
|
|
128
|
+
return await (await new GoogleAuth({ scopes: "https://www.googleapis.com/auth/cloud-platform" }).getIdTokenClient(clientId)).idTokenProvider.fetchIdToken(clientId);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (process.env.GCP_IAM_SOFT_FAIL === "true") {
|
|
131
|
+
logger.info("Soft fail enabled, returning empty JWT.");
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
logger.error("Error generating system JWT", error);
|
|
135
|
+
throw new Error(`Error generating system JWT: ${JSON.stringify(error)}`);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Generates a JWT for the API Gateway, using Client ID as audience.
|
|
140
|
+
* @param clientId OAUTH Client ID.
|
|
141
|
+
* @returns ID Token.
|
|
142
|
+
*/
|
|
143
|
+
const getApiGatewayTokenByClientId = async (clientId) => {
|
|
144
|
+
return checkCache({
|
|
145
|
+
cacheKey: clientId,
|
|
146
|
+
generate: () => generateTokenByClientId(clientId)
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
export { clearCache, getApiGatewayTokenByClientId, getApiGatewayTokenByUrl };
|
|
152
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/lruCache.ts","../src/apiGatewayToken.ts"],"sourcesContent":["export class LruCache<T> {\n private values: Map<string, { timestamp: number; data: T; ttl: number }> =\n new Map<string, { timestamp: number; data: T; ttl: number }>()\n private readonly maxEntries: number = 10000\n private readonly defaultTTL: number = 1000 * 10 // 10 seconds\n\n constructor(props?: { ttl?: number; maxEntries?: number }) {\n this.defaultTTL = props?.ttl ?? this.defaultTTL\n this.maxEntries = props?.maxEntries ?? this.maxEntries\n }\n\n public get(key: string): T | undefined {\n const hasKey = this.values.has(key)\n\n if (hasKey) {\n // peek the entry, re-insert for LRU strategy\n const entry = this.values.get(key) as {\n timestamp: number\n data: T\n ttl: number\n }\n if (Date.now() - entry.timestamp > entry.ttl) {\n this.values.delete(key)\n return undefined\n }\n this.values.delete(key)\n this.values.set(key, entry)\n return entry.data\n }\n }\n\n public put(key: string, value: T, ttl?: number) {\n if (this.values.size >= this.maxEntries) {\n // least-recently used cache eviction strategy\n const keyToDelete = this.values.keys().next().value as string\n this.values.delete(keyToDelete)\n }\n\n this.values.set(key, {\n data: value,\n timestamp: Date.now(),\n ttl: ttl || this.defaultTTL,\n })\n }\n\n public clear(key: string) {\n this.values.delete(key)\n }\n}\n","import { IAMCredentialsClient } from '@google-cloud/iam-credentials'\nimport { getLogger } from '@sebspark/otel'\nimport { GoogleAuth } from 'google-auth-library'\nimport { LruCache } from './lruCache'\n\nconst expInSeconds = 60 * 60\n// TODO: Make ttl changeable from getApiGatewayToken function\nconst apiGatewayJwtCache = new LruCache<Promise<string>>()\n\nconst logger = getLogger('gcp-iam')\n\nconst generateTokenByUrl = async ({\n apiURL,\n key,\n}: {\n apiURL: string\n key?: string\n}) => {\n try {\n const iamClient = new IAMCredentialsClient()\n const auth = new GoogleAuth()\n const cred = await auth.getCredentials()\n const serviceAccountEmail = cred.client_email\n\n if (!serviceAccountEmail) {\n throw new Error('No service account e-mail could be found.')\n }\n\n // Remove when verified\n logger.info(`Service account e-mail being used: ${serviceAccountEmail}`)\n\n /**\n * JWT Header.\n */\n\n const header = {\n alg: 'RS256',\n typ: 'JWT',\n }\n const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64')\n\n /**\n * JWT Payload.\n */\n\n const now = Math.floor(Date.now() / 1000)\n const payload = {\n iss: serviceAccountEmail,\n sub: serviceAccountEmail,\n aud: apiURL,\n iat: now,\n exp: now + expInSeconds,\n }\n\n const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString(\n 'base64'\n )\n\n /**\n * JWT Signature.\n */\n\n const unsignedJWT = `${headerBase64}.${payloadBase64}`\n const [response] = await iamClient.signBlob({\n delegates: [serviceAccountEmail],\n name: `projects/-/serviceAccounts/${serviceAccountEmail}`,\n payload: new Uint8Array(Buffer.from(unsignedJWT)),\n })\n\n if (!response.signedBlob) {\n throw new Error(\n 'signBlob(...) returned an empty response. Cannot sign JWT.'\n )\n }\n\n // Debug.\n logger.debug(\n `New JWT for ${key || apiURL} created. Signed with ${response.keyId}.`\n )\n\n // Encode the binary signature to Base64.\n const signature = Buffer.from(response.signedBlob).toString('base64')\n\n // Combine into the final JWT.\n const signedJWT = `${unsignedJWT}.${signature}`\n\n return signedJWT\n } catch (error) {\n if (process.env.GCP_IAM_SOFT_FAIL === 'true') {\n logger.info('Soft fail enabled, returning empty JWT')\n return ''\n }\n\n logger.error('Error generating system JWT', error as Error)\n\n throw new Error(`Error generating system JWT: ${JSON.stringify(error)}`)\n }\n}\n\n/**\n * Generate a system token for the API Gateway.\n * This is intended to be run under the context of the service account signing the JWT.\n * @param apiUrl The URL of the API Gateway including the path of the specific API to be accessed using the token.\n * @param serviceAccountEmail The email of the service account to be used.\n * @param logger An optional logger to use for logging.\n * @returns A JWT.\n */\nexport const getApiGatewayTokenByUrl = async ({\n apiURL,\n key,\n}: {\n apiURL: string\n key?: string\n}): Promise<string> => {\n return checkCache({\n cacheKey: key || apiURL,\n generate: () => generateTokenByUrl({ apiURL, key }),\n })\n}\n\n/**\n *\n * @param key Clears a cached JWT by key.\n */\nexport const clearCache = async (key: string) => {\n apiGatewayJwtCache.clear(key)\n}\n\nconst checkCache = ({\n cacheKey,\n generate,\n}: {\n cacheKey: string\n generate: () => Promise<string>\n}) => {\n /**\n * Check if there is a cached JWT\n */\n\n const cachedJwt = apiGatewayJwtCache.get(cacheKey)\n if (cachedJwt) {\n logger.debug(`JWT for ${cacheKey} found in cache.`)\n return cachedJwt\n }\n\n const jwtPromise = generate()\n\n // cache generated jwt\n apiGatewayJwtCache.put(cacheKey, jwtPromise, (expInSeconds / 2) * 1000)\n\n return jwtPromise\n}\n\nconst generateTokenByClientId = async (clientId: string) => {\n try {\n const auth = new GoogleAuth({\n scopes: 'https://www.googleapis.com/auth/cloud-platform',\n })\n const client = await auth.getIdTokenClient(clientId)\n\n return await client.idTokenProvider.fetchIdToken(clientId)\n } catch (error) {\n if (process.env.GCP_IAM_SOFT_FAIL === 'true') {\n logger.info('Soft fail enabled, returning empty JWT.')\n return ''\n }\n\n logger.error('Error generating system JWT', error as Error)\n\n throw new Error(`Error generating system JWT: ${JSON.stringify(error)}`)\n }\n}\n\n/**\n * Generates a JWT for the API Gateway, using Client ID as audience.\n * @param clientId OAUTH Client ID.\n * @returns ID Token.\n */\nexport const getApiGatewayTokenByClientId = async (\n clientId: string\n): Promise<string> => {\n return checkCache({\n cacheKey: clientId,\n generate: () => generateTokenByClientId(clientId),\n })\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sebspark/gcp-iam",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.7",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.
|
|
7
|
-
"types": "dist/index.d.
|
|
6
|
+
"main": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.mts",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist"
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "
|
|
12
|
+
"build": "tsdown src/index.ts",
|
|
13
13
|
"depcheck": "depcheck",
|
|
14
14
|
"dev": "tsc --watch --noEmit",
|
|
15
15
|
"lint": "biome check .",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@sebspark/tsconfig": "*",
|
|
21
|
-
"
|
|
21
|
+
"tsdown": "0.16.6",
|
|
22
22
|
"vitest": "4.0.6"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
package/dist/index.js
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
// src/apiGatewayToken.ts
|
|
2
|
-
import { IAMCredentialsClient } from "@google-cloud/iam-credentials";
|
|
3
|
-
import { getLogger } from "@sebspark/otel";
|
|
4
|
-
import { GoogleAuth } from "google-auth-library";
|
|
5
|
-
|
|
6
|
-
// src/lruCache.ts
|
|
7
|
-
var LruCache = class {
|
|
8
|
-
values = /* @__PURE__ */ new Map();
|
|
9
|
-
maxEntries = 1e4;
|
|
10
|
-
defaultTTL = 1e3 * 10;
|
|
11
|
-
// 10 seconds
|
|
12
|
-
constructor(props) {
|
|
13
|
-
this.defaultTTL = props?.ttl ?? this.defaultTTL;
|
|
14
|
-
this.maxEntries = props?.maxEntries ?? this.maxEntries;
|
|
15
|
-
}
|
|
16
|
-
get(key) {
|
|
17
|
-
const hasKey = this.values.has(key);
|
|
18
|
-
if (hasKey) {
|
|
19
|
-
const entry = this.values.get(key);
|
|
20
|
-
if (Date.now() - entry.timestamp > entry.ttl) {
|
|
21
|
-
this.values.delete(key);
|
|
22
|
-
return void 0;
|
|
23
|
-
}
|
|
24
|
-
this.values.delete(key);
|
|
25
|
-
this.values.set(key, entry);
|
|
26
|
-
return entry.data;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
put(key, value, ttl) {
|
|
30
|
-
if (this.values.size >= this.maxEntries) {
|
|
31
|
-
const keyToDelete = this.values.keys().next().value;
|
|
32
|
-
this.values.delete(keyToDelete);
|
|
33
|
-
}
|
|
34
|
-
this.values.set(key, {
|
|
35
|
-
data: value,
|
|
36
|
-
timestamp: Date.now(),
|
|
37
|
-
ttl: ttl || this.defaultTTL
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
clear(key) {
|
|
41
|
-
this.values.delete(key);
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// src/apiGatewayToken.ts
|
|
46
|
-
var expInSeconds = 60 * 60;
|
|
47
|
-
var apiGatewayJwtCache = new LruCache();
|
|
48
|
-
var logger = getLogger("gcp-iam");
|
|
49
|
-
var generateTokenByUrl = async ({
|
|
50
|
-
apiURL,
|
|
51
|
-
key
|
|
52
|
-
}) => {
|
|
53
|
-
try {
|
|
54
|
-
const iamClient = new IAMCredentialsClient();
|
|
55
|
-
const auth = new GoogleAuth();
|
|
56
|
-
const cred = await auth.getCredentials();
|
|
57
|
-
const serviceAccountEmail = cred.client_email;
|
|
58
|
-
if (!serviceAccountEmail) {
|
|
59
|
-
throw new Error("No service account e-mail could be found.");
|
|
60
|
-
}
|
|
61
|
-
logger.info(`Service account e-mail being used: ${serviceAccountEmail}`);
|
|
62
|
-
const header = {
|
|
63
|
-
alg: "RS256",
|
|
64
|
-
typ: "JWT"
|
|
65
|
-
};
|
|
66
|
-
const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64");
|
|
67
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
68
|
-
const payload = {
|
|
69
|
-
iss: serviceAccountEmail,
|
|
70
|
-
sub: serviceAccountEmail,
|
|
71
|
-
aud: apiURL,
|
|
72
|
-
iat: now,
|
|
73
|
-
exp: now + expInSeconds
|
|
74
|
-
};
|
|
75
|
-
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString(
|
|
76
|
-
"base64"
|
|
77
|
-
);
|
|
78
|
-
const unsignedJWT = `${headerBase64}.${payloadBase64}`;
|
|
79
|
-
const [response] = await iamClient.signBlob({
|
|
80
|
-
delegates: [serviceAccountEmail],
|
|
81
|
-
name: `projects/-/serviceAccounts/${serviceAccountEmail}`,
|
|
82
|
-
payload: new Uint8Array(Buffer.from(unsignedJWT))
|
|
83
|
-
});
|
|
84
|
-
if (!response.signedBlob) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
"signBlob(...) returned an empty response. Cannot sign JWT."
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
logger.debug(
|
|
90
|
-
`New JWT for ${key || apiURL} created. Signed with ${response.keyId}.`
|
|
91
|
-
);
|
|
92
|
-
const signature = Buffer.from(response.signedBlob).toString("base64");
|
|
93
|
-
const signedJWT = `${unsignedJWT}.${signature}`;
|
|
94
|
-
return signedJWT;
|
|
95
|
-
} catch (error) {
|
|
96
|
-
if (process.env.GCP_IAM_SOFT_FAIL === "true") {
|
|
97
|
-
logger.info("Soft fail enabled, returning empty JWT");
|
|
98
|
-
return "";
|
|
99
|
-
}
|
|
100
|
-
logger.error("Error generating system JWT", error);
|
|
101
|
-
throw new Error(`Error generating system JWT: ${JSON.stringify(error)}`);
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
var getApiGatewayTokenByUrl = async ({
|
|
105
|
-
apiURL,
|
|
106
|
-
key
|
|
107
|
-
}) => {
|
|
108
|
-
return checkCache({
|
|
109
|
-
cacheKey: key || apiURL,
|
|
110
|
-
generate: () => generateTokenByUrl({ apiURL, key })
|
|
111
|
-
});
|
|
112
|
-
};
|
|
113
|
-
var clearCache = async (key) => {
|
|
114
|
-
apiGatewayJwtCache.clear(key);
|
|
115
|
-
};
|
|
116
|
-
var checkCache = ({
|
|
117
|
-
cacheKey,
|
|
118
|
-
generate
|
|
119
|
-
}) => {
|
|
120
|
-
const cachedJwt = apiGatewayJwtCache.get(cacheKey);
|
|
121
|
-
if (cachedJwt) {
|
|
122
|
-
logger.debug(`JWT for ${cacheKey} found in cache.`);
|
|
123
|
-
return cachedJwt;
|
|
124
|
-
}
|
|
125
|
-
const jwtPromise = generate();
|
|
126
|
-
apiGatewayJwtCache.put(cacheKey, jwtPromise, expInSeconds / 2 * 1e3);
|
|
127
|
-
return jwtPromise;
|
|
128
|
-
};
|
|
129
|
-
var generateTokenByClientId = async (clientId) => {
|
|
130
|
-
try {
|
|
131
|
-
const auth = new GoogleAuth({
|
|
132
|
-
scopes: "https://www.googleapis.com/auth/cloud-platform"
|
|
133
|
-
});
|
|
134
|
-
const client = await auth.getIdTokenClient(clientId);
|
|
135
|
-
return await client.idTokenProvider.fetchIdToken(clientId);
|
|
136
|
-
} catch (error) {
|
|
137
|
-
if (process.env.GCP_IAM_SOFT_FAIL === "true") {
|
|
138
|
-
logger.info("Soft fail enabled, returning empty JWT.");
|
|
139
|
-
return "";
|
|
140
|
-
}
|
|
141
|
-
logger.error("Error generating system JWT", error);
|
|
142
|
-
throw new Error(`Error generating system JWT: ${JSON.stringify(error)}`);
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
var getApiGatewayTokenByClientId = async (clientId) => {
|
|
146
|
-
return checkCache({
|
|
147
|
-
cacheKey: clientId,
|
|
148
|
-
generate: () => generateTokenByClientId(clientId)
|
|
149
|
-
});
|
|
150
|
-
};
|
|
151
|
-
export {
|
|
152
|
-
clearCache,
|
|
153
|
-
getApiGatewayTokenByClientId,
|
|
154
|
-
getApiGatewayTokenByUrl
|
|
155
|
-
};
|
|
156
|
-
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/apiGatewayToken.ts","../src/lruCache.ts"],"sourcesContent":["import { IAMCredentialsClient } from '@google-cloud/iam-credentials'\nimport { getLogger } from '@sebspark/otel'\nimport { GoogleAuth } from 'google-auth-library'\nimport { LruCache } from './lruCache'\n\nconst expInSeconds = 60 * 60\n// TODO: Make ttl changeable from getApiGatewayToken function\nconst apiGatewayJwtCache = new LruCache<Promise<string>>()\n\nconst logger = getLogger('gcp-iam')\n\nconst generateTokenByUrl = async ({\n apiURL,\n key,\n}: {\n apiURL: string\n key?: string\n}) => {\n try {\n const iamClient = new IAMCredentialsClient()\n const auth = new GoogleAuth()\n const cred = await auth.getCredentials()\n const serviceAccountEmail = cred.client_email\n\n if (!serviceAccountEmail) {\n throw new Error('No service account e-mail could be found.')\n }\n\n // Remove when verified\n logger.info(`Service account e-mail being used: ${serviceAccountEmail}`)\n\n /**\n * JWT Header.\n */\n\n const header = {\n alg: 'RS256',\n typ: 'JWT',\n }\n const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64')\n\n /**\n * JWT Payload.\n */\n\n const now = Math.floor(Date.now() / 1000)\n const payload = {\n iss: serviceAccountEmail,\n sub: serviceAccountEmail,\n aud: apiURL,\n iat: now,\n exp: now + expInSeconds,\n }\n\n const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString(\n 'base64'\n )\n\n /**\n * JWT Signature.\n */\n\n const unsignedJWT = `${headerBase64}.${payloadBase64}`\n const [response] = await iamClient.signBlob({\n delegates: [serviceAccountEmail],\n name: `projects/-/serviceAccounts/${serviceAccountEmail}`,\n payload: new Uint8Array(Buffer.from(unsignedJWT)),\n })\n\n if (!response.signedBlob) {\n throw new Error(\n 'signBlob(...) returned an empty response. Cannot sign JWT.'\n )\n }\n\n // Debug.\n logger.debug(\n `New JWT for ${key || apiURL} created. Signed with ${response.keyId}.`\n )\n\n // Encode the binary signature to Base64.\n const signature = Buffer.from(response.signedBlob).toString('base64')\n\n // Combine into the final JWT.\n const signedJWT = `${unsignedJWT}.${signature}`\n\n return signedJWT\n } catch (error) {\n if (process.env.GCP_IAM_SOFT_FAIL === 'true') {\n logger.info('Soft fail enabled, returning empty JWT')\n return ''\n }\n\n logger.error('Error generating system JWT', error as Error)\n\n throw new Error(`Error generating system JWT: ${JSON.stringify(error)}`)\n }\n}\n\n/**\n * Generate a system token for the API Gateway.\n * This is intended to be run under the context of the service account signing the JWT.\n * @param apiUrl The URL of the API Gateway including the path of the specific API to be accessed using the token.\n * @param serviceAccountEmail The email of the service account to be used.\n * @param logger An optional logger to use for logging.\n * @returns A JWT.\n */\nexport const getApiGatewayTokenByUrl = async ({\n apiURL,\n key,\n}: {\n apiURL: string\n key?: string\n}): Promise<string> => {\n return checkCache({\n cacheKey: key || apiURL,\n generate: () => generateTokenByUrl({ apiURL, key }),\n })\n}\n\n/**\n *\n * @param key Clears a cached JWT by key.\n */\nexport const clearCache = async (key: string) => {\n apiGatewayJwtCache.clear(key)\n}\n\nconst checkCache = ({\n cacheKey,\n generate,\n}: {\n cacheKey: string\n generate: () => Promise<string>\n}) => {\n /**\n * Check if there is a cached JWT\n */\n\n const cachedJwt = apiGatewayJwtCache.get(cacheKey)\n if (cachedJwt) {\n logger.debug(`JWT for ${cacheKey} found in cache.`)\n return cachedJwt\n }\n\n const jwtPromise = generate()\n\n // cache generated jwt\n apiGatewayJwtCache.put(cacheKey, jwtPromise, (expInSeconds / 2) * 1000)\n\n return jwtPromise\n}\n\nconst generateTokenByClientId = async (clientId: string) => {\n try {\n const auth = new GoogleAuth({\n scopes: 'https://www.googleapis.com/auth/cloud-platform',\n })\n const client = await auth.getIdTokenClient(clientId)\n\n return await client.idTokenProvider.fetchIdToken(clientId)\n } catch (error) {\n if (process.env.GCP_IAM_SOFT_FAIL === 'true') {\n logger.info('Soft fail enabled, returning empty JWT.')\n return ''\n }\n\n logger.error('Error generating system JWT', error as Error)\n\n throw new Error(`Error generating system JWT: ${JSON.stringify(error)}`)\n }\n}\n\n/**\n * Generates a JWT for the API Gateway, using Client ID as audience.\n * @param clientId OAUTH Client ID.\n * @returns ID Token.\n */\nexport const getApiGatewayTokenByClientId = async (\n clientId: string\n): Promise<string> => {\n return checkCache({\n cacheKey: clientId,\n generate: () => generateTokenByClientId(clientId),\n })\n}\n","export class LruCache<T> {\n private values: Map<string, { timestamp: number; data: T; ttl: number }> =\n new Map<string, { timestamp: number; data: T; ttl: number }>()\n private readonly maxEntries: number = 10000\n private readonly defaultTTL: number = 1000 * 10 // 10 seconds\n\n constructor(props?: { ttl?: number; maxEntries?: number }) {\n this.defaultTTL = props?.ttl ?? this.defaultTTL\n this.maxEntries = props?.maxEntries ?? this.maxEntries\n }\n\n public get(key: string): T | undefined {\n const hasKey = this.values.has(key)\n\n if (hasKey) {\n // peek the entry, re-insert for LRU strategy\n const entry = this.values.get(key) as {\n timestamp: number\n data: T\n ttl: number\n }\n if (Date.now() - entry.timestamp > entry.ttl) {\n this.values.delete(key)\n return undefined\n }\n this.values.delete(key)\n this.values.set(key, entry)\n return entry.data\n }\n }\n\n public put(key: string, value: T, ttl?: number) {\n if (this.values.size >= this.maxEntries) {\n // least-recently used cache eviction strategy\n const keyToDelete = this.values.keys().next().value as string\n this.values.delete(keyToDelete)\n }\n\n this.values.set(key, {\n data: value,\n timestamp: Date.now(),\n ttl: ttl || this.defaultTTL,\n })\n }\n\n public clear(key: string) {\n this.values.delete(key)\n }\n}\n"],"mappings":";AAAA,SAAS,4BAA4B;AACrC,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;;;ACFpB,IAAM,WAAN,MAAkB;AAAA,EACf,SACN,oBAAI,IAAyD;AAAA,EAC9C,aAAqB;AAAA,EACrB,aAAqB,MAAO;AAAA;AAAA,EAE7C,YAAY,OAA+C;AACzD,SAAK,aAAa,OAAO,OAAO,KAAK;AACrC,SAAK,aAAa,OAAO,cAAc,KAAK;AAAA,EAC9C;AAAA,EAEO,IAAI,KAA4B;AACrC,UAAM,SAAS,KAAK,OAAO,IAAI,GAAG;AAElC,QAAI,QAAQ;AAEV,YAAM,QAAQ,KAAK,OAAO,IAAI,GAAG;AAKjC,UAAI,KAAK,IAAI,IAAI,MAAM,YAAY,MAAM,KAAK;AAC5C,aAAK,OAAO,OAAO,GAAG;AACtB,eAAO;AAAA,MACT;AACA,WAAK,OAAO,OAAO,GAAG;AACtB,WAAK,OAAO,IAAI,KAAK,KAAK;AAC1B,aAAO,MAAM;AAAA,IACf;AAAA,EACF;AAAA,EAEO,IAAI,KAAa,OAAU,KAAc;AAC9C,QAAI,KAAK,OAAO,QAAQ,KAAK,YAAY;AAEvC,YAAM,cAAc,KAAK,OAAO,KAAK,EAAE,KAAK,EAAE;AAC9C,WAAK,OAAO,OAAO,WAAW;AAAA,IAChC;AAEA,SAAK,OAAO,IAAI,KAAK;AAAA,MACnB,MAAM;AAAA,MACN,WAAW,KAAK,IAAI;AAAA,MACpB,KAAK,OAAO,KAAK;AAAA,IACnB,CAAC;AAAA,EACH;AAAA,EAEO,MAAM,KAAa;AACxB,SAAK,OAAO,OAAO,GAAG;AAAA,EACxB;AACF;;;AD3CA,IAAM,eAAe,KAAK;AAE1B,IAAM,qBAAqB,IAAI,SAA0B;AAEzD,IAAM,SAAS,UAAU,SAAS;AAElC,IAAM,qBAAqB,OAAO;AAAA,EAChC;AAAA,EACA;AACF,MAGM;AACJ,MAAI;AACF,UAAM,YAAY,IAAI,qBAAqB;AAC3C,UAAM,OAAO,IAAI,WAAW;AAC5B,UAAM,OAAO,MAAM,KAAK,eAAe;AACvC,UAAM,sBAAsB,KAAK;AAEjC,QAAI,CAAC,qBAAqB;AACxB,YAAM,IAAI,MAAM,2CAA2C;AAAA,IAC7D;AAGA,WAAO,KAAK,sCAAsC,mBAAmB,EAAE;AAMvE,UAAM,SAAS;AAAA,MACb,KAAK;AAAA,MACL,KAAK;AAAA,IACP;AACA,UAAM,eAAe,OAAO,KAAK,KAAK,UAAU,MAAM,CAAC,EAAE,SAAS,QAAQ;AAM1E,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,UAAM,UAAU;AAAA,MACd,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK,MAAM;AAAA,IACb;AAEA,UAAM,gBAAgB,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,EAAE;AAAA,MACzD;AAAA,IACF;AAMA,UAAM,cAAc,GAAG,YAAY,IAAI,aAAa;AACpD,UAAM,CAAC,QAAQ,IAAI,MAAM,UAAU,SAAS;AAAA,MAC1C,WAAW,CAAC,mBAAmB;AAAA,MAC/B,MAAM,8BAA8B,mBAAmB;AAAA,MACvD,SAAS,IAAI,WAAW,OAAO,KAAK,WAAW,CAAC;AAAA,IAClD,CAAC;AAED,QAAI,CAAC,SAAS,YAAY;AACxB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAGA,WAAO;AAAA,MACL,eAAe,OAAO,MAAM,yBAAyB,SAAS,KAAK;AAAA,IACrE;AAGA,UAAM,YAAY,OAAO,KAAK,SAAS,UAAU,EAAE,SAAS,QAAQ;AAGpE,UAAM,YAAY,GAAG,WAAW,IAAI,SAAS;AAE7C,WAAO;AAAA,EACT,SAAS,OAAO;AACd,QAAI,QAAQ,IAAI,sBAAsB,QAAQ;AAC5C,aAAO,KAAK,wCAAwC;AACpD,aAAO;AAAA,IACT;AAEA,WAAO,MAAM,+BAA+B,KAAc;AAE1D,UAAM,IAAI,MAAM,gCAAgC,KAAK,UAAU,KAAK,CAAC,EAAE;AAAA,EACzE;AACF;AAUO,IAAM,0BAA0B,OAAO;AAAA,EAC5C;AAAA,EACA;AACF,MAGuB;AACrB,SAAO,WAAW;AAAA,IAChB,UAAU,OAAO;AAAA,IACjB,UAAU,MAAM,mBAAmB,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpD,CAAC;AACH;AAMO,IAAM,aAAa,OAAO,QAAgB;AAC/C,qBAAmB,MAAM,GAAG;AAC9B;AAEA,IAAM,aAAa,CAAC;AAAA,EAClB;AAAA,EACA;AACF,MAGM;AAKJ,QAAM,YAAY,mBAAmB,IAAI,QAAQ;AACjD,MAAI,WAAW;AACb,WAAO,MAAM,WAAW,QAAQ,kBAAkB;AAClD,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,SAAS;AAG5B,qBAAmB,IAAI,UAAU,YAAa,eAAe,IAAK,GAAI;AAEtE,SAAO;AACT;AAEA,IAAM,0BAA0B,OAAO,aAAqB;AAC1D,MAAI;AACF,UAAM,OAAO,IAAI,WAAW;AAAA,MAC1B,QAAQ;AAAA,IACV,CAAC;AACD,UAAM,SAAS,MAAM,KAAK,iBAAiB,QAAQ;AAEnD,WAAO,MAAM,OAAO,gBAAgB,aAAa,QAAQ;AAAA,EAC3D,SAAS,OAAO;AACd,QAAI,QAAQ,IAAI,sBAAsB,QAAQ;AAC5C,aAAO,KAAK,yCAAyC;AACrD,aAAO;AAAA,IACT;AAEA,WAAO,MAAM,+BAA+B,KAAc;AAE1D,UAAM,IAAI,MAAM,gCAAgC,KAAK,UAAU,KAAK,CAAC,EAAE;AAAA,EACzE;AACF;AAOO,IAAM,+BAA+B,OAC1C,aACoB;AACpB,SAAO,WAAW;AAAA,IAChB,UAAU;AAAA,IACV,UAAU,MAAM,wBAAwB,QAAQ;AAAA,EAClD,CAAC;AACH;","names":[]}
|