Skip to content

Commit 1ba9e4e

Browse files
committed
empty
1 parent 991edca commit 1ba9e4e

File tree

7 files changed

+276
-0
lines changed

7 files changed

+276
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.build
2+
coverage
3+
node_modules
4+
dist
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Config } from 'jest';
2+
import { baseJestConfig } from 'nhs-notify-web-template-management-utils';
3+
4+
const config: Config = {
5+
...baseJestConfig,
6+
testEnvironment: 'node',
7+
};
8+
9+
export default config;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"dependencies": {
3+
"@aws-sdk/client-cognito-identity-provider": "^3.864.0",
4+
"jsonwebtoken": "^9.0.2",
5+
"jwks-rsa": "^3.2.0",
6+
"jwt-decode": "^4.0.0",
7+
"nhs-notify-web-template-management-utils": "^0.0.1",
8+
"zod": "^4.0.5"
9+
},
10+
"devDependencies": {
11+
"@swc/core": "^1.11.13",
12+
"@swc/jest": "^0.2.37",
13+
"@tsconfig/node20": "^20.1.5",
14+
"@types/aws-lambda": "^8.10.148",
15+
"@types/jest": "^29.5.14",
16+
"@types/jsonwebtoken": "^9.0.9",
17+
"esbuild": "^0.25.8",
18+
"jest": "^29.7.0",
19+
"jest-mock-extended": "^3.0.7",
20+
"typescript": "^5.8.2"
21+
},
22+
"name": "nhs-notify-templates-api-authorizer",
23+
"private": true,
24+
"scripts": {
25+
"lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts",
26+
"lint": "eslint .",
27+
"lint:fix": "eslint . --fix",
28+
"test:unit": "jest",
29+
"typecheck": "tsc --noEmit"
30+
},
31+
"version": "0.0.1"
32+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import type { APIGatewayRequestAuthorizerEvent, Context } from 'aws-lambda';
2+
import { mock } from 'jest-mock-extended';
3+
import { logger } from 'nhs-notify-web-template-management-utils/logger';
4+
import { handler } from '../index';
5+
import { LambdaCognitoAuthorizer } from 'nhs-notify-web-template-management-utils/lambda-cognito-authorizer';
6+
import { CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider';
7+
8+
const requestContext = {
9+
accountId: '000000000000',
10+
apiId: 'api-id',
11+
stage: 'stage',
12+
};
13+
14+
const methodArn = 'arn:aws:execute-api:eu-west-2:000000000000:api-id/stage/*';
15+
16+
jest.mock('nhs-notify-web-template-management-utils/logger');
17+
const mockLogger = jest.mocked(logger);
18+
19+
jest.mock('nhs-notify-web-template-management-utils/lambda-cognito-authorizer');
20+
const lambdaCognitoAuthorizer = mock<LambdaCognitoAuthorizer>();
21+
jest
22+
.mocked(LambdaCognitoAuthorizer)
23+
.mockImplementation(() => lambdaCognitoAuthorizer);
24+
25+
jest.mock('@aws-sdk/client-cognito-identity-provider');
26+
const cognitoClientMock = mock<CognitoIdentityProviderClient>();
27+
28+
jest
29+
.mocked(CognitoIdentityProviderClient)
30+
.mockImplementation(() => cognitoClientMock);
31+
32+
const allowPolicy = {
33+
principalId: 'api-caller',
34+
policyDocument: {
35+
Version: '2012-10-17',
36+
Statement: [
37+
{
38+
Action: 'execute-api:Invoke',
39+
Effect: 'Allow',
40+
Resource: methodArn,
41+
},
42+
],
43+
},
44+
context: {
45+
user: 'sub',
46+
},
47+
};
48+
49+
const denyPolicy = {
50+
principalId: 'api-caller',
51+
policyDocument: {
52+
Version: '2012-10-17',
53+
Statement: [
54+
{
55+
Action: 'execute-api:Invoke',
56+
Effect: 'Deny',
57+
Resource: methodArn,
58+
},
59+
],
60+
},
61+
};
62+
63+
const originalEnv = { ...process.env };
64+
65+
beforeEach(() => {
66+
jest.clearAllMocks();
67+
process.env.USER_POOL_ID = 'user-pool-id';
68+
process.env.USER_POOL_CLIENT_ID = 'user-pool-client-id';
69+
});
70+
71+
afterEach(() => {
72+
process.env = originalEnv;
73+
});
74+
75+
test('returns Allow policy on valid token', async () => {
76+
lambdaCognitoAuthorizer.authorize.mockResolvedValue({
77+
success: true,
78+
subject: 'sub',
79+
});
80+
81+
const res = await handler(
82+
mock<APIGatewayRequestAuthorizerEvent>({
83+
requestContext,
84+
headers: { Authorization: 'jwt' },
85+
type: 'REQUEST',
86+
}),
87+
mock<Context>(),
88+
jest.fn()
89+
);
90+
91+
expect(res).toEqual(allowPolicy);
92+
expect(mockLogger.warn).not.toHaveBeenCalled();
93+
expect(mockLogger.error).not.toHaveBeenCalled();
94+
95+
expect(lambdaCognitoAuthorizer.authorize).toHaveBeenCalledWith(
96+
'user-pool-id',
97+
'user-pool-client-id',
98+
'jwt'
99+
);
100+
});
101+
102+
test('returns Deny policy on lambda misconfiguration', async () => {
103+
process.env.USER_POOL_ID = '';
104+
105+
const res = await handler(
106+
mock<APIGatewayRequestAuthorizerEvent>({
107+
requestContext,
108+
headers: { Authorization: '123' },
109+
type: 'REQUEST',
110+
}),
111+
mock<Context>(),
112+
jest.fn()
113+
);
114+
115+
expect(res).toEqual(denyPolicy);
116+
expect(mockLogger.error).toHaveBeenCalledWith('Lambda misconfiguration');
117+
});
118+
119+
test('returns Deny policy if no Authorization token in header', async () => {
120+
const res = await handler(
121+
mock<APIGatewayRequestAuthorizerEvent>({
122+
requestContext,
123+
headers: { Authorization: undefined },
124+
type: 'REQUEST',
125+
}),
126+
mock<Context>(),
127+
jest.fn()
128+
);
129+
130+
expect(res).toEqual(denyPolicy);
131+
});
132+
133+
test('returns Deny policy when authorization fails', async () => {
134+
lambdaCognitoAuthorizer.authorize.mockResolvedValue({
135+
success: false,
136+
});
137+
138+
const res = await handler(
139+
mock<APIGatewayRequestAuthorizerEvent>({
140+
requestContext,
141+
headers: { Authorization: 'jwt' },
142+
type: 'REQUEST',
143+
}),
144+
mock<Context>(),
145+
jest.fn()
146+
);
147+
148+
expect(res).toEqual(denyPolicy);
149+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type {
2+
APIGatewayEventRequestContextWithAuthorizer,
3+
APIGatewayRequestAuthorizerHandler,
4+
} from 'aws-lambda';
5+
import { CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider';
6+
import { logger } from 'nhs-notify-web-template-management-utils/logger';
7+
import { LambdaCognitoAuthorizer } from 'nhs-notify-web-template-management-utils/lambda-cognito-authorizer';
8+
9+
const cognitoClient = new CognitoIdentityProviderClient({
10+
region: 'eu-west-2',
11+
});
12+
13+
const getEnvironmentVariable = (envName: string) => process.env[envName];
14+
15+
const generateMethodArn = (
16+
requestContext: APIGatewayEventRequestContextWithAuthorizer<undefined>
17+
) =>
18+
`arn:aws:execute-api:eu-west-2:${requestContext.accountId}:${requestContext.apiId}/${requestContext.stage}/*`;
19+
20+
const generatePolicy = (
21+
Resource: string,
22+
Effect: 'Allow' | 'Deny',
23+
context?: { user: string; clientId?: string }
24+
) => ({
25+
principalId: 'api-caller',
26+
policyDocument: {
27+
Version: '2012-10-17',
28+
Statement: [
29+
{
30+
Action: 'execute-api:Invoke',
31+
Effect,
32+
Resource,
33+
},
34+
],
35+
},
36+
context,
37+
});
38+
39+
export const handler: APIGatewayRequestAuthorizerHandler = async (event) => {
40+
const { headers, requestContext } = event;
41+
const methodArn = generateMethodArn(requestContext);
42+
43+
if (!headers?.Authorization) {
44+
return generatePolicy(methodArn, 'Deny');
45+
}
46+
47+
const userPoolId = getEnvironmentVariable('USER_POOL_ID');
48+
const userPoolClientId = getEnvironmentVariable('USER_POOL_CLIENT_ID');
49+
const authorizationToken = headers.Authorization;
50+
51+
if (!userPoolId || !userPoolClientId) {
52+
logger.error('Lambda misconfiguration');
53+
return generatePolicy(methodArn, 'Deny');
54+
}
55+
56+
const lambdaCognitoAuthorizer = new LambdaCognitoAuthorizer(
57+
cognitoClient,
58+
logger
59+
);
60+
61+
const authResult = await lambdaCognitoAuthorizer.authorize(
62+
userPoolId,
63+
userPoolClientId,
64+
authorizationToken
65+
);
66+
67+
if (authResult.success) {
68+
return generatePolicy(methodArn, 'Allow', {
69+
user: authResult.subject,
70+
clientId: authResult.clientId,
71+
});
72+
}
73+
74+
return generatePolicy(methodArn, 'Deny');
75+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "@tsconfig/node20/tsconfig.json",
3+
"include": [
4+
"src/**/*"
5+
]
6+
}

0 commit comments

Comments
 (0)