Reduce risk by implementing HttpOnly cookie authentication in Amazon API Gateway
Some web applications need to protect their authentication tokens or session IDs from cross-site scripting (XSS). It’s an Open Web Application Security Project (OWASP) best practice for session management to store secrets in the browsers’ cookie store with the HttpOnly attribute enabled. When cookies have the HttpOnly attribute set, the browser will prevent client-side JavaScript code from accessing the value. This reduces the risk of secrets being compromised.
<p>In this blog post, you’ll learn how to store access tokens and authenticate with HttpOnly cookies in your own workloads when using <a href="https://aws.amazon.com/api-gateway/" target="_blank" rel="noopener">Amazon API Gateway</a> as the client-facing endpoint. The tutorial in this post will show you a solution to store OAuth2 access tokens in the browser cookie store, and verify user authentication through Amazon API Gateway. This post describes how to use <a href="https://aws.amazon.com/cognito/" target="_blank" rel="noopener">Amazon Cognito</a> to issue OAuth2 access tokens, but the solution is not limited to OAuth2. You can use <a href="https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html" target="_blank" rel="noopener">other kinds of tokens or session IDs</a>.</p>
<p>The solution consists of two decoupled parts:</p>
<ol>
<li>OAuth2 flow</li>
<li>Authentication check</li>
</ol>
<blockquote>
<p><strong>Note:</strong> This tutorial takes you through detailed step-by-step instructions to deploy an example solution. If you prefer to deploy the solution with a script, see the <a href="https://github.com/aws-samples/api-gw-http-only-cookie-auth" target="_blank" rel="noopener">api-gw-http-only-cookie-auth</a> GitHub repository.</p>
</blockquote>
<h2>Prerequisites</h2>
<p>No costs should incur when you deploy the application from this tutorial because the services you’re going to use are included in the <a href="https://aws.amazon.com/free" target="_blank" rel="noopener">AWS Free Tier</a>. However, be aware that small charges may apply if you have other workloads running in your AWS account and exceed the free tier. Make sure to <a href="https://aws.amazon.com/blogs/security/reduce-risk-by-implementing-httponly-cookie-authentication-in-amazon-api-gateway/#cleanup">clean up your resources</a> from this tutorial after deployment.</p>
<h2>Solution architecture</h2>
<p>This solution uses <a href="https://aws.amazon.com/cognito/" target="_blank" rel="noopener">Amazon Cognito</a>, <a href="https://aws.amazon.com/api-gateway/" target="_blank" rel="noopener">Amazon API Gateway</a>, and <a href="https://aws.amazon.com/lambda/" target="_blank" rel="noopener">AWS Lambda</a> to build a solution that persists OAuth2 access tokens in the browser cookie store. Figure 1 illustrates the solution architecture for the OAuth2 flow.</p>
<div id="attachment_28294" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28294" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img1-2.png" alt="Figure 1: OAuth2 flow solution architecture" width="761" height="221" class="size-full wp-image-28294">
<p id="caption-attachment-28294" class="wp-caption-text">Figure 1: OAuth2 flow solution architecture</p>
</div>
<ol>
<li>A user authenticates by using Amazon Cognito.</li>
<li>Amazon Cognito has an <a href="https://www.oauth.com/oauth2-servers/redirect-uris/" target="_blank" rel="noopener">OAuth2 redirect URI</a> pointing to your API Gateway endpoint and invokes the integrated Lambda function <code>oAuth2Callback</code>.</li>
<li>The <code>oAuth2Callback</code> Lambda function makes a request to the <a href="https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html" target="_blank" rel="noopener">Amazon Cognito token endpoint</a> with the OAuth2 authorization code to get the access token.</li>
<li>The Lambda function returns a response with the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie" target="_blank" rel="noopener">Set-Cookie</a> header, instructing the web browser to persist the access token as an HttpOnly cookie. The browser will automatically interpret the <code>Set-Cookie</code> header, because it’s a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies" target="_blank" rel="noopener">web standard</a>. HttpOnly cookies can’t be accessed through JavaScript—they can only be set through the <code>Set-Cookie </code>header.</li>
</ol>
<p>After the OAuth2 flow, you are set up to issue and store access tokens. Next, you need to verify that users are authenticated before they are allowed to access your protected backend. Figure 2 illustrates how the authentication check is handled.</p>
<div id="attachment_28297" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28297" loading="lazy" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img2-3.png" alt="Figure 2: Authentication check solution architecture" width="541" height="341" class="size-full wp-image-28297">
<p id="caption-attachment-28297" class="wp-caption-text">Figure 2: Authentication check solution architecture</p>
</div>
<ol>
<li>A user requests a protected backend resource. The browser automatically attaches HttpOnly cookies to every request, as defined in the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies" target="_blank" rel="noopener">web standard</a>.</li>
<li>The Lambda function <code>oAuth2Authorizer</code> acts as the <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html" target="_blank" rel="noopener">Lambda authorizer for HTTP APIs</a>. It validates whether requests are authenticated. If requests include the proper access token in the request cookie header, then it allows the request.</li>
<li>API Gateway only passes through requests that are authenticated.</li>
</ol>
<p>Amazon Cognito is not involved in the authentication check, because the Lambda function can validate the OAuth2 access tokens by using a <a href="https://auth0.com/docs/secure/tokens/json-web-tokens/validate-json-web-tokens" target="_blank" rel="noopener">JSON Web Token (JWT) validation check</a>.</p>
<h2>1. Deploying the OAuth2 flow</h2>
<p>In this section, you’ll deploy the first part of the solution, which is the OAuth2 flow. The OAuth2 flow is responsible for issuing and persisting OAuth2 access tokens in the browser’s cookie store.</p>
<h3>1.1. Create a mock protected backend</h3>
<p>As shown in in Figure 2, you need to protect a backend. For the purposes of this post, you create a mock backend by creating a simple Lambda function with a default response.</p>
<h4>To create the Lambda function</h4>
<ol>
<li>In the <a href="https://console.aws.amazon.com/lambda" target="_blank" rel="noopener">Lambda console</a>, choose <strong>Create function</strong>.<br><blockquote>
<p><strong>Note: </strong>Make sure to <a href="https://docs.aws.amazon.com/awsconsolehelpdocs/latest/gsg/select-region.html" target="_blank" rel="noopener">select your desired AWS Region</a>.</p>
</blockquote> </li>
<li>Choose <strong>Author from scratch</strong> as the option to create the function.</li>
<li>In the <strong>Basic information</strong> section as shown in , enter or select the following values:
</li>
<li>Choose <strong>Create function</strong>.
<div id="attachment_28298" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28298" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img3-1-1.png" alt="Figure 3: Configuring the getProtectedResource Lambda function" width="720" class="size-full wp-image-28298">
<p id="caption-attachment-28298" class="wp-caption-text">Figure 3: Configuring the getProtectedResource Lambda function</p>
</div> </li>
</ol>
<p>The default Lambda function code returns a simple <code>Hello from Lambda</code> message, which is sufficient to demonstrate the concept of this solution.</p>
<h3>1.2. Create an HTTP API in Amazon API Gateway</h3>
<p>Next, you create an <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html" target="_blank" rel="noopener">HTTP API</a> by using API Gateway. Either an HTTP API or a REST API will work. In this example, choose HTTP API because <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html" target="_blank" rel="noopener">it’s offered at a lower price point</a> (for this tutorial you will stay within the free tier).</p>
<h4>To create the API Gateway API</h4>
<ol>
<li>In the <a href="https://console.aws.amazon.com/apigateway" target="_blank" rel="noopener">API Gateway console</a>, under <strong>HTTP API</strong>, choose <strong>Build</strong>.</li>
<li>On the <strong>Create and configure integrations</strong> page, as shown in Figure 4, choose <strong>Add integration</strong>, then enter or select the following values:
<ul>
<li>Select <strong>Lambda</strong>.</li>
<li>For <strong>Lambda function</strong>, select the <strong>getProtectedResource</strong> Lambda function that you created in the previous section.</li>
<li>For <strong>API name</strong>, enter a name. In this example, I used <span>MyApp</span>.</li>
<li>Choose <strong>Next</strong>.</li>
</ul>
<div id="attachment_28299" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28299" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img4-2.png" alt="Figure 4: Configuring API Gateway integrations and API name" width="680" class="size-full wp-image-28299">
<p id="caption-attachment-28299" class="wp-caption-text">Figure 4: Configuring API Gateway integrations and API name</p>
</div> </li>
<li>On the <strong>Configure routes</strong> page, as shown in Figure 5, enter or select the following values:
<ul>
<li>For <strong>Method</strong>, select <strong>GET</strong>.</li>
<li>For <strong>Resource path</strong>, enter <strong>/</strong> (a single forward slash).</li>
<li>For <strong>Integration target</strong>, select the <strong>getProtectedResource</strong> Lambda function.</li>
<li>Choose <strong>Next</strong>.</li>
</ul>
<div id="attachment_28300" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28300" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img5-1-1024x469-1.png" alt="Figure 5: Configuring API Gateway routes" width="680" class="size-large wp-image-28300">
<p id="caption-attachment-28300" class="wp-caption-text">Figure 5: Configuring API Gateway routes</p>
</div> </li>
<li>On the <strong>Configure stages </strong>page, keep all the default options, and choose <strong>Next</strong>.</li>
<li>On the <strong>Review and create</strong> page, choose <strong>Create</strong>.</li>
<li>Note down the value of <strong>Invoke URL</strong>, as shown in Figure 6.
<div id="attachment_28301" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28301" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img6-1-1024x167-1.png" alt="Figure 6: Note down the invoke URL" width="680" class="size-large wp-image-28301">
<p id="caption-attachment-28301" class="wp-caption-text">Figure 6: Note down the invoke URL</p>
</div> </li>
</ol>
<p>Now it’s time to test your API Gateway API. Paste the value of <strong>Invoke URL</strong> into your browser. You’ll see the following message from your Lambda function: <code>Hello from Lambda</code>.</p>
<h3>1.3. Use Amazon Cognito</h3>
<p>You’ll use <a href="https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-with-cognito-user-pools.html" target="_blank" rel="noopener">Amazon Cognito user pools</a> to create and maintain a user directory, and add sign-up and sign-in to your web application.</p>
<h4>To create an Amazon Cognito user pool</h4>
<ol>
<li>In the <a href="https://console.aws.amazon.com/cognito/home" target="_blank" rel="noopener">Amazon Cognito console</a>, choose <strong>Create user pool</strong>.</li>
<li>On the <strong>Authentication providers</strong> page, as shown in Figure 7, for <strong>Cognito user pool sign-in options</strong>, select <strong>Email</strong>, then choose <strong>Next</strong>.
<div id="attachment_28302" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28302" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img7-1-1024x751-1.png" alt="Figure 7: Configuring authentication providers" width="680" class="size-large wp-image-28302">
<p id="caption-attachment-28302" class="wp-caption-text">Figure 7: Configuring authentication providers</p>
</div> </li>
<li>In the <strong>Multi-factor authentication</strong> pane of the <strong>Configure Security requirements</strong> page, as shown in Figure 8, choose your MFA enforcement. For this example, choose <strong>No MFA</strong> to make it simpler for you to test your solution. However, in production for data sensitive workloads you should choose <strong>Require MFA – Recommended</strong>. Choose <strong>Next</strong>.
<div id="attachment_28303" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28303" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img8-1-1024x375-1.png" alt="Figure 8: Configuring MFA" width="680" class="size-large wp-image-28303">
<p id="caption-attachment-28303" class="wp-caption-text">Figure 8: Configuring MFA</p>
</div> </li>
<li>On the <strong>Configure sign-up experience</strong> page, keep all the default options and choose <strong>Next</strong>.</li>
<li>On the <strong>Configure message delivery</strong> page, as shown in Figure 9, choose your email provider. For this example, choose <strong>Send email with Cognito</strong> to make it simple to test your solution. In production workloads, you should choose <strong>Send email with Amazon SES – Recommended</strong>. Choose <strong>Next</strong>.
<div id="attachment_28304" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28304" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img9-1-1024x697-1.png" alt="Figure 9: Configuring email" width="680" class="size-large wp-image-28304">
<p id="caption-attachment-28304" class="wp-caption-text">Figure 9: Configuring email</p>
</div> </li>
<li>In the <strong>User pool name</strong> section of the <strong>Integrate your app</strong> page, as shown in Figure 10, enter or select the following values:
<ol>
<li>For <strong>User pool name</strong>, enter a name. In this example, I used <span>MyUserPool</span>.
<div id="attachment_28305" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28305" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img10-1.png" alt="Figure 10: Configuring user pool name" width="640" class="size-full wp-image-28305">
<p id="caption-attachment-28305" class="wp-caption-text">Figure 10: Configuring user pool name</p>
</div> </li>
<li>In the <strong>Hosted authentication pages</strong> section, as shown in Figure 11, select <strong>Use the Cognito Hosted UI</strong>.
<div id="attachment_28306" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28306" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img11-1.png" alt="Figure 11: Configuring hosted authentication pages" width="640" class="size-full wp-image-28306">
<p id="caption-attachment-28306" class="wp-caption-text">Figure 11: Configuring hosted authentication pages</p>
</div> </li>
<li>In the <strong>Domain</strong> section, as shown in Figure 12, for <strong>Domain type</strong>, choose <strong>Use a Cognito domain</strong>. For <strong>Cognito domain</strong>, enter a domain name. Note that domains in Cognito must be unique. Make sure to enter a unique name, for example by appending random numbers at the end of your domain name. For this example, I used <span>https://http-only-cookie-secured-app</span>.
<div id="attachment_28307" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28307" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img12-1.png" alt="Figure 12: Configuring an Amazon Cognito domain" width="640" class="size-full wp-image-28307">
<p id="caption-attachment-28307" class="wp-caption-text">Figure 12: Configuring an Amazon Cognito domain</p>
</div> </li>
<li>In the <strong>Initial app client</strong> section, as shown in Figure 13, enter or select the following values:
<ul>
<li>For <strong>App type</strong>, keep the default setting <strong>Public client</strong>.</li>
<li>For <strong>App client name</strong>, enter a friendly name. In this example, I used <span>MyAppClient</span>.</li>
<li>For <strong>Client secret</strong>, keep the default setting <strong>Don’t generate a client secret</strong>.</li>
<li>For<strong> Allowed callback URLs</strong>, enter <span></span><span>/oauth2/callback</span>, replacing <span></span> with the invoke URL you noted down from API Gateway in the previous section.
<div id="attachment_28308" class="wp-caption aligncenter">
<img aria-describedby="caption-attachment-28308" src="https://www.infracom.com.sg/wp-content/uploads/2023/01/img13-870x1024-1.png" alt="Figure 13: Configuring the initial app client" width="680" class="size-large wp-image-28308">
<p id="caption-attachment-28308" class="wp-caption-text">Figure 13: Configuring the initial app client</p>
</div> </li>
</ul> </li>
<li>Choose <strong>Next</strong>.</li>
</ol> </li>
<li>Choose <strong>Create user pool</strong>.</li>
</ol>
<p>Next, you need to retrieve some Amazon Cognito information for later use.</p>
<h4>To note down Amazon Cognito information</h4>
<ol>
<li>In the <a href="https://console.aws.amazon.com/cognito/home" target="_blank" rel="noopener">Amazon Cognito console</a>, choose the user pool you created in the previous steps.</li>
<li>Under <strong>User pool overview</strong>, make<strong> </strong>note of the <strong>User pool ID</strong> value.</li>
<li>On the <strong>App integration</strong> tab, under <strong>Cognito Domain</strong>, make note of the <strong>Domain</strong> value.</li>
<li>Under <strong>App client list</strong>, make note of the <strong>Client ID</strong> value.</li>
<li>Under <strong>App client list</strong>, choose the app client name you created in the previous steps.</li>
<li>Under <strong>Hosted UI</strong>, make note of the <strong>Allowed callback URLs</strong> value.</li>
</ol>
<p>Next, create the user that you will use in a later section of this post to run your test.</p>
<h4>To create a user</h4>
<ol>
<li>In the <a href="https://console.aws.amazon.com/cognito/home" target="_blank" rel="noopener">Amazon Cognito console</a>, choose the user pool you created in the previous steps.</li>
<li>Under <strong>Users</strong>, choose <strong>Create user</strong>.</li>
<li>For <strong>Email address</strong>, enter <span>user@example.com</span>. For this tutorial, you don’t need to send out actual emails, so the email address does not need to actually exist.</li>
<li>Choose <strong>Mark email address as verified</strong>.</li>
<li>For <strong>password</strong>, enter a password you can remember (or even better: use a password generator).</li>
<li>Remember the email and password for later use.</li>
<li>Choose <strong>Create user</strong>.</li>
</ol>
<h3>1.4. Create the Lambda function oAuth2Callback</h3>
<p>Next, you create the Lambda function <span>oAuth2Callback</span>, which is responsible for issuing and persisting the OAuth2 access tokens.</p>
<h4>To create the Lambda function oAuth2Callback</h4>
<ol>
<li>In the <a href="https://console.aws.amazon.com/lambda" target="_blank" rel="noopener">Lambda console</a>, choose <strong>Create function</strong>.<br><blockquote>
<p><strong>Note: </strong>Make sure to <a href="https://docs.aws.amazon.com/awsconsolehelpdocs/latest/gsg/select-region.html" target="_blank" rel="noopener">select your desired Region</a>.</p>
</blockquote> </li>
<li>For <strong>Function name</strong>, enter <span>oAuth2Callback</span>.</li>
<li>For <strong>Runtime</strong>, select <strong>Node.js 16.x</strong>.</li>
<li>For <strong>Architecture</strong>, select <strong>arm64</strong>.</li>
<li>Choose <strong>Create function</strong>.</li>
</ol>
<p>After you create the Lambda function, you need to add the code. Create a new folder on your local machine and open it with your preferred integrated development environment (IDE). Add the <span>package.json</span> and <span>index.js</span> files, as shown in the following examples.</p>
<p><strong>package.json</strong></p>
<pre><code class="lang-json">{
“name”: “oAuth2Callback”,
“version”: “0.0.1”,
“dependencies”: {
“axios”: “^0.27.2”,
“qs”: “^6.11.0”
}
}
In a terminal at the root of your created folder, run the following command.
$ npm install
In the index.js example code that follows, be sure to replace the placeholders with your values.
index.js
const qs = require("qs");
const axios = require("axios").default;
exports.handler = async function (event) {
const code = event.queryStringParameters?.code;
if (code == null) {
return {
statusCode: 400,
body: "code query param required",
};
}
const data = {
grant_type: "authorization_code",
client_id: "",
// The redirect has already happened, but you still need to pass the URI for validation, so a valid oAuth2 access token can be generated
redirect_uri: encodeURI(""),
code: code,
};
// 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
const res = await axios.post(
"/oauth2/token",
qs.stringify(data),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
return {
statusCode: 302,
// These headers are returned as part of the response to the browser.
headers: {
// The Location header tells the browser it should redirect to the root of the URL
Location: "/",
// The Set-Cookie header tells the browser to persist the access token in the cookie store
"Set-Cookie": accessToken=${res.data.access_token}; Secure; HttpOnly; SameSite=Lax; Path=/
, }, }; };
Along with the HttpOnly attribute, you pass along two additional cookie attributes:
Secure
– Indicates that cookies are only sent by the browser to the server when a request is made with thehttps:
scheme.SameSite
– Controls whether or not a cookie is sent with cross-site requests, providing protection against cross-site request forgery attacks. You set the value toLax
because you want the cookie to be set when the user is forwarded from Amazon Cognito to your web application (which runs under a different URL).
For more information, see Using HTTP cookies on the MDN Web Docs site.
Afterwards, upload the code to the oAuth2Callback Lambda function as described in Upload a Lambda Function in the AWS Toolkit for VS Code User Guide.
1.5. Configure an OAuth2 callback route in API Gateway
Now, you configure API Gateway to use your new Lambda function through a Lambda proxy integration.
To configure API Gateway to use your Lambda function
- In the API Gateway console, under APIs, choose your API name. For me, the name is MyApp.
- Under Develop, choose Routes.
- Choose Create.
- Enter or select the following values:
- For method, select GET.
- For path, enter /oauth2/callback.
- Choose Create.
- Choose GET under /oauth2/callback, and then choose Attach integration.
- Choose Create and attach an integration.
- For Integration type, choose Lambda function.
- For Lambda function, choose oAuth2Callback from the last step.
- Choose Create.
Your route configuration in API Gateway should now look like Figure 14.
2. Testing the OAuth2 flow
Now that you have the components in place, you can test your OAuth2 flow. You test the OAuth2 flow by invoking the login on your browser.
To test the OAuth2 flow
- In the Amazon Cognito console, choose your user pool name. For me, the name is MyUserPool.
- Under the navigation tabs, choose App integration.
- Under App client list, choose your app client name. For me, the name is MyAppClient.
- Choose View Hosted UI.
- In the newly opened browser tab, open your developer tools, so you can inspect the network requests.
- Log in with the email address and password you set in the previous section. Change your password, if you’re asked to do so. You can also choose the same password as you set in the previous section.
- You should see your
Hello from Lambda
message.
To test that the cookie was accurately set
- Check your browser network tab in the browser developer settings. You’ll see the
/oauth2/callback
request, as shown in Figure 15.The response headers should include a
set-cookie
header, as you specified in your Lambda function. With theset-cookie
header, your OAuth2 access token is set as an HttpOnly cookie in the browser, and access is prohibited from any client-side code. - Alternatively, you can inspect the cookie in the browser cookie storage, as shown in Figure 16.
- If you want to retry the authentication, navigate in your browser to your Amazon Cognito domain that you chose in the previous section and clear all site data in the browser developer tools. Do the same with your API Gateway invoke URL. Now you can restart the test with a clean state.
3. Deploying the authentication check
In this section, you’ll deploy the second part of your application: the authentication check. The authentication check makes it so that only authenticated users can access your protected backend. The authentication check works with the HttpOnly cookie, which is stored in the user’s cookie store.
3.1. Create the Lambda function oAuth2Authorizer
This Lambda function checks that requests are authenticated.
To create the Lambda function
- In the Lambda console, choose Create function.
Note: Make sure to select your desired Region.
- For Function name, enter oAuth2Authorizer.
- For Runtime, select Node.js 16.x.
- For Architecture, select arm64.
- Choose Create function.
After you create the Lambda function, you need to add the code. Create a new folder on your local machine and open it with your preferred IDE. Add the package.json and index.js files as shown in the following examples.
package.json
{
"name": "oAuth2Authorizer",
"version": "0.0.1",
"dependencies": {
"aws-jwt-verify": "^3.1.0"
}
}
In a terminal at the root of your created folder, run the following command.
$ npm install
In the index.js example code, be sure to replace the placeholders with your values.
index.js
const { CognitoJwtVerifier } = require("aws-jwt-verify");
function getAccessTokenFromCookies(cookiesArray) {
// cookieStr contains the full cookie definition string: "accessToken=abc"
for (const cookieStr of cookiesArray) {
const cookieArr = cookieStr.split("accessToken=");
// 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"]
if (cookieArr[1] != null) {
return cookieArr[1]; // Returning only the value of the access token without cookie name
}
}
return null;
}
// Create the verifier outside the Lambda handler (= during cold start),
// so the cache can be reused for subsequent invocations. Then, only during the
// first invocation, will the verifier actually need to fetch the JWKS.
const verifier = CognitoJwtVerifier.create({
userPoolId: "",
tokenUse: "access",
clientId: "",
});
exports.handler = async (event) => {
if (event.cookies == null) {
console.log("No cookies found");
return {
isAuthorized: false,
};
}
// Cookies array looks something like this: ["accessToken=abc", "otherCookie=Random Value"]
const accessToken = getAccessTokenFromCookies(event.cookies);
if (accessToken == null) {
console.log("Access token not found in cookies");
return {
isAuthorized: false,
};
}
try {
await verifier.verify(accessToken);
return {
isAuthorized: true,
};
} catch (e) {
console.error(e);
return {
isAuthorized: false,
};
}
};
After you add the package.json and index.js files, upload the code to the oAuth2Authorizer Lambda function as described in Upload a Lambda Function in the AWS Toolkit for VS Code User Guide.
3.2. Configure the Lambda authorizer in API Gateway
Next, you configure your authorizer Lambda function to protect your backend. This way you control access to your HTTP API.
To configure the authorizer Lambda function
- In the API Gateway console, under APIs, choose your API name. For me, the name is MyApp.
- Under Develop, choose Routes.
- Under / (a single forward slash) GET, choose Attach authorization.
- Choose Create and attach an authorizer.
- Choose Lambda.
- Enter or select the following values:
- For Name, enter oAuth2Authorizer.
- For Lambda function, choose oAuth2Authorizer.
- Clear Authorizer caching. For this tutorial, you disable authorizer caching to make testing simpler. See the section Bonus: Enabling authorizer caching for more information about enabling caching to increase performance.
- Under Identity sources, choose Remove.
Note: Identity sources are ignored for your Lambda authorizer. These are only used for caching.
- Choose Create and attach.
- Under Develop, choose Routes to inspect all routes.
Now your API Gateway route /oauth2/callback
should be configured as shown in Figure 17.
4. Testing the OAuth2 authorizer
You did it! From your last test, you should still be authenticated. So, if you open the API Gateway Invoke URL in your browser, you’ll be greeted from your protected backend.
In case you are not authenticated anymore, you’ll have to follow the steps again from the section Testing the OAuth2 flow to authenticate.
When you inspect the HTTP request that your browser makes in the developer tools as shown in Figure 18, you can see that authentication works because the HttpOnly cookie is automatically attached to every request.
To verify that your authorizer Lambda function works correctly, paste the same Invoke URL you noted previously in an incognito window. Incognito windows do not share the cookie store with your browser session, so you see a {"message":"Forbidden"}
error message with HTTP response code 403 – Forbidden
.
Cleanup
Delete all unwanted resources to avoid incurring costs.
To delete the Amazon Cognito domain and user pool
- In the Amazon Cognito console, choose your user pool name. For me, the name is MyUserPool.
- Under the navigation tabs, choose App integration.
- Under Domain, choose Actions, then choose Delete Cognito domain.
- Confirm by entering your custom Amazon Cognito domain, and choose Delete.
- Choose Delete user pool.
- Confirm by entering your user pool name (in my case, MyUserPool), and then choose Delete.
To delete your API Gateway resource
- In the API Gateway console, select your API name. For me, the name is MyApp.
- Under Actions, choose Delete and confirm your deletion.
To delete the AWS Lambda functions
- In the Lambda console, select all three of the Lambda functions you created.
- Under Actions, choose Delete and confirm your deletion.
Bonus: Enabling authorizer caching
As mentioned earlier, you can enable authorizer caching to help improve your performance. When caching is enabled for an authorizer, API Gateway uses the authorizer’s identity sources as the cache key. If a client specifies the same parameters in identity sources within the configured Time to Live (TTL), then API Gateway uses the cached authorizer result, rather than invoking your Lambda function.
To enable caching, your authorizer must have at least one identity source. To cache by the cookie request header, you specify $request.header.cookie
as the identity source. Be aware that caching will be affected if you pass along additional HttpOnly cookies apart from the access token.
For more information, see Working with AWS Lambda authorizers for HTTP APIs in the Amazon API Gateway Developer Guide.
Conclusion
In this blog post, you learned how to implement authentication by using HttpOnly cookies. You used Amazon API Gateway and AWS Lambda to persist and validate the HttpOnly cookies, and you used Amazon Cognito to issue OAuth2 access tokens. If you want to try an automated deployment of this solution with a script, see the api-gw-http-only-cookie-auth GitHub repository.
The application of this solution to protect your secrets from potential cross-site scripting (XSS) attacks is not limited to OAuth2. You can protect other kinds of tokens, sessions, or tracking IDs with HttpOnly cookies.
In this solution, you used NodeJS for your Lambda functions to implement authentication. But HttpOnly cookies are widely supported by many programing frameworks. You can find more implementation options on the OWASP Secure Cookie Attribute page.
Although this blog post gives you a tutorial on how to implement HttpOnly cookie authentication in API Gateway, it may not meet all your security and functional requirements. Make sure to check your business requirements and talk to your stakeholders before you adopt techniques from this blog post.
Furthermore, it’s a good idea to continuously test your web application, so that cookies are only set with your approved security attributes. For more information, see the OWASP Testing for Cookies Attributes page.
If you have feedback about this post, submit comments in the Comments section below. If you have questions about this post, start a new thread on the Amazon API Gateway re:Post or contact AWS Support.
Want more AWS Security news? Follow us on Twitter.
<!-- '"` -->
You must be logged in to post a comment.