Skip to content

Commit 0bd6b87

Browse files
authored
Preload JWKS (#213)
1 parent 607074a commit 0bd6b87

File tree

10 files changed

+216
-12
lines changed

10 files changed

+216
-12
lines changed

example-serverless-app-reuse/reuse-auth-only.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Parameters:
3232
SemanticVersion:
3333
Type: String
3434
Description: Semantic version of the back end
35-
Default: 2.1.2
35+
Default: 2.1.3
3636

3737
HttpHeaders:
3838
Type: String

example-serverless-app-reuse/reuse-complete-cdk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const authAtEdge = new sam.CfnApplication(stack, "AuthorizationAtEdge", {
1919
location: {
2020
applicationId:
2121
"arn:aws:serverlessrepo:us-east-1:520945424137:applications/cloudfront-authorization-at-edge",
22-
semanticVersion: "2.1.2",
22+
semanticVersion: "2.1.3",
2323
},
2424
parameters: {
2525
EmailAddress: "[email protected]",

example-serverless-app-reuse/reuse-complete.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Resources:
1212
Properties:
1313
Location:
1414
ApplicationId: arn:aws:serverlessrepo:us-east-1:520945424137:applications/cloudfront-authorization-at-edge
15-
SemanticVersion: 2.1.2
15+
SemanticVersion: 2.1.3
1616
AlanTuring:
1717
Type: AWS::Cognito::UserPoolUser
1818
Properties:

example-serverless-app-reuse/reuse-with-existing-user-pool.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Resources:
7575
Properties:
7676
Location:
7777
ApplicationId: arn:aws:serverlessrepo:us-east-1:520945424137:applications/cloudfront-authorization-at-edge
78-
SemanticVersion: 2.1.2
78+
SemanticVersion: 2.1.3
7979
Parameters:
8080
UserPoolArn: !GetAtt UserPool.Arn
8181
UserPoolClientId: !Ref UserPoolClient
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
4+
import { request } from "https";
5+
6+
export enum Status {
7+
"SUCCESS" = "SUCCESS",
8+
"FAILED" = "FAILED",
9+
}
10+
11+
export async function sendCfnResponse(props: {
12+
event: {
13+
StackId: string;
14+
RequestId: string;
15+
LogicalResourceId: string;
16+
ResponseURL: string;
17+
};
18+
status: Status;
19+
reason?: string;
20+
data?: {
21+
[key: string]: string;
22+
};
23+
physicalResourceId?: string;
24+
}) {
25+
const response = {
26+
Status: props.status,
27+
Reason: props.reason?.toString() || "See CloudWatch logs",
28+
PhysicalResourceId: props.physicalResourceId || "no-explicit-id",
29+
StackId: props.event.StackId,
30+
RequestId: props.event.RequestId,
31+
LogicalResourceId: props.event.LogicalResourceId,
32+
Data: props.data || {},
33+
};
34+
35+
await new Promise<void>((resolve, reject) => {
36+
const options = {
37+
method: "PUT",
38+
headers: { "content-type": "" },
39+
};
40+
request(props.event.ResponseURL, options)
41+
.on("error", (err) => {
42+
reject(err);
43+
})
44+
.end(JSON.stringify(response), "utf8", resolve);
45+
});
46+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
4+
import { request } from "https";
5+
import { Writable, pipeline } from "stream";
6+
7+
export async function fetch(uri: string) {
8+
return new Promise<Buffer>((resolve, reject) => {
9+
const req = request(uri, (res) =>
10+
pipeline([res, collectBuffer(resolve)], done)
11+
);
12+
13+
function done(error?: Error | null) {
14+
if (!error) return;
15+
req.destroy(error);
16+
reject(error);
17+
}
18+
19+
req.on("error", done);
20+
21+
req.end();
22+
});
23+
}
24+
25+
const collectBuffer = (callback: (collectedBuffer: Buffer) => void) => {
26+
const chunks = [] as Buffer[];
27+
return new Writable({
28+
write: (chunk, _encoding, done) => {
29+
try {
30+
chunks.push(chunk);
31+
done();
32+
} catch (err) {
33+
done(err as Error);
34+
}
35+
},
36+
final: (done) => {
37+
try {
38+
callback(Buffer.concat(chunks));
39+
done();
40+
} catch (err) {
41+
done(err as Error);
42+
}
43+
},
44+
});
45+
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
4+
import {
5+
CloudFormationCustomResourceHandler,
6+
CloudFormationCustomResourceDeleteEvent,
7+
CloudFormationCustomResourceUpdateEvent,
8+
} from "aws-lambda";
9+
import { sendCfnResponse, Status } from "./cfn-response";
10+
import { fetch } from "./https";
11+
12+
async function fetchJwks(
13+
action: "Create" | "Update" | "Delete",
14+
userPoolArn: string,
15+
physicalResourceId?: string
16+
) {
17+
if (action === "Delete") {
18+
// Deletes aren't executed
19+
return { physicalResourceId: physicalResourceId!, Data: {} };
20+
}
21+
console.log(`Fetching JWKS for ${userPoolArn}`);
22+
23+
const match = userPoolArn.match(
24+
new RegExp("userpool/(?<region>.+)_(?<userPoolId>.+)$")
25+
);
26+
if (!match?.groups) {
27+
throw new Error("Failed to parse User Pool ARN");
28+
}
29+
const url = `https://cognito-idp.${match.groups.region}.amazonaws.com/${match.groups.region}_${match.groups.userPoolId}/.well-known/jwks.json`;
30+
31+
console.log(`Fetching JWKS from ${url}`);
32+
const jwks = (await fetch(url)).toString();
33+
console.log(`Fetched JWKS: ${jwks}`);
34+
35+
return {
36+
physicalResourceId: userPoolArn,
37+
Data: { Jwks: jwks },
38+
};
39+
}
40+
41+
export const handler: CloudFormationCustomResourceHandler = async (event) => {
42+
const { ResourceProperties, RequestType } = event;
43+
44+
const { PhysicalResourceId } = event as
45+
| CloudFormationCustomResourceDeleteEvent
46+
| CloudFormationCustomResourceUpdateEvent;
47+
48+
const { UserPoolArn } = ResourceProperties;
49+
50+
let status = Status.SUCCESS;
51+
let physicalResourceId: string | undefined;
52+
let data: { [key: string]: any } | undefined;
53+
let reason: string | undefined;
54+
try {
55+
({ physicalResourceId, Data: data } = await fetchJwks(
56+
RequestType,
57+
UserPoolArn,
58+
PhysicalResourceId
59+
));
60+
} catch (err) {
61+
console.error(err);
62+
status = Status.FAILED;
63+
reason = `${err}`;
64+
}
65+
await sendCfnResponse({
66+
event,
67+
status,
68+
data,
69+
physicalResourceId,
70+
reason,
71+
});
72+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "fetch-jwks",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"keywords": [],
10+
"author": ""
11+
}

src/lambda-edge/shared/shared.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { fetch } from "./https";
1010
import { Agent, RequestOptions } from "https";
1111
import html from "./error-page/template.html";
1212
import { CognitoJwtVerifier } from "aws-jwt-verify";
13+
import { Jwks } from "aws-jwt-verify/jwk";
1314
export {
1415
CognitoJwtInvalidGroupError,
1516
JwtExpiredError,
@@ -75,6 +76,7 @@ interface ConfigFromDiskWithHeaders extends ConfigFromDisk {
7576

7677
interface ConfigFromDiskComplete extends ConfigFromDiskWithHeaders {
7778
userPoolArn: string;
79+
jwks: Jwks;
7880
clientId: string;
7981
oauthScopes: string[];
8082
cognitoAuthDomain: string;
@@ -228,14 +230,21 @@ export function getCompleteConfig(): CompleteConfig {
228230
export function getConfigWithJwtVerifier() {
229231
const config = getCompleteConfig();
230232
const userPoolId = config.userPoolArn.split("/")[1];
233+
const jwtVerifier = CognitoJwtVerifier.create({
234+
userPoolId,
235+
clientId: config.clientId,
236+
tokenUse: "id",
237+
groups: config.requiredGroup || undefined,
238+
});
239+
240+
// Optimization: load the JWKS (as it was at deploy-time) into the cache.
241+
// Then, the JWKS does not need to be fetched at runtime,
242+
// as long as only JWTs come by with a kid that is in this cached JWKS:
243+
jwtVerifier.cacheJwks(config.jwks);
244+
231245
return {
232246
...config,
233-
jwtVerifier: CognitoJwtVerifier.create({
234-
userPoolId,
235-
clientId: config.clientId,
236-
tokenUse: "id",
237-
groups: config.requiredGroup || undefined,
238-
}),
247+
jwtVerifier,
239248
};
240249
}
241250

template.yaml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Metadata:
2727
"amplify",
2828
]
2929
HomePageUrl: https://github.com/aws-samples/cloudfront-authorization-at-edge
30-
SemanticVersion: 2.1.2
30+
SemanticVersion: 2.1.3
3131
SourceCodeUrl: https://github.com/aws-samples/cloudfront-authorization-at-edge
3232

3333
Parameters:
@@ -150,7 +150,7 @@ Parameters:
150150
Version:
151151
Type: String
152152
Description: "Changing this parameter after initial deployment forces redeployment of Lambda@Edge functions"
153-
Default: "2.1.2"
153+
Default: "2.1.3"
154154
LogLevel:
155155
Type: String
156156
Description: "Use for development: setting to a value other than none turns on logging at that level. Warning! This will log sensitive data, use for development only"
@@ -444,6 +444,7 @@ Resources:
444444
- lambda:UpdateFunctionCode
445445
- lambda:UpdateFunctionConfiguration
446446
- lambda:TagResource
447+
- lambda:ListTags
447448
Resource:
448449
- !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-CheckAuthHandler-*"
449450
- !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-ParseAuthHandler-*"
@@ -754,6 +755,22 @@ Resources:
754755
- !GetAtt UserPool.Arn
755756
- !Ref UserPoolArn
756757

758+
CognitoJwksFetchHandler:
759+
Type: AWS::Serverless::Function
760+
Properties:
761+
CodeUri: src/cfn-custom-resources/fetch-jwks/
762+
Handler: index.handler
763+
764+
FetchedJwks:
765+
Type: Custom::FetchedJwks
766+
Properties:
767+
ServiceToken: !GetAtt CognitoJwksFetchHandler.Arn
768+
Version: !Ref Version
769+
UserPoolArn: !If
770+
- CreateUserPoolAndClient
771+
- !GetAtt UserPool.Arn
772+
- !Ref UserPoolArn
773+
757774
StaticSite:
758775
Type: Custom::StaticSite
759776
Condition: CreateSampleStaticSite
@@ -849,6 +866,7 @@ Resources:
849866
- >
850867
{
851868
"userPoolArn": "${UserPoolArn}",
869+
"jwks": ${FetchedJwks.Jwks},
852870
"clientId": "${ClientId}",
853871
"clientSecret": "${ClientSecret}",
854872
"oauthScopes": ${OAuthScopesJsonArray},
@@ -911,6 +929,7 @@ Resources:
911929
- >
912930
{
913931
"userPoolArn": "${UserPoolArn}",
932+
"jwks": ${FetchedJwks.Jwks},
914933
"clientId": "${ClientId}",
915934
"clientSecret": "${ClientSecret}",
916935
"oauthScopes": ${OAuthScopesJsonArray},
@@ -1005,6 +1024,7 @@ Resources:
10051024
- >
10061025
{
10071026
"userPoolArn": "${UserPoolArn}",
1027+
"jwks": ${FetchedJwks.Jwks},
10081028
"clientId": "${ClientId}",
10091029
"clientSecret": "${ClientSecret}",
10101030
"oauthScopes": ${OAuthScopesJsonArray},
@@ -1067,6 +1087,7 @@ Resources:
10671087
- >
10681088
{
10691089
"userPoolArn": "${UserPoolArn}",
1090+
"jwks": ${FetchedJwks.Jwks},
10701091
"clientId": "${ClientId}",
10711092
"clientSecret": "${ClientSecret}",
10721093
"oauthScopes": ${OAuthScopesJsonArray},

0 commit comments

Comments
 (0)