Skip to content

Commit 4f5ebc1

Browse files
authoredFeb 8, 2022
Use host-only cookies by default (aws-samples#184)
* Use @tsconfig/node14 * Pass cookiesettings to React App. Support host-only cookie in React App * In static site mode, default to sending the refresh token cookie ONLY when navigating to the refresh endpoint * Tweak and document cookie settings in React App * Refactored the code for better readability, and use host-only cookies * v2.1.0 * Tweak docs * Further simplfy error scenario's * Fixed spelling
1 parent 8b61156 commit 4f5ebc1

File tree

18 files changed

+467
-443
lines changed

18 files changed

+467
-443
lines changed
 

‎README.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This repo accompanies the [blog post](https://aws.amazon.com/blogs/networking-and-content-delivery/authorizationedge-using-cookies-protect-your-amazon-cloudfront-content-from-being-downloaded-by-unauthenticated-users/).
44

5-
In that blog post a solution is explained, that puts **Cognito** authentication in front of (S3) downloads from **CloudFront**, using **Lambda@Edge**. **JWT's** are transferred using **cookies** to make authorization transparent to clients.
5+
In that blog post a solution is explained, that puts **Cognito** authentication in front of (S3) downloads from **CloudFront**, using **Lambda@Edge**. **JWTs** are transferred using **cookies** to make authorization transparent to clients.
66

77
The sources in this repo implement that solution.
88

@@ -22,7 +22,7 @@ More deployment options below: [Deploying the solution](#deploying-the-solution)
2222

2323
### Alternative: use HTTP headers
2424

25-
This repo is the "sibling" of another repo here on aws-samples ([authorization-lambda-at-edge](https://github.com/aws-samples/authorization-lambda-at-edge)). The difference is that the solution in that repo uses http headers (not cookies) to transfer JWT's. While also a valid approach, the downside of it is that your Web App (SPA) needs to be altered to pass these headers, as browsers do not send these along automatically (which they do for cookies).
25+
This repo is the "sibling" of another repo here on aws-samples ([authorization-lambda-at-edge](https://github.com/aws-samples/authorization-lambda-at-edge)). The difference is that the solution in that repo uses http headers (not cookies) to transfer JWTs. While also a valid approach, the downside of it is that your Web App (SPA) needs to be altered to pass these headers, as browsers do not send these along automatically (which they do for cookies).
2626

2727
### Alternative: build an Auth@Edge solution yourself, using NPM library [cognito-at-edge](https://github.com/awslabs/cognito-at-edge)
2828

@@ -36,7 +36,7 @@ This repo contains (a.o.) the following files and directories:
3636

3737
Lambda@Edge functions in [src/lambda-edge](src/lambda-edge):
3838

39-
- [check-auth](src/lambda-edge/check-auth): Lambda@Edge function that checks each incoming request for valid JWT's in the request cookies
39+
- [check-auth](src/lambda-edge/check-auth): Lambda@Edge function that checks each incoming request for valid JWTs in the request cookies
4040
- [parse-auth](src/lambda-edge/parse-auth): Lambda@Edge function that handles the redirect from the Cognito hosted UI, after the user signed in
4141
- [refresh-auth](src/lambda-edge/refresh-auth): Lambda@Edge function that handles JWT refresh requests
4242
- [sign-out](src/lambda-edge/sign-out): Lambda@Edge function that handles sign-out
@@ -46,7 +46,7 @@ Lambda@Edge functions in [src/lambda-edge](src/lambda-edge):
4646
CloudFormation custom resources in [src/cfn-custom-resources](src/cfn-custom-resources):
4747

4848
- [us-east-1-lambda-stack](src/cfn-custom-resources/us-east-1-lambda-stack): Lambda function that implements a CloudFormation custom resource that makes sure the Lambda@Edge functions are deployed to us-east-1 (which is a CloudFront requirement, see below.)
49-
- [react-app](src/cfn-custom-resources/react-app): A sample React app that is protected by the solution. It uses AWS Amplify Framework to read the JWT's from cookies. The directory also contains a Lambda function that implements a CloudFormation custom resource to build the React app and upload it to S3
49+
- [react-app](src/cfn-custom-resources/react-app): A sample React app that is protected by the solution. It uses AWS Amplify Framework to read the JWTs from cookies. The directory also contains a Lambda function that implements a CloudFormation custom resource to build the React app and upload it to S3
5050
- [static-site](src/cfn-custom-resources/static-site): A sample static site (see [SPA mode or Static Site mode?](#spa-mode-or-static-site-mode)) that is protected by the solution. The directory also contains a Lambda function that implements a CloudFormation custom resource to upload the static site to S3
5151
- [user-pool-client](src/cfn-custom-resources/user-pool-client): Lambda function that implements a CloudFormation custom resource to update the User Pool client with OAuth config
5252
- [user-pool-domain](src/cfn-custom-resources/user-pool-domain): Lambda function that implements a CloudFormation custom resource to lookup the User Pool's domain, at which the Hosted UI is available
@@ -156,7 +156,7 @@ You can deploy this solution to any AWS region of your liking (that supports the
156156
The default deployment mode of this sample application is "SPA mode" - which entails some settings that make the deployment suitable for hosting a SPA such as a React/Angular/Vue app:
157157

158158
- The User Pool client does not use a client secret, as that would not make sense for JavaScript running in the browser
159-
- The cookies with JWT's are not "http only", so that they can be read and used by the SPA (e.g. to display the user name, or to refresh tokens)
159+
- The cookies with JWTs are not "http only", so that they can be read and used by the SPA (e.g. to display the user name, or to refresh tokens)
160160
- 404's (page not found on S3) will return index.html, to enable SPA-routing
161161

162162
If you do not want to deploy a SPA but rather a static site, then it is more secure to use a client secret and http-only cookies. Also, SPA routing is not needed then. To this end, upon deploying, set parameter `EnableSPAMode` to false (`--parameter-overrides EnableSPAMode="false"`). This will:
@@ -165,6 +165,7 @@ If you do not want to deploy a SPA but rather a static site, then it is more sec
165165
- Set cookies to be http only by default (unless you've provided other cookie settings explicitly)
166166
- Skip deployment of the sample React app. Rather a sample index.html is uploaded, that you can replace with your own pages
167167
- Skip setting up the custom error document mapping 404's to index.html (404's will instead show the plain S3 404 page)
168+
- Set the refresh token's path explicitly to the refresh path, `"/refreshauth"` instead of `"/"` (unless you've provided other cookie settings explicitly), and thus the refresh token will not be sent to other paths (more secure and more performant)
168169

169170
In case you're choosing Static Site mode, it might make sense to set parameter `RewritePathWithTrailingSlashToIndex` to `true` (`--parameter-overrides RewritePathWithTrailingSlashToIndex="true"`). This will append `index.html` to all paths that include a trailing slash, so that e.g. when the user goes to `/some/sub/dir/`, this is translated to `/some/sub/dir/index.html` in the request to S3.
170171

‎SERVERLESS-REPO.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This serverless application accompanies the [blog post](https://aws.amazon.com/blogs/networking-and-content-delivery/authorizationedge-using-cookies-protect-your-amazon-cloudfront-content-from-being-downloaded-by-unauthenticated-users/).
44

5-
In that blog post a solution is explained, that puts Cognito authentication in front of (S3) downloads from CloudFront, using Lambda@Edge. JWT's are transferred using cookies to make authorization transparent to clients.
5+
In that blog post a solution is explained, that puts Cognito authentication in front of (S3) downloads from CloudFront, using Lambda@Edge. JWTs are transferred using cookies to make authorization transparent to clients.
66

77
This application is an implementation of that solution. If you deploy it, this is what you get:
88

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

+1-1
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.0.19
35+
Default: 2.1.0
3636

3737
HttpHeaders:
3838
Type: String

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

+1-1
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.0.19",
22+
semanticVersion: "2.1.0",
2323
},
2424
parameters: {
2525
EmailAddress: "johndoe@example.com",

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

+1-1
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.0.19
15+
SemanticVersion: 2.1.0
1616
AlanTuring:
1717
Type: AWS::Cognito::UserPoolUser
1818
Properties:

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

+1-1
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.0.19
78+
SemanticVersion: 2.1.0
7979
Parameters:
8080
UserPoolArn: !GetAtt UserPool.Arn
8181
UserPoolClientId: !Ref UserPoolClient

‎package-lock.json

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
"keywords": [],
1717
"author": "",
1818
"devDependencies": {
19-
"aws-sdk": "^2.1066.0",
19+
"@tsconfig/node14": "^1.0.1",
2020
"@types/adm-zip": "^0.4.34",
2121
"@types/aws-lambda": "^8.10.92",
2222
"@types/cookie": "^0.4.1",
2323
"@types/fs-extra": "^9.0.13",
2424
"@types/node": "^17.0.14",
25+
"aws-sdk": "^2.1066.0",
2526
"html-loader": "^3.1.0",
2627
"prettier": "^2.5.1",
2728
"terser-webpack-plugin": "^5.3.1",

‎src/cfn-custom-resources/react-app/index.ts

+42-16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface Configuration {
2121
UserPoolArn: string;
2222
OAuthScopes: string;
2323
SignOutUrl: string;
24+
CookieSettings: string;
2425
}
2526

2627
async function buildSpa(config: Configuration) {
@@ -50,22 +51,47 @@ async function buildSpa(config: Configuration) {
5051

5152
const userPoolId = config.UserPoolArn.split("/")[1];
5253
const userPoolRegion = config.UserPoolArn.split(":")[3];
53-
54-
console.log(`Creating environment file ${temp_dir}/.env ...`);
55-
writeFileSync(
56-
`${temp_dir}/.env`,
57-
`SKIP_PREFLIGHT_CHECK=true
58-
REACT_APP_USER_POOL_ID=${userPoolId}
59-
REACT_APP_USER_POOL_REGION=${userPoolRegion}
60-
REACT_APP_USER_POOL_WEB_CLIENT_ID=${config.ClientId}
61-
REACT_APP_USER_POOL_AUTH_DOMAIN=${config.CognitoAuthDomain}
62-
REACT_APP_USER_POOL_REDIRECT_PATH_SIGN_IN=${config.RedirectPathSignIn}
63-
REACT_APP_USER_POOL_REDIRECT_PATH_SIGN_OUT=${config.RedirectPathSignOut}
64-
REACT_APP_SIGN_OUT_URL=${config.SignOutUrl}
65-
REACT_APP_USER_POOL_SCOPES=${config.OAuthScopes}
66-
INLINE_RUNTIME_CHUNK=false
67-
`
68-
);
54+
const cookieSettings = JSON.parse(config.CookieSettings).idToken as
55+
| string
56+
| null;
57+
let cookieDomain = cookieSettings
58+
?.split(";")
59+
.map((part) => {
60+
const match = part.match(/domain(\s*)=(\s*)(?<domain>.+)/i);
61+
return match?.groups?.domain;
62+
})
63+
.find((domain) => !!domain);
64+
if (!cookieDomain) {
65+
// Cookies without a domain, are called host-only cookies, and are perfectly normal.
66+
// However, AmplifyJS requires to be passed a value for domain, when using cookie storage.
67+
// We'll use " " as a trick to satisfy this check by AmplifyJS, and support host-only cookies.
68+
//
69+
// Note that you do not want to add an exact domain name to a cookie, if you want to have a host-only cookie,
70+
// because a cookie that's explicitly set for e.g. example.com is also readable by subdomain.example.com.
71+
// (In a cookie domain, example.com is treated the same as .example.com)
72+
// The ONLY way to get a host-only cookie, is by NOT including the domain attribute for the cookie at all.
73+
//
74+
// Note that if the cookie storage used in Amplify specifies a domain, this must match 1:1 the domain that
75+
// is used for the cookie by Auth@Edge, otherwise Amplify will have trouble setting that cookie
76+
// (and then e.g. signing out via Amplify no longer works, as that sets the cookies to expire them)
77+
cookieDomain = " ";
78+
}
79+
const reactEnv = `SKIP_PREFLIGHT_CHECK=true
80+
REACT_APP_USER_POOL_ID=${userPoolId}
81+
REACT_APP_USER_POOL_REGION=${userPoolRegion}
82+
REACT_APP_USER_POOL_WEB_CLIENT_ID=${config.ClientId}
83+
REACT_APP_USER_POOL_AUTH_DOMAIN=${config.CognitoAuthDomain}
84+
REACT_APP_USER_POOL_REDIRECT_PATH_SIGN_IN=${config.RedirectPathSignIn}
85+
REACT_APP_USER_POOL_REDIRECT_PATH_SIGN_OUT=${config.RedirectPathSignOut}
86+
REACT_APP_SIGN_OUT_URL=${config.SignOutUrl}
87+
REACT_APP_USER_POOL_SCOPES=${config.OAuthScopes}
88+
REACT_APP_COOKIE_DOMAIN="${cookieDomain}"
89+
INLINE_RUNTIME_CHUNK=false
90+
`;
91+
console.log("React env:\n", reactEnv);
92+
93+
console.log(`Creating React environment file ${temp_dir}/.env ...`);
94+
writeFileSync(`${temp_dir}/.env`, reactEnv);
6995

7096
console.log(`Installing dependencies to build React app in ${temp_dir} ...`);
7197
execSync("npm ci", {

‎src/cfn-custom-resources/react-app/react-app/src/App.js

+10-6
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ Amplify.configure({
1212
userPoolId: process.env.REACT_APP_USER_POOL_ID,
1313
userPoolWebClientId: process.env.REACT_APP_USER_POOL_WEB_CLIENT_ID,
1414
cookieStorage: {
15+
domain: process.env.REACT_APP_COOKIE_DOMAIN, // Use a single space " " for host-only cookies
16+
expires: null, // null means session cookies
1517
path: "/",
16-
expires: "",
17-
domain: window.location.hostname,
18-
secure: true,
18+
secure: true, // for developing on localhost over http: set to false
19+
sameSite: "lax",
1920
},
2021
oauth: {
2122
domain: process.env.REACT_APP_USER_POOL_AUTH_DOMAIN,
@@ -99,10 +100,13 @@ const App = () => {
99100
userPoolId: "${process.env.REACT_APP_USER_POOL_ID}",
100101
userPoolWebClientId: "${process.env.REACT_APP_USER_POOL_WEB_CLIENT_ID}",
101102
cookieStorage: {
103+
domain: "${
104+
process.env.REACT_APP_COOKIE_DOMAIN
105+
}", // Use a single space " " for host-only cookies
106+
expires: null, // null means session cookies
102107
path: "/",
103-
expires: "",
104-
domain: "${window.location.hostname}",
105-
secure: true,
108+
secure: true, // for developing on localhost over http: set to false
109+
sameSite: "lax"
106110
},
107111
oauth: {
108112
domain: "${process.env.REACT_APP_USER_POOL_AUTH_DOMAIN}",

0 commit comments

Comments
 (0)
Please sign in to comment.