opticedge-cloud-utils 1.0.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/.eslintrc.js +29 -0
- package/dist/verify.d.ts +7 -0
- package/dist/verify.js +23 -0
- package/dist/verify.test.d.ts +1 -0
- package/dist/verify.test.js +79 -0
- package/jest.config.js +11 -0
- package/package.json +30 -0
- package/src/auth/verify.test.ts +89 -0
- package/src/auth/verify.ts +32 -0
- package/tsconfig.json +17 -0
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
root: true,
|
|
3
|
+
parser: '@typescript-eslint/parser',
|
|
4
|
+
parserOptions: {
|
|
5
|
+
ecmaVersion: 2020, // modern JS features
|
|
6
|
+
sourceType: 'module',
|
|
7
|
+
},
|
|
8
|
+
env: {
|
|
9
|
+
node: true,
|
|
10
|
+
jest: true,
|
|
11
|
+
es2020: true,
|
|
12
|
+
},
|
|
13
|
+
plugins: ['@typescript-eslint', 'jest'],
|
|
14
|
+
extends: [
|
|
15
|
+
'eslint:recommended',
|
|
16
|
+
'plugin:@typescript-eslint/recommended',
|
|
17
|
+
'plugin:jest/recommended',
|
|
18
|
+
],
|
|
19
|
+
rules: {
|
|
20
|
+
// Your custom rules here, for example:
|
|
21
|
+
'no-console': 'warn',
|
|
22
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
23
|
+
},
|
|
24
|
+
settings: {
|
|
25
|
+
jest: {
|
|
26
|
+
version: 29, // or your jest version
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
package/dist/verify.d.ts
ADDED
package/dist/verify.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyRequest = verifyRequest;
|
|
4
|
+
const google_auth_library_1 = require("google-auth-library");
|
|
5
|
+
async function verifyRequest(req, options) {
|
|
6
|
+
const authHeader = req.headers['authorization'];
|
|
7
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const token = authHeader.split(' ')[1];
|
|
11
|
+
const client = new google_auth_library_1.OAuth2Client();
|
|
12
|
+
try {
|
|
13
|
+
const ticket = await client.verifyIdToken({
|
|
14
|
+
idToken: token,
|
|
15
|
+
audience: options.allowedAudience,
|
|
16
|
+
});
|
|
17
|
+
const payload = ticket.getPayload();
|
|
18
|
+
return payload?.email === options.allowedServiceAccount;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const verify_1 = require("./verify");
|
|
4
|
+
// Mock the google-auth-library
|
|
5
|
+
jest.mock('google-auth-library', () => {
|
|
6
|
+
return {
|
|
7
|
+
OAuth2Client: jest.fn().mockImplementation(() => ({
|
|
8
|
+
verifyIdToken: jest.fn(({ idToken, audience }) => {
|
|
9
|
+
if (idToken === 'valid-token' && audience === 'audience') {
|
|
10
|
+
return {
|
|
11
|
+
getPayload: () => ({
|
|
12
|
+
email: 'allowed@example.com',
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
else if (idToken === 'wrong-email-token' && audience === 'audience') {
|
|
17
|
+
return {
|
|
18
|
+
getPayload: () => ({
|
|
19
|
+
email: 'unauthorized@example.com',
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
throw new Error('Invalid token');
|
|
24
|
+
}),
|
|
25
|
+
})),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
describe('verifyRequest', () => {
|
|
29
|
+
it('returns true for valid token and matching email', async () => {
|
|
30
|
+
const mockReq = {
|
|
31
|
+
headers: {
|
|
32
|
+
authorization: 'Bearer valid-token',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
const result = await (0, verify_1.verifyRequest)(mockReq, {
|
|
37
|
+
allowedAudience: 'audience',
|
|
38
|
+
allowedServiceAccount: 'allowed@example.com',
|
|
39
|
+
});
|
|
40
|
+
expect(result).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it('returns false for invalid token', async () => {
|
|
43
|
+
const mockReq = {
|
|
44
|
+
headers: {
|
|
45
|
+
authorization: 'Bearer invalid-token',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
const result = await (0, verify_1.verifyRequest)(mockReq, {
|
|
50
|
+
allowedAudience: 'audience',
|
|
51
|
+
allowedServiceAccount: 'allowed@example.com',
|
|
52
|
+
});
|
|
53
|
+
expect(result).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
it('returns false for missing authorization header', async () => {
|
|
56
|
+
const mockReq = {
|
|
57
|
+
headers: {},
|
|
58
|
+
};
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
const result = await (0, verify_1.verifyRequest)(mockReq, {
|
|
61
|
+
allowedAudience: 'audience',
|
|
62
|
+
allowedServiceAccount: 'allowed@example.com',
|
|
63
|
+
});
|
|
64
|
+
expect(result).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
it('returns false for incorrect email', async () => {
|
|
67
|
+
const mockReq = {
|
|
68
|
+
headers: {
|
|
69
|
+
authorization: 'Bearer wrong-email-token',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
const result = await (0, verify_1.verifyRequest)(mockReq, {
|
|
74
|
+
allowedAudience: 'audience',
|
|
75
|
+
allowedServiceAccount: 'allowed@example.com',
|
|
76
|
+
});
|
|
77
|
+
expect(result).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opticedge-cloud-utils",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Common utilities for cloud functions",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest --coverage",
|
|
10
|
+
"lint": "eslint . --ext .ts",
|
|
11
|
+
"prepare": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"author": "Evans Musonda",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"google-auth-library": "^9.15.1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@google-cloud/functions-framework": "^4.0.0",
|
|
20
|
+
"@types/jest": "^29.5.14",
|
|
21
|
+
"@types/node": "^22.15.23",
|
|
22
|
+
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
|
23
|
+
"@typescript-eslint/parser": "^8.33.0",
|
|
24
|
+
"eslint": "^8.57.1",
|
|
25
|
+
"eslint-plugin-jest": "^28.11.1",
|
|
26
|
+
"jest": "^29.7.0",
|
|
27
|
+
"ts-jest": "^29.3.4",
|
|
28
|
+
"typescript": "^5.8.3"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { verifyRequest } from './verify';
|
|
2
|
+
|
|
3
|
+
// Mock the google-auth-library
|
|
4
|
+
jest.mock('google-auth-library', () => {
|
|
5
|
+
return {
|
|
6
|
+
OAuth2Client: jest.fn().mockImplementation(() => ({
|
|
7
|
+
verifyIdToken: jest.fn(({ idToken, audience }) => {
|
|
8
|
+
if (idToken === 'valid-token' && audience === 'audience') {
|
|
9
|
+
return {
|
|
10
|
+
getPayload: () => ({
|
|
11
|
+
email: 'allowed@example.com',
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
} else if (idToken === 'wrong-email-token' && audience === 'audience') {
|
|
15
|
+
return {
|
|
16
|
+
getPayload: () => ({
|
|
17
|
+
email: 'unauthorized@example.com',
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
throw new Error('Invalid token');
|
|
22
|
+
}),
|
|
23
|
+
})),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('verifyRequest', () => {
|
|
28
|
+
it('returns true for valid token and matching email', async () => {
|
|
29
|
+
const mockReq = {
|
|
30
|
+
headers: {
|
|
31
|
+
authorization: 'Bearer valid-token',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
const result = await verifyRequest(mockReq as any, {
|
|
37
|
+
allowedAudience: 'audience',
|
|
38
|
+
allowedServiceAccount: 'allowed@example.com',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(result).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns false for invalid token', async () => {
|
|
45
|
+
const mockReq = {
|
|
46
|
+
headers: {
|
|
47
|
+
authorization: 'Bearer invalid-token',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
const result = await verifyRequest(mockReq as any, {
|
|
53
|
+
allowedAudience: 'audience',
|
|
54
|
+
allowedServiceAccount: 'allowed@example.com',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(result).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns false for missing authorization header', async () => {
|
|
61
|
+
const mockReq = {
|
|
62
|
+
headers: {},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
+
const result = await verifyRequest(mockReq as any, {
|
|
67
|
+
allowedAudience: 'audience',
|
|
68
|
+
allowedServiceAccount: 'allowed@example.com',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(result).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns false for incorrect email', async () => {
|
|
75
|
+
const mockReq = {
|
|
76
|
+
headers: {
|
|
77
|
+
authorization: 'Bearer wrong-email-token',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
82
|
+
const result = await verifyRequest(mockReq as any, {
|
|
83
|
+
allowedAudience: 'audience',
|
|
84
|
+
allowedServiceAccount: 'allowed@example.com',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { OAuth2Client } from 'google-auth-library';
|
|
2
|
+
import { Request } from '@google-cloud/functions-framework';
|
|
3
|
+
|
|
4
|
+
interface VerifyOptions {
|
|
5
|
+
allowedAudience: string;
|
|
6
|
+
allowedServiceAccount: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function verifyRequest(
|
|
10
|
+
req: Request,
|
|
11
|
+
options: VerifyOptions
|
|
12
|
+
): Promise<boolean> {
|
|
13
|
+
const authHeader = req.headers['authorization'];
|
|
14
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const token = authHeader.split(' ')[1];
|
|
19
|
+
const client = new OAuth2Client();
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const ticket = await client.verifyIdToken({
|
|
23
|
+
idToken: token,
|
|
24
|
+
audience: options.allowedAudience,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const payload = ticket.getPayload();
|
|
28
|
+
return payload?.email === options.allowedServiceAccount;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"moduleResolution": "node",
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"types": ["node", "jest"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|