├── .npmignore ├── .gitignore ├── src ├── oAuth2Authorizer │ ├── package.json │ ├── package-lock.json │ └── index.js └── oAuth2Callback │ ├── package.json │ ├── index.js │ └── package-lock.json ├── jest.config.js ├── CODE_OF_CONDUCT.md ├── test └── api-gw-http-only-cookie-auth.test.ts ├── tsconfig.json ├── package.json ├── LICENSE ├── bin └── api-gw-http-only-cookie-auth.ts ├── cdk.json ├── CONTRIBUTING.md ├── lib └── api-gw-http-only-cookie-auth-stack.ts └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !jest.config.js 2 | *.d.ts 3 | node_modules 4 | 5 | # CDK asset staging directory 6 | .cdk.staging 7 | cdk.out 8 | -------------------------------------------------------------------------------- /src/oAuth2Authorizer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oAuth2Authorizer", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "aws-jwt-verify": "^3.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/oAuth2Callback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oAuth2Callback", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "axios": "^1.12.0", 6 | "qs": "^6.11.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /src/oAuth2Authorizer/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oAuth2Authorizer", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "oAuth2Authorizer", 9 | "version": "0.0.1", 10 | "dependencies": { 11 | "aws-jwt-verify": "^3.1.0" 12 | } 13 | }, 14 | "node_modules/aws-jwt-verify": { 15 | "version": "3.4.0", 16 | "resolved": "https://registry.npmjs.org/aws-jwt-verify/-/aws-jwt-verify-3.4.0.tgz", 17 | "integrity": "sha512-GmcJZ3jd3adDvVpiAY5S223z1Bz+fOfqHpOdRnDJFeS5Kt5ERggdUPa3PmHdsO/tYaqrkl3Tm15frLa84sQ8Mw==", 18 | "engines": { 19 | "node": ">=14.0.0" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/api-gw-http-only-cookie-auth.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as ApiGwHttpOnlyCookieAuth from '../lib/api-gw-http-only-cookie-auth-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/api-gw-http-only-cookie-auth-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new ApiGwHttpOnlyCookieAuth.ApiGwHttpOnlyCookieAuthStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-gw-http-only-cookie-auth", 3 | "version": "0.1.0", 4 | "bin": { 5 | "api-gw-http-only-cookie-auth": "bin/api-gw-http-only-cookie-auth.js" 6 | }, 7 | "scripts": { 8 | "predeploy": "npm i && cd ./src/oAuth2Authorizer && npm i && cd ../oAuth2Callback && npm i", 9 | "deploy": "cdk bootstrap && cdk deploy" 10 | }, 11 | "dependencies": { 12 | "aws-cdk-lib": "^2.100.0", 13 | "constructs": "^10.0.0", 14 | "source-map-support": "^0.5.21" 15 | }, 16 | "devDependencies": { 17 | "@types/jest": "^29.5.0", 18 | "@types/node": "^20.0.0", 19 | "@types/prettier": "^3.0.0", 20 | "aws-cdk": "^2.100.0", 21 | "jest": "^29.5.0", 22 | "ts-jest": "^29.1.0", 23 | "ts-node": "^10.9.1", 24 | "typescript": "~5.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /bin/api-gw-http-only-cookie-auth.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { ApiGwHttpOnlyCookieAuthStack } from '../lib/api-gw-http-only-cookie-auth-stack'; 5 | 6 | const app = new cdk.App(); 7 | new ApiGwHttpOnlyCookieAuthStack(app, 'ApiGwHttpOnlyCookieAuthStack', { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /src/oAuth2Callback/index.js: -------------------------------------------------------------------------------- 1 | const qs = require("qs"); 2 | const axios = require("axios").default; 3 | 4 | exports.handler = async function (event) { 5 | const code = event.queryStringParameters?.code; 6 | 7 | if (code == null) { 8 | return { 9 | statusCode: 400, 10 | body: "code query param required", 11 | }; 12 | } 13 | 14 | const data = { 15 | grant_type: "authorization_code", 16 | client_id: process.env.CLIENT_ID, 17 | // The redirect has already happened, but you still need to pass the URI for validation, so a valid oAuth2 access token can be generated 18 | redirect_uri: encodeURI(process.env.REDIRECT_URI), 19 | code: code, 20 | }; 21 | 22 | // Every Cognito instance has its own token endpoints. For more information check the documentation: https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html 23 | const res = await axios.post(process.env.TOKEN_ENDPOINT, qs.stringify(data), { 24 | headers: { 25 | "Content-Type": "application/x-www-form-urlencoded", 26 | }, 27 | }); 28 | 29 | return { 30 | statusCode: 302, 31 | // These headers are returned as part of the response to the browser. 32 | headers: { 33 | // The Location header tells the browser it should redirect to the root of the URL 34 | Location: "/", 35 | // The Set-Cookie header tells the browser to persist the access token in the cookie store 36 | "Set-Cookie": `accessToken=${res.data.access_token}; Secure; HttpOnly; SameSite=Lax; Path=/`, 37 | }, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/api-gw-http-only-cookie-auth.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/core:enablePartitionLiterals": true, 29 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 30 | "@aws-cdk/aws-iam:minimizePolicies": true, 31 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 32 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 33 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 34 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 35 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 36 | "@aws-cdk/core:stackRelativeExports": true, 37 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 38 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 39 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 40 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/oAuth2Authorizer/index.js: -------------------------------------------------------------------------------- 1 | const { CognitoJwtVerifier } = require("aws-jwt-verify"); 2 | 3 | function getAccessTokenFromCookies(cookiesArray) { 4 | // cookieStr contains the full cookie definition string: "accessToken=abc" 5 | for (const cookieStr of cookiesArray) { 6 | const cookieArr = cookieStr.split("accessToken="); 7 | // After splitting you should get an array with 2 entries: ["", "abc"] - Or only 1 entry in case it was a different cookie string: ["test=test"] 8 | if (cookieArr[1] != null) { 9 | return cookieArr[1]; // Returning only the value of the access token without cookie name 10 | } 11 | } 12 | 13 | return null; 14 | } 15 | 16 | // Create the verifier outside the Lambda handler (= during cold start), 17 | // so the cache can be reused for subsequent invocations. Then, only during the 18 | // first invocation, will the verifier actually need to fetch the JWKS. 19 | const verifier = CognitoJwtVerifier.create({ 20 | userPoolId: process.env.USER_POOL_ID, 21 | tokenUse: "access", 22 | clientId: process.env.CLIENT_ID, 23 | }); 24 | 25 | exports.handler = async (event) => { 26 | if (event.cookies == null) { 27 | console.log("No cookies found"); 28 | 29 | return { 30 | isAuthorized: false, 31 | }; 32 | } 33 | 34 | // Cookies array looks something like this: ["accessToken=abc", "otherCookie=Random Value"] 35 | const accessToken = getAccessTokenFromCookies(event.cookies); 36 | 37 | if (accessToken == null) { 38 | console.log("Access token not found in cookies"); 39 | return { 40 | isAuthorized: false, 41 | }; 42 | } 43 | 44 | try { 45 | await verifier.verify(accessToken); 46 | return { 47 | isAuthorized: true, 48 | }; 49 | } catch (e) { 50 | console.error(e); 51 | return { 52 | isAuthorized: false, 53 | }; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /lib/api-gw-http-only-cookie-auth-stack.ts: -------------------------------------------------------------------------------- 1 | import { HttpApi, HttpMethod } from "aws-cdk-lib/aws-apigatewayv2"; 2 | import { HttpLambdaAuthorizer, HttpLambdaResponseType } from "aws-cdk-lib/aws-apigatewayv2-authorizers"; 3 | import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations"; 4 | import { CfnOutput, Duration, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; 5 | import { UserPool } from "aws-cdk-lib/aws-cognito"; 6 | import { Architecture, Code, Function, InlineCode, Runtime } from "aws-cdk-lib/aws-lambda"; 7 | import { Construct } from "constructs"; 8 | import { randomUUID } from "crypto"; 9 | import * as path from "path"; 10 | 11 | export class ApiGwHttpOnlyCookieAuthStack extends Stack { 12 | constructor(scope: Construct, id: string, props?: StackProps) { 13 | super(scope, id, props); 14 | 15 | /** 16 | * API Gateway 17 | */ 18 | const httpApi = new HttpApi(this, "HttpOnlyCookieHttpApi"); 19 | 20 | const httpApiDomainName = `https://${httpApi.apiId}.execute-api.${Stack.of(this).region}.amazonaws.com`; 21 | 22 | new CfnOutput(this, "App URL", { 23 | value: httpApiDomainName, 24 | description: "The base URL of the endpoint", 25 | }); 26 | 27 | /** 28 | * Cognito 29 | */ 30 | const userPool = new UserPool(this, "HttpOnlyCookieUserPool", { 31 | userPoolName: "HttpOnlyCookie", 32 | removalPolicy: RemovalPolicy.DESTROY, 33 | }); 34 | 35 | const callbackUrl = httpApiDomainName + "/oauth2/callback"; 36 | const userPoolClient = userPool.addClient("MyAppClient", { 37 | oAuth: { 38 | callbackUrls: [callbackUrl], 39 | }, 40 | }); 41 | 42 | const domain = userPool.addDomain("UserPoolDomain", { 43 | cognitoDomain: { 44 | domainPrefix: "http-only-cookie" + "-" + randomUUID(), 45 | }, 46 | }); 47 | const signInUrl = domain.signInUrl(userPoolClient, { 48 | redirectUri: callbackUrl, // must be a URL configured under 'callbackUrls' with the client 49 | }); 50 | new CfnOutput(this, "Sign-in URL", { 51 | value: signInUrl, 52 | description: "Use this URL to sign-in to your app", 53 | }); 54 | 55 | /** 56 | * Lambda getProtectedResource 57 | */ 58 | const getProtectedResourceFunction = new Function(this, "getProtectedResource", { 59 | functionName: "getProtectedResource", 60 | handler: "index.handler", 61 | runtime: Runtime.NODEJS_20_X, 62 | architecture: Architecture.ARM_64, 63 | code: new InlineCode(` 64 | exports.handler = async () => { 65 | return { 66 | statusCode: 200, 67 | body: JSON.stringify("Hello from Lambda!"), 68 | }; 69 | };`), 70 | reservedConcurrentExecutions: 1, 71 | }); 72 | 73 | /** 74 | * Lambda oAuth2Callback 75 | */ 76 | const oAuth2CallbackFunction = new Function(this, "oAuth2Callback", { 77 | functionName: "oAuth2Callback", 78 | runtime: Runtime.NODEJS_20_X, 79 | architecture: Architecture.ARM_64, 80 | handler: "index.handler", 81 | code: Code.fromAsset(path.join(__dirname, "../src/oAuth2Callback")), 82 | environment: { 83 | TOKEN_ENDPOINT: `${domain.baseUrl()}/oauth2/token`, 84 | CLIENT_ID: userPoolClient.userPoolClientId, 85 | REDIRECT_URI: callbackUrl, 86 | }, 87 | reservedConcurrentExecutions: 1, 88 | }); 89 | 90 | /** 91 | * Lambda oAuth2Authorizer 92 | */ 93 | const oAuth2AuthorizerFunction = new Function(this, "oAuth2Authorizer", { 94 | functionName: "oAuth2Authorizer", 95 | runtime: Runtime.NODEJS_20_X, 96 | architecture: Architecture.ARM_64, 97 | handler: "index.handler", 98 | code: Code.fromAsset(path.join(__dirname, "../src/oAuth2Authorizer")), 99 | environment: { 100 | USER_POOL_ID: userPool.userPoolId, 101 | CLIENT_ID: userPoolClient.userPoolClientId, 102 | }, 103 | reservedConcurrentExecutions: 1, 104 | }); 105 | 106 | /** 107 | * API Gateway routes 108 | */ 109 | httpApi.addRoutes({ 110 | path: "/", 111 | methods: [HttpMethod.GET], 112 | integration: new HttpLambdaIntegration("getProtectedResource", getProtectedResourceFunction), 113 | authorizer: new HttpLambdaAuthorizer("oAuth2Authorizer", oAuth2AuthorizerFunction, { 114 | responseTypes: [HttpLambdaResponseType.SIMPLE], 115 | identitySource: [], 116 | resultsCacheTtl: Duration.seconds(0), 117 | }), 118 | }); 119 | httpApi.addRoutes({ 120 | path: "/oauth2/callback", 121 | methods: [HttpMethod.GET], 122 | integration: new HttpLambdaIntegration("oAuth2ServerIntegration", oAuth2CallbackFunction), 123 | }); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to implement HttpOnly cookie authentication in Amazon API Gateway 2 | 3 | This repository contains accompanying source code for the AWS Blog post, [How to implement HttpOnly cookie authentication in Amazon API Gateway](https://aws.amazon.com/blogs/security/reduce-risk-by-implementing-httponly-cookie-authentication-in-amazon-api-gateway/). Read the blog for more information on architecture & concept. This repository only contains automated tools to easily deploy the solution. 4 | 5 | ## Pre-requisites 6 | 7 | - You should have level 200-300 know-how on the [OAuth2 protocol](https://oauth.net/2/). 8 | - You should have the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) installed & [configured](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html). 9 | - Download & install [NodeJS](https://nodejs.org/en/download/) 10 | 11 | ## Deployment 12 | 13 | You are going to use [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) to deploy the solution with infrastructure as code. Follow the following steps to deploy the solution. 14 | 15 | Open a terminal & clone this repository: 16 | ``` 17 | $ git clone https://github.com/aws-samples/api-gw-http-only-cookie-auth.git 18 | ``` 19 | 20 | Go into the cloned folder: 21 | ``` 22 | $ cd ./api-gw-http-only-cookie-auth 23 | ``` 24 | 25 | Run the deployment script and follow the on-screen quesetions: 26 | ``` 27 | $ npm run deploy 28 | ``` 29 | 30 | _...Wait until the script finishes & all resources are deployed..._ 31 | 32 | You should get the relevant URL's from the output. Note down those URL's. The output looks something like this: 33 | ``` 34 | Outputs: 35 | ApiGwHttpOnlyCookieAuthStack.AppURL = https://1234567890.execute-api.eu-central-1.amazonaws.com 36 | ApiGwHttpOnlyCookieAuthStack.SigninURL = https://http-only-cookie-1234567890-abcd-efgh-ijkl-1234567890.auth.eu-central-1.amazoncognito.com/login?client_id=1234567890&response_type=code&redirect_uri=https://1234567890.execute-api.eu-central-1.amazonaws.com/oauth2/callback 37 | ``` 38 | 39 | Create test user: 40 | 1. Navigate to the [Amazon Cognito](https://console.aws.amazon.com/cognito/home) console and choose **HttpOnlyCookie**. 41 | 1. Under **Users** choose **Create user**. 42 | 1. For **User name** enter any user name you like. 43 | 1. For **Email address** enter any email you like. 44 | > Note: For this tutorial you don’t need to send out actual emails. That’s why the email does not need to actually exist. 45 | 5. Choose **Mark email address as verified**. 46 | 6. For **password** enter a password you can remember (or even better: use a password generator). 47 | 7. Remember the **email** and **password** for later use. 48 | 8. Choose **Create user**. 49 | 50 | Create user 51 | 52 | That's it 🎉 53 | 54 | ## Testing 55 | 56 | Now that you have all components in place, you can test your OAuth2 Flow: 57 | 1. Paste in your `SigninURL` (from above) in a browser. 58 | 2. In the newly opened browser tab, open [your developer tools](https://balsamiq.com/support/faqs/browserconsole/), so you can inspect the network requests. 59 | 3. Login with your Email & password. 60 | 4. Now you’ll see your `Hello from Lambda` message. 61 | 62 | How do you know that the cookie was accurately set? 63 | Check your browser network tab in the browser developer settings. You’ll see the `/oauth2/callback` request which looks like this: 64 | 65 | Callback network request 66 | 67 | As you can see the response headers include a `Set-Cookie` header as is specified in the `oAuth2Callback` Lambda function. This ensures that your OAuth2 access token is set as a HttpOnly cookie in the browser & access is prohibited from any client-side code. 68 | 69 | Also, you can inspect the cookie in the browser cookie storage: 70 | 71 | Cookie storage 72 | 73 | > Note: In case you want to retry the authentication: Navigate in your browser to the base URL of Amazon Cognito & clear all site data in the browser developer tools. Do the same for your API Gateway website. Now you can restart the test with a clean state. 74 | 75 | When inspecting the HTTP request your browser makes in the developer tools you can see why authentication works. The HttpOnly cookie is automatically attached to every request: 76 | 77 | Browser requests include HttpOnly cookies 78 | 79 | To verify that the `oAuth2Authorizer` Lambda function works correctly you need to paste the `AppURL` (from above) in an incognito window. Incognito windows do not share the cookie store with your browser session. That’s why you see a `{"message":"Forbidden"}` error message with HTTP response code `403 – Forbidden`. 80 | 81 | You did it 🎉 82 | 83 | ## Tear down 84 | 85 | Don’t forget to delete all unwanted resources to avoid costs. Simply run at the root of your project: 86 | ``` 87 | npx cdk destroy 88 | ``` 89 | 90 | ## Security 91 | 92 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 93 | 94 | ## License 95 | 96 | This library is licensed under the MIT-0 License. See the LICENSE file. 97 | -------------------------------------------------------------------------------- /src/oAuth2Callback/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oAuth2Callback", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "oAuth2Callback", 9 | "version": "0.0.1", 10 | "dependencies": { 11 | "axios": "^1.12.0", 12 | "qs": "^6.11.0" 13 | } 14 | }, 15 | "node_modules/asynckit": { 16 | "version": "0.4.0", 17 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 18 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 19 | }, 20 | "node_modules/axios": { 21 | "version": "1.12.0", 22 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", 23 | "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", 24 | "license": "MIT", 25 | "dependencies": { 26 | "follow-redirects": "^1.15.6", 27 | "form-data": "^4.0.4", 28 | "proxy-from-env": "^1.1.0" 29 | } 30 | }, 31 | "node_modules/call-bind": { 32 | "version": "1.0.2", 33 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 34 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 35 | "dependencies": { 36 | "function-bind": "^1.1.1", 37 | "get-intrinsic": "^1.0.2" 38 | }, 39 | "funding": { 40 | "url": "https://github.com/sponsors/ljharb" 41 | } 42 | }, 43 | "node_modules/call-bind-apply-helpers": { 44 | "version": "1.0.2", 45 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 46 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 47 | "license": "MIT", 48 | "dependencies": { 49 | "es-errors": "^1.3.0", 50 | "function-bind": "^1.1.2" 51 | }, 52 | "engines": { 53 | "node": ">= 0.4" 54 | } 55 | }, 56 | "node_modules/combined-stream": { 57 | "version": "1.0.8", 58 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 59 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 60 | "dependencies": { 61 | "delayed-stream": "~1.0.0" 62 | }, 63 | "engines": { 64 | "node": ">= 0.8" 65 | } 66 | }, 67 | "node_modules/delayed-stream": { 68 | "version": "1.0.0", 69 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 70 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 71 | "engines": { 72 | "node": ">=0.4.0" 73 | } 74 | }, 75 | "node_modules/dunder-proto": { 76 | "version": "1.0.1", 77 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 78 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 79 | "license": "MIT", 80 | "dependencies": { 81 | "call-bind-apply-helpers": "^1.0.1", 82 | "es-errors": "^1.3.0", 83 | "gopd": "^1.2.0" 84 | }, 85 | "engines": { 86 | "node": ">= 0.4" 87 | } 88 | }, 89 | "node_modules/es-define-property": { 90 | "version": "1.0.1", 91 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 92 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 93 | "license": "MIT", 94 | "engines": { 95 | "node": ">= 0.4" 96 | } 97 | }, 98 | "node_modules/es-errors": { 99 | "version": "1.3.0", 100 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 101 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 102 | "license": "MIT", 103 | "engines": { 104 | "node": ">= 0.4" 105 | } 106 | }, 107 | "node_modules/es-object-atoms": { 108 | "version": "1.1.1", 109 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 110 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 111 | "license": "MIT", 112 | "dependencies": { 113 | "es-errors": "^1.3.0" 114 | }, 115 | "engines": { 116 | "node": ">= 0.4" 117 | } 118 | }, 119 | "node_modules/es-set-tostringtag": { 120 | "version": "2.1.0", 121 | "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 122 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 123 | "license": "MIT", 124 | "dependencies": { 125 | "es-errors": "^1.3.0", 126 | "get-intrinsic": "^1.2.6", 127 | "has-tostringtag": "^1.0.2", 128 | "hasown": "^2.0.2" 129 | }, 130 | "engines": { 131 | "node": ">= 0.4" 132 | } 133 | }, 134 | "node_modules/follow-redirects": { 135 | "version": "1.15.6", 136 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", 137 | "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", 138 | "funding": [ 139 | { 140 | "type": "individual", 141 | "url": "https://github.com/sponsors/RubenVerborgh" 142 | } 143 | ], 144 | "engines": { 145 | "node": ">=4.0" 146 | }, 147 | "peerDependenciesMeta": { 148 | "debug": { 149 | "optional": true 150 | } 151 | } 152 | }, 153 | "node_modules/form-data": { 154 | "version": "4.0.4", 155 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", 156 | "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", 157 | "license": "MIT", 158 | "dependencies": { 159 | "asynckit": "^0.4.0", 160 | "combined-stream": "^1.0.8", 161 | "es-set-tostringtag": "^2.1.0", 162 | "hasown": "^2.0.2", 163 | "mime-types": "^2.1.12" 164 | }, 165 | "engines": { 166 | "node": ">= 6" 167 | } 168 | }, 169 | "node_modules/function-bind": { 170 | "version": "1.1.2", 171 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 172 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 173 | "license": "MIT", 174 | "funding": { 175 | "url": "https://github.com/sponsors/ljharb" 176 | } 177 | }, 178 | "node_modules/get-intrinsic": { 179 | "version": "1.3.0", 180 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 181 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 182 | "license": "MIT", 183 | "dependencies": { 184 | "call-bind-apply-helpers": "^1.0.2", 185 | "es-define-property": "^1.0.1", 186 | "es-errors": "^1.3.0", 187 | "es-object-atoms": "^1.1.1", 188 | "function-bind": "^1.1.2", 189 | "get-proto": "^1.0.1", 190 | "gopd": "^1.2.0", 191 | "has-symbols": "^1.1.0", 192 | "hasown": "^2.0.2", 193 | "math-intrinsics": "^1.1.0" 194 | }, 195 | "engines": { 196 | "node": ">= 0.4" 197 | }, 198 | "funding": { 199 | "url": "https://github.com/sponsors/ljharb" 200 | } 201 | }, 202 | "node_modules/get-proto": { 203 | "version": "1.0.1", 204 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 205 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 206 | "license": "MIT", 207 | "dependencies": { 208 | "dunder-proto": "^1.0.1", 209 | "es-object-atoms": "^1.0.0" 210 | }, 211 | "engines": { 212 | "node": ">= 0.4" 213 | } 214 | }, 215 | "node_modules/gopd": { 216 | "version": "1.2.0", 217 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 218 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 219 | "license": "MIT", 220 | "engines": { 221 | "node": ">= 0.4" 222 | }, 223 | "funding": { 224 | "url": "https://github.com/sponsors/ljharb" 225 | } 226 | }, 227 | "node_modules/has-symbols": { 228 | "version": "1.1.0", 229 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 230 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 231 | "license": "MIT", 232 | "engines": { 233 | "node": ">= 0.4" 234 | }, 235 | "funding": { 236 | "url": "https://github.com/sponsors/ljharb" 237 | } 238 | }, 239 | "node_modules/has-tostringtag": { 240 | "version": "1.0.2", 241 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 242 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 243 | "license": "MIT", 244 | "dependencies": { 245 | "has-symbols": "^1.0.3" 246 | }, 247 | "engines": { 248 | "node": ">= 0.4" 249 | }, 250 | "funding": { 251 | "url": "https://github.com/sponsors/ljharb" 252 | } 253 | }, 254 | "node_modules/hasown": { 255 | "version": "2.0.2", 256 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 257 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 258 | "license": "MIT", 259 | "dependencies": { 260 | "function-bind": "^1.1.2" 261 | }, 262 | "engines": { 263 | "node": ">= 0.4" 264 | } 265 | }, 266 | "node_modules/math-intrinsics": { 267 | "version": "1.1.0", 268 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 269 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 270 | "license": "MIT", 271 | "engines": { 272 | "node": ">= 0.4" 273 | } 274 | }, 275 | "node_modules/mime-db": { 276 | "version": "1.52.0", 277 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 278 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 279 | "engines": { 280 | "node": ">= 0.6" 281 | } 282 | }, 283 | "node_modules/mime-types": { 284 | "version": "2.1.35", 285 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 286 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 287 | "dependencies": { 288 | "mime-db": "1.52.0" 289 | }, 290 | "engines": { 291 | "node": ">= 0.6" 292 | } 293 | }, 294 | "node_modules/object-inspect": { 295 | "version": "1.12.3", 296 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 297 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", 298 | "funding": { 299 | "url": "https://github.com/sponsors/ljharb" 300 | } 301 | }, 302 | "node_modules/proxy-from-env": { 303 | "version": "1.1.0", 304 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 305 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 306 | }, 307 | "node_modules/qs": { 308 | "version": "6.11.2", 309 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", 310 | "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", 311 | "dependencies": { 312 | "side-channel": "^1.0.4" 313 | }, 314 | "engines": { 315 | "node": ">=0.6" 316 | }, 317 | "funding": { 318 | "url": "https://github.com/sponsors/ljharb" 319 | } 320 | }, 321 | "node_modules/side-channel": { 322 | "version": "1.0.4", 323 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 324 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 325 | "dependencies": { 326 | "call-bind": "^1.0.0", 327 | "get-intrinsic": "^1.0.2", 328 | "object-inspect": "^1.9.0" 329 | }, 330 | "funding": { 331 | "url": "https://github.com/sponsors/ljharb" 332 | } 333 | } 334 | } 335 | } 336 | --------------------------------------------------------------------------------