2024 07 02 implement httponly cookies with cognito use tokens for authentication code adjustments - bizkite-co/handterm GitHub Wiki


original file name: Codeium Chat - W4XTfYNrGj70Jfv697l6al8nYNv8PkBT.md summary: The user is facing an issue with implementing HttpOnly Cookies in their client-side code to authenticate through Cognito via an AWS CDK endpoint. Due to cookie setting problems, they have temporarily switched to using access tokens and ID tokens for user validation. They have modified their client-side useAuth code to save tokens to localStorage and want to update the getUser function to send the proper token to the server-side getUser Lambda function for authentication on the server side.

I provided guidance on updating the getUser function to include the ID Token in the Authorization header of the Axios request. Additionally, the user shared their getUser Lambda function in the CDK code, and I suggested modifying the function to extract the Bearer token from the Authorization header, authenticate with Cognito, and retrieve user data.

When testing the login process, the user encountered issues logging in on the client side and calling the getUser function, as seen in the CloudWatch log. They shared the logged headers, including the Authorization header with the Bearer token.

If you need more details on any specific part of the conversation, feel free to ask! date: 2024-07-02T13:00

Codeium Chat Conversation

Note: This is purely the output of the chat conversation and does not contain any raw data, codebase snippets, etc. used to generate the output.

User at 1:00 pm, Jul 2, 2024

I have been trying to implement HttpOnly Cookies in my client-side code which connects to my AWS CDK endpoint to authenticate through Cognito.

I have all the code implemented by the browser is not setting the cookies in the browser and that breaks all the subsequent steps which rely on the cookies being set so that they can be included in subsequent requests by setting "WithCredentials" to true in the Axios request.

I want to set that problem aside for a while and just use the access token and id token to validate the user, as a temporary workaround.

I've updated my client-side useAuth code to save the tokens to localStorage:

// src/lib/useAuth.ts

import { useState, useEffect } from 'react';
import axios from 'axios';
import { ENDPOINTS } from '../shared/endpoints';

const API_URL = ENDPOINTS.api.BaseUrl;

const config = {
 headers: {
  "Content-Type": "application/json",
 },
 withCredentials: true, // Correctly positioned and boolean value
};

export const useAuth = () => {
 const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);

 useEffect(() => {
  // Optionally check session validity on hook mount

 }, []);

 const checkSession = async () => {
  try {
   // This could be a call to a `/session` endpoint that verifies the session
   await axios.get(`${API_URL}${ENDPOINTS.api.CheckSession}`, config);
   setIsLoggedIn(true);
  } catch (error) {
   console.error('Session check failed:', error);
   setIsLoggedIn(false);
  }
 };

 const signUp = async (username: string, password: string, email: string) => {
  if (!username || !password || !email) {
   throw new Error('All fields are required');
  }
  try {
   await axios.post(`${API_URL}${ENDPOINTS.api.SignUp}`, { username, password, email }, config);
   // Handle post-signup logic (e.g., auto-login or redirect to login page)
  } catch (error) {
   console.error('Signup failed:', error);
   throw error;
  }
 };

 const getUser = async () => {
  try {
   const response = await axios.get(`${API_URL}${ENDPOINTS.api.GetUser}`, config);
   return response.data; // Contains username, attributes, etc.
  } catch (error) {
   console.error('Error fetching current user:', error);
   return null;
  }
 };

 const signIn = async (username: string, password: string) => {
  if (!username || !password) {
   throw new Error('All fields are required');
  }
  try {
   const response = await axios.post(`${API_URL}${ENDPOINTS.api.SignIn}`, { username, password });
   // The API should set an HttpOnly cookie directly, no need to handle tokens here
   console.log('Login successful:', response.data);
   setIsLoggedIn(true);
   localStorage.setItem('AccessToken', response.data.AccessToken);
   localStorage.setItem('RefreshToken', response.data.RefreshToken);
   localStorage.setItem('IdToken', response.data.IdToken);
   localStorage.setItem(
    'ExpiresAt', 
    new Date().getTime() + (parseInt(response.data.ExpiresIn, 10) * 1000).toString(10)
   );
   return response.data; // Return session data if needed
  } catch (error) {
   console.error('Login failed:', error);
   throw error;
  }
 };

 const signOut = async () => {
  try {
   await axios.get(`${API_URL}${ENDPOINTS.api.SignOut}`);
   setIsLoggedIn(false);
  } catch (error) {
   console.error('Logout failed:', error);
   throw error;
  }
 };

 const refreshSession = async () => {
  try {
   // This endpoint should refresh the session and set a new HttpOnly cookie
   await axios.post(`${API_URL}${ENDPOINTS.api.RefreshSession}`);
   setIsLoggedIn(true);
  } catch (error) {
   console.error('Session refresh failed:', error);
   setIsLoggedIn(false);
   throw error;
  }
 };

 const changePassword = async (oldPassword: string, newPassword: string) => {
  try {
   await axios.post(`${API_URL}${ENDPOINTS.api.ChangePassword}`, { oldPassword, newPassword });
  } catch (error) {
   console.error('Password change failed:', error);
   throw error;
  }
 };

 return { isLoggedIn, login: signIn, logout: signOut, signUp, refreshSession, getUser, checkSession, changePassword };
};

Now I want to make the getUser function send the proper token to the server-side getUser lambda in my AWS HTTP API, so it can be authentidated there by calling Cognito on the server side like I would do if the HttpOnly Cookies had worked.

I need your help with that part.


Codeium at 1:00 pm, Jul 2, 2024

To modify the getUser function in your useAuth hook to send the proper token to the server-side getUser lambda, you can include the Authorization header in the Axios request. This header should contain the ID Token retrieved from localStorage, which will then be used for authentication on the server side by calling Cognito.

Here's how you can update the getUser function:

// src/lib/useAuth.ts

const getUser = async () => {
  try {
    // Retrieve the ID Token from localStorage
    const idToken = localStorage.getItem('IdToken');
    if (!idToken) {
      throw new Error('No ID token found');
    }

    // Include the ID Token in the Authorization header
    const authConfig = {
      ...config, // Spread the existing config to keep content-type
      headers: {
        ...config.headers, // Spread any existing headers
        'Authorization': `Bearer ${idToken}` // Add the Authorization header
      }
    };

    // Make the request with the ID Token
    const response = await axios.get(`${API_URL}${ENDPOINTS.api.GetUser}`, authConfig);
    return response.data; // Contains username, attributes, etc.
  } catch (error) {
    console.error('Error fetching current user:', error);
    return null;
  }
};

This update ensures that when the getUser function is called, it sends the ID Token in the Authorization header. The server-side lambda function handling the getUser request needs to parse this token, validate it with AWS Cognito, and then proceed with the user data retrieval based on the token's validity.

Remember, on the server-side, you'll need to implement logic within your getUser lambda function to:

  1. Extract the Authorization header.
  2. Use the token from the Authorization header to make a call to Cognito for token validation.
  3. Retrieve the user's information from Cognito or your application's database as needed, based on the token's claims.

This approach bypasses the need for HttpOnly cookies by directly using tokens for authentication and session management. While it's less secure than the HttpOnly cookie method (since tokens stored in localStorage can be accessed by JavaScript and are thus vulnerable to XSS attacks), it serves as a valid temporary workaround until you resolve the cookie handling issue.


User at 1:09 pm, Jul 2, 2024

Ok, and this is the getUser Lambda function in my CDK code.

// cdk/lambda/authentication/checkConnection.ts

import * as AWS from 'aws-sdk';

const s3 = new AWS.S3({ region: 'us-east-1' });
const cognito = new AWS.CognitoIdentityServiceProvider({ region: 'us-east-1' });


export const handler = async (event: any) => {
  console.log('getUser received event:', event);
  let response = { statusCode: 401, body: 'Unauthorized', log: 'init\r' };
  try {
    console.log('event.headers: ', event.headers);
    const cookies = event.headers['Cookies'] || event.headers['cookies'];
    console.log('cookies: ', cookies);
    // Parse the cookies to extract needed tokens
    const accessToken = cookies["accessToken"]; // Consider renaming if using an actual access token
    console.log('accessToken: ', accessToken);
    response.log += `accessToken: ${accessToken}\r`;

    // Identify the user from the token and proceed with S3 operations
    // Use the accessToken to get user information from Cognito
    const user = await cognito.getUser({
      AccessToken: accessToken
    }).promise();

    const userId = user.Username;
    console.log('userId: ', userId);
    response.log += `userId: ${userId}\r`;
    // Example S3 operation
    const objectKey = `user_data/${userId}/data.txt`;

    response.log += `objectKey: ${objectKey}\r`;
    const s3Response = await s3.getObject({ Bucket: 'handterm', Key: objectKey }).promise();
    response.log += `s3Response: ${s3Response}\r`;
    console.log('s3Response: ', s3Response);
    // If the file is found, return its content
    const fileContent = s3Response.Body?.toString('utf-8');
    response.log += `fileContent: ${fileContent}\r`;
    console.log('fileContent: ', fileContent);
    response.statusCode = 200;
    response.body = JSON.stringify({ userId: userId, content: fileContent });
    return response;
  } catch (err) {
    response.statusCode = 500;
    return { statusCode: 500, body: JSON.stringify(err) };
  }
}

How would I extract the bearer token and use it to authenticate the user with Cognito.

I want to keep the overall structure as close as is practical to the HttpOnly Cookies process flow, so that I will have to do as little change is is practical when (if?) I finally get the HttpOnly Cookies process flow working.


Codeium at 1:10 pm, Jul 2, 2024

To modify your getUser Lambda function to extract the Bearer token from the Authorization header and use it to authenticate with Cognito, you can adjust the function to look for the token in the Authorization header instead of looking for cookies. This involves parsing the Authorization header to extract the token and then using that token in your call to Cognito.

Here's how you can modify the function to use the Bearer token:

// cdk/lambda/authentication/checkConnection.ts

import * as AWS from 'aws-sdk';

const s3 = new AWS.S3({ region: 'us-east-1' });
const cognito = new AWS.CognitoIdentityServiceProvider({ region: 'us-east-1' });

export const handler = async (event: any) => {
  console.log('getUser received event:', event);
  let response = { statusCode: 401, body: 'Unauthorized', log: 'init\r' };
  try {
    console.log('event.headers: ', event.headers);
    // Extract the Authorization header
    const authorizationHeader = event.headers['Authorization'] || event.headers['authorization'];
    if (!authorizationHeader) {
      throw new Error('Authorization header is missing');
    }

    // Extract the token from the Authorization header
    const match = authorizationHeader.match(/^Bearer (.*)$/);
    if (!match || match.length < 2) {
      throw new Error('Bearer token not found');
    }
    const accessToken = match[1];

    console.log('accessToken: ', accessToken);
    response.log += `accessToken: ${accessToken}\r`;

    // Use the accessToken to get user information from Cognito
    const user = await cognito.getUser({
      AccessToken: accessToken
    }).promise();

    const userId = user.Username;
    console.log('userId: ', userId);
    response.log += `userId: ${userId}\r`;
    // Example S3 operation
    const objectKey = `user_data/${userId}/data.txt`;

    response.log += `objectKey: ${objectKey}\r`;
    const s3Response = await s3.getObject({ Bucket: 'handterm', Key: objectKey }).promise();
    response.log += `s3Response: ${s3Response}\r`;
    console.log('s3Response: ', s3Response);
    // If the file is found, return its content
    const fileContent = s3Response.Body?.toString('utf-8');
    response.log += `fileContent: ${fileContent}\r`;
    console.log('fileContent: ', fileContent);
    response.statusCode = 200;
    response.body = JSON.stringify({ userId: userId, content: fileContent });
    return response;
  } catch (err) {
    console.error('Error:', err);
    response.statusCode = 500;
    return { statusCode: 500, body: JSON.stringify(err) };
  }
}

This version of the function looks for an Authorization header and parses it to extract the Bearer token. It then uses this token to authenticate the user by calling cognito.getUser with the token. This approach aligns closely with the HttpOnly Cookies process flow in terms of how the authentication step is handled, with the main difference being the source of the token.

By keeping the structure similar, you should be able to switch back to using HttpOnly Cookies with minimal changes once you resolve the issues with cookie handling.


User at 1:38 pm, Jul 2, 2024

OK, I've removed a little bit of the loggin features and it looks like this now:

// cdk/lambda/authentication/checkConnection.ts

import * as AWS from 'aws-sdk';

const s3 = new AWS.S3({ region: 'us-east-1' });
const cognito = new AWS.CognitoIdentityServiceProvider({ region: 'us-east-1' });

export const handler = async (event: any) => {
  try {
    console.log('event.headers: ', event.headers);
    const cookies = event.headers['Cookies'] || event.headers['cookies'];
    console.log('cookies: ', cookies);

    // Extract the Authorization header
    const authorizationHeader = event.headers['Authorization'] || event.headers['authorization'];
    if (!authorizationHeader) {
      throw new Error('Authorization header is missing');
    }

    // Extract the token from the Authorization header
    const match = authorizationHeader.match(/^Bearer (.*)$/);
    if (!match || match.length < 2) {
      throw new Error('Bearer token not found');
    }
    const accessToken = match[1];
    console.log('accessToken: ', accessToken);
    // Use the accessToken to get user information from Cognito
    const user = await cognito.getUser({
      AccessToken: accessToken
    }).promise();

    const userId = user.Username;
    console.log('userId: ', userId);
    // Example S3 operation
    const objectKey = `user_data/${userId}/_index.md`;

    const s3Response = await s3.getObject({ Bucket: 'handterm', Key: objectKey }).promise();
    console.log('s3Response: ', s3Response);
    // If the file is found, return its content
    const fileContent = s3Response.Body?.toString('utf-8');
    console.log('fileContent: ', fileContent);
    const response = {
      statusCode: 200,
      body: JSON.stringify({ content: fileContent || 'Profile doesn\'t exist yet' }),
    };
    return response;
  } catch (err) {
    console.error('Error:', err);
    return { statusCode: 500, body: JSON.stringify(err) };
  }
}

I put the cookies logging back in there because I am going to be trying to implement that and I want to know when it works.

When I log in on the client side and then call getUser I get this in the CloudWatch log:

2024-07-02T20:31:49.309Z	08809faf-f0eb-4516-9adf-9277cc4b4185	INFO	event.headers:  {
  accept: 'application/json, text/plain, */*',
  'accept-encoding': 'gzip, deflate, br, zstd',
  'accept-language': 'en-US,en;q=0.9',
  authorization: 'Bearer eyJraWQiOiJ6SldrNXZ1XC81TUVXc2Z1YU5ZZVNPK1wvYStEaENOR3QrUmxKc2xVOThGakk9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJlNDE4MjRhOC0yMDYxLTcwOTMtYzk0OS02MDJkYjkyNmI1OTEiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfVEw0N09MY3lJIiwiY29nbml0bzp1c2VybmFtZSI6ImU0MTgyNGE4LTIwNjEtNzA5My1jOTQ5LTYwMmRiOTI2YjU5MSIsIm9yaWdpbl9qdGkiOiJhMjI3NzA1ZS04MDE1LTQzNTctOTQ1ZS0wNjQzZTFhN2NkYWQiLCJhdWQiOiIxbGkxYWEwNTU4aDF1Z3Nob29jNGw2OXI2biIsImV2ZW50X2lkIjoiYjMzOWRkMDItMGQ4Ny00MmQyLTk4YzItZDBjNWM5OGY4NjMxIiwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE3MTk5NTIyNDIsImV4cCI6MTcxOTk1NTg0MiwiaWF0IjoxNzE5OTUyMjQyLCJqdGkiOiIzZTNmYzNhMC00NDAxLTQ4YTQtOTJlNS00ZDI5ODMxMzE0MTYiLCJlbWFpbCI6Im1hcmsrM0B3aHl3ZS50ZWNoIn0.g2yhDxNt5zHJujLyi3gxu4nbKm_p1kraRxTjJ87k0s6-WkKV0yInpkdrA169qVpkdAb5klV6qEj3jSEkRpVIMlZ1Yn-5bJ20My9ICjHjxXUHVGKDCjPzff7aRl1osf9N2ReTsqQGg-a-Id1F1v65ZoiFgmwyK9w2T0bKVg2lFuu7H2k5tn66a26guObUNNIRnUpI3zQt2EsbF7oVIAZExXdygPX7eQ_wm-guK1iNcI6QPTMRB_VtSopRA_MjpXi_-HXVex7udixiyZBBIhtKARhMWcGxpYivS7qXb-Un_D_CFJZCLJHEmRjeNyTMTXnJpSraXRqvk9RMEqvICJ6scA',
  'content-length': '0',
  host: 'x7jzrjfuv8.execute-api.us-east-1.amazonaws.com',
  origin: 'http://localhost:5173',
  priority: 'u=1, i',
  referer: 'http://localhost:5173/',
  'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Microsoft Edge";v="126"',
  'sec-ch-ua-mobile': '?0',
  'sec-ch-ua-platform': '"Windows"',
  'sec-fetch-dest': 'empty',
  'sec-fetch-mode': 'cors',
  'sec-fetch-site': 'cross-site',
  'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0',
  'x-amzn-trace-id': 'Root=1-668463b4-65f1a5d336f5a2c233c655eb',
  'x-forwarded-for': '104.32.208.200',
  'x-forwarded-port': '443',
  'x-forwarded-proto': 'https'
}
2024-07-02T20:31:49.312Z	08809faf-f0eb-4516-9adf-9277cc4b4185	INFO	cookies:  undefined
2024-07-02T20:31:49.368Z	08809faf-f0eb-4516-9adf-9277cc4b4185	INFO	accessToken:  eyJraWQiOiJ6SldrNXZ1XC81TUVXc2Z1YU5ZZVNPK1wvYStEaENOR3QrUmxKc2xVOThGakk9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJlNDE4MjRhOC0yMDYxLTcwOTMtYzk0OS02MDJkYjkyNmI1OTEiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfVEw0N09MY3lJIiwiY29nbml0bzp1c2VybmFtZSI6ImU0MTgyNGE4LTIwNjEtNzA5My1jOTQ5LTYwMmRiOTI2YjU5MSIsIm9yaWdpbl9qdGkiOiJhMjI3NzA1ZS04MDE1LTQzNTctOTQ1ZS0wNjQzZTFhN2NkYWQiLCJhdWQiOiIxbGkxYWEwNTU4aDF1Z3Nob29jNGw2OXI2biIsImV2ZW50X2lkIjoiYjMzOWRkMDItMGQ4Ny00MmQyLTk4YzItZDBjNWM5OGY4NjMxIiwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE3MTk5NTIyNDIsImV4cCI6MTcxOTk1NTg0MiwiaWF0IjoxNzE5OTUyMjQyLCJqdGkiOiIzZTNmYzNhMC00NDAxLTQ4YTQtOTJlNS00ZDI5ODMxMzE0MTYiLCJlbWFpbCI6Im1hcmsrM0B3aHl3ZS50ZWNoIn0.g2yhDxNt5zHJujLyi3gxu4nbKm_p1kraRxTjJ87k0s6-WkKV0yInpkdrA169qVpkdAb5klV6qEj3jSEkRpVIMlZ1Yn-5bJ20My9ICjHjxXUHVGKDCjPzff7aRl1osf9N2ReTsqQGg-a-Id1F1v65ZoiFgmwyK9w2T0bKVg2lFuu7H2k5tn66a26guObUNNIRnUpI3zQt2EsbF7oVIAZExXdygPX7eQ_wm-guK1iNcI6QPTMRB_VtSopRA_MjpXi_-HXVex7udixiyZBBIhtKARhMWcGxpYivS7qXb-Un_D_CFJZCLJHEmRjeNyTMTXnJpSraXRqvk9RMEqvICJ6scA
2024-07-02T20:31:49.870Z	08809faf-f0eb-4516-9adf-9277cc4b4185	ERROR	Error: NotAuthorizedException: Invalid Access Token
    at Request.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/json.js:61:27)
    at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:686:14)
    at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9)
    at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:688:12)
    at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:116:18) {
  code: 'NotAuthorizedException',
  time: 2024-07-02T20:31:49.868Z,
  requestId: '6037625f-e0e0-44dd-a7bc-38ecfc928891',
  statusCode: 400,
  retryable: false,
  retryDelay: 97.74827225944466
}

Codeium at 1:39 pm, Jul 2, 2024

The error you encountered, NotAuthorizedException: Invalid Access Token, indicates that the access token sent to Cognito is not valid. This can happen for a few reasons:

  1. The Token is Expired: Access tokens from Cognito typically have a 1-hour validity period. If the token was obtained more than an hour before it was used in the request, it will be rejected.
  2. Token Type Mismatch: The error log indicates that you're using an ID token (token_use: id in the decoded token) while the AWS SDK call to getUser expects an access token. Cognito issues three types of tokens upon authentication: ID token, access token, and refresh token. For operations like getUser, you should use the access token, not the ID token.
  3. The Token is Altered or Incorrect: Ensure that the token hasn't been modified or truncated when being set or sent in the request.

Given the error message and the token type indicated in the log, it seems like the issue is related to using the wrong token type (ID token instead of access token). Here's how you can address and troubleshoot the issue:

Correcting the Token Usage

Ensure that you're using the access token when making requests that require authentication with AWS Cognito. In your client-side code, when storing tokens received from the sign-in response, make sure you correctly identify and store the access token. When you send this token in your requests to your backend, specifically set the Authorization header to use the access token.

Client-Side Code Adjustment

When storing tokens:

localStorage.setItem('AccessToken', response.data.AccessToken); // This should store the correct access token

Ensure you're sending the correct token in the Authorization header:

const accessToken = localStorage.getItem('AccessToken');
// Then use `Bearer ${accessToken}` in the Authorization header

Server-Side Code Adjustment

On your server-side (the Lambda function), you're already correctly extracting the token from the Authorization header. Just ensure it's the access token being sent from the client-side:

// This part is correct; just ensure the accessToken variable contains the actual access token
const user = await cognito.getUser({
    AccessToken: accessToken
}).promise();

Verifying Token Type

You can decode the JWT token without verifying its signature client-side (for debugging purposes only) to check if it's indeed an access token. Access tokens and ID tokens have different claims. For example, the token_use claim in an access token is access, whereas in an ID token, it's id.

Next Steps

  • Ensure your client-side code sends the access token, not the ID token.
  • Verify the validity period of the token to ensure it's not expired.
  • If you continue to face issues, you might want to log the actual token you're sending to getUser and manually decode it using a JWT decoder (like jwt.io) to inspect its content and validity.

Switching between token types when you fix the cookies issue should be straightforward if you structure your token handling correctly, focusing on using the appropriate token for each type of request.


User at 1:43 pm, Jul 2, 2024

The signIn in the client-side useAuth saves all the tokens it receives into localStorage along with an expiresAt value:

 const signIn = async (username: string, password: string) => {
  if (!username || !password) {
   throw new Error('All fields are required');
  }
  try {
   const response = await axios.post(`${API_URL}${ENDPOINTS.api.SignIn}`, { username, password });
   // The API should set an HttpOnly cookie directly, no need to handle tokens here
   console.log('Login successful:', response.data);
   setIsLoggedIn(true);
   localStorage.setItem('AccessToken', response.data.AccessToken);
   localStorage.setItem('RefreshToken', response.data.RefreshToken);
   localStorage.setItem('IdToken', response.data.IdToken);
   localStorage.setItem(
    'ExpiresAt',
    new Date().getTime() + (parseInt(response.data.ExpiresIn, 10) * 1000).toString(10)
   );
   return response.data; // Return session data if needed
  } catch (error) {
   console.error('Login failed:', error);
   throw error;
  }
 };

So the problem is in the getUser, I think:

 const getUser = async () => {
  try {
   // Retrieve the ID Token from localStorage
   const idToken = localStorage.getItem('IdToken');
   if (!idToken) {
    throw new Error('No ID token found');
   }

   // Include the ID Token in the Authorization header
   const authConfig = {
    ...config, // Spread the existing config to keep content-type
    headers: {
     ...config.headers, // Spread any existing headers
     'Authorization': `Bearer ${idToken}` // Add the Authorization header
    }
   };

   // Make the request with the ID Token
   const response = await axios.get(`${API_URL}${ENDPOINTS.api.GetUser}`, authConfig);
   return response.data; // Contains username, attributes, etc.
  } catch (error) {
   console.error('Error fetching current user:', error);
   return null;
  }
 };

I think you should have used the AccessToken. Is that correct?


Codeium at 1:43 pm, Jul 2, 2024

Yes, you're correct. The issue in the getUser function arises from using the ID token instead of the access token. For operations that require user authentication and authorization, such as retrieving user-specific data, you should use the access token issued by Cognito. The ID token is typically used for identity information and is intended to be sent to your application backend for authentication purposes, whereas the access token grants access to authorized resources and should be used in API calls that access or modify the user's data based on their permissions.

To fix the getUser function, you should modify it to use the AccessToken stored in localStorage:

const getUser = async () => {
  try {
    // Retrieve the Access Token from localStorage
    const accessToken = localStorage.getItem('AccessToken');
    if (!accessToken) {
      throw new Error('No access token found');
    }

    // Include the Access Token in the Authorization header
    const authConfig = {
      ...config, // Spread the existing config to keep content-type
      headers: {
        ...config.headers, // Spread any existing headers
        'Authorization': `Bearer ${accessToken}` // Add the Authorization header with the Access Token
      }
    };

    // Make the request with the Access Token
    const response = await axios.get(`${API_URL}${ENDPOINTS.api.GetUser}`, authConfig);
    return response.data; // Contains username, attributes, etc.
  } catch (error) {
    console.error('Error fetching current user:', error);
    return null;
  }
};

By using the access token, you ensure that the request to the getUser endpoint is correctly authorized, allowing the backend to retrieve and return the user-specific data. This change should resolve the NotAuthorizedException: Invalid Access Token error you encountered, as the correct token type is now being used for the operation.


User at 1:53 pm, Jul 2, 2024

Ok, Cognito is working and returning a userId:

2024-07-02T20:47:27.205Z	f9a2a1b8-8ef2-416a-b1bb-67deb07c85d5	INFO	userId:  e41824a8-2061-7093-c949-602db926b591
2024-07-02T20:47:27.297Z	f9a2a1b8-8ef2-416a-b1bb-67deb07c85d5	ERROR	Error: NoSuchKey: The specified key does not exist.
    at Request.extractError (/var/runtime/node_modules/aws-sdk/lib/services/s3.js:711:35)
    at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:686:14)
    at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9)
    at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:688:12)
    at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:116:18) {
  code: 'NoSuchKey',
  region: null,
  time: 2024-07-02T20:47:27.297Z,
  requestId: 'M8M97BACSKYG73CX',
  extendedRequestId: 'GRX1tTp8eAnk6OmCtcc4qWVfpFCH7rR04GPE/BqL1zPXU9vC1/Z+6AgEbT9HIP3UnZLYwYq8o4A=',
  cfId: undefined,
  statusCode: 404,
  retryable: false,
  retryDelay: 33.126325112742315
}

The error is because the S3 key does not exist.

Can we modify the getUser lambda to execute a S3 head function to check if the key exists and fail gracefully?


Codeium at 1:53 pm, Jul 2, 2024

To modify your getUser Lambda function to gracefully handle the case where the S3 object does not exist, you can use the headObject method provided by the AWS SDK for S3. This method retrieves metadata about the specified object without returning the object itself, which makes it a good choice for checking if an object exists without consuming unnecessary resources.

If the headObject method does not throw an error, it means the object exists. If it throws an error with the code NoSuchKey, the object does not exist. You can use this behavior to conditionally handle the presence or absence of the S3 object.

Here's how you can integrate the headObject check into your function:

import * as AWS from 'aws-sdk';

const s3 = new AWS.S3({ region: 'us-east-1' });
const cognito = new AWS.CognitoIdentityServiceProvider({ region: 'us-east-1' });

export const handler = async (event: any) => {
  try {
    console.log('event.headers: ', event.headers);
    const authorizationHeader = event.headers['Authorization'] || event.headers['authorization'];
    if (!authorizationHeader) {
      throw new Error('Authorization header is missing');
    }

    const match = authorizationHeader.match(/^Bearer (.*)$/);
    if (!match || match.length < 2) {
      throw new Error('Bearer token not found');
    }
    const accessToken = match[1];
    console.log('accessToken: ', accessToken);

    const user = await cognito.getUser({
      AccessToken: accessToken
    }).promise();

    const userId = user.Username;
    console.log('userId: ', userId);
    const objectKey = `user_data/${userId}/_index.md`;

    try {
      await s3.headObject({
        Bucket: 'handterm',
        Key: objectKey
      }).promise();

      // If headObject succeeds, the object exists, and you can proceed to get the object
      const s3Response = await s3.getObject({
        Bucket: 'handterm',
        Key: objectKey
      }).promise();

      const fileContent = s3Response.Body.toString('utf-8');
      console.log('fileContent: ', fileContent);

      return {
        statusCode: 200,
        body: JSON.stringify({ userId: userId, content: fileContent }),
      };
    } catch (headErr) {
      if (headErr.code === 'NoSuchKey') {
        // If the object does not exist, return a custom message
        return {
          statusCode: 404,
          body: JSON.stringify({ message: 'Profile does not exist yet' }),
        };
      }
      // If it's a different kind of error, rethrow it
      throw headErr;
    }
  } catch (err) {
    console.error('Error:', err);
    return { statusCode: 500, body: JSON.stringify(err) };
  }
};

This updated function first attempts to check if the object exists using headObject. If the object exists, it proceeds to retrieve it with getObject. If the object does not exist (NoSuchKey error), it returns a custom message indicating that the profile does not exist yet. This approach allows your function to fail gracefully when the specified S3 object is not found.


User at 1:56 pm, Jul 2, 2024

How can I fix this type of problem?

'headErr' is of type 'unknown'.ts(18046)
 Error (TS18046)  | 
headErr is of type  unknown 

Codeium at 1:57 pm, Jul 2, 2024

The TypeScript error headErr is of type 'unknown' occurs because TypeScript, by default, types catch clause variables as unknown. This is a safer typing, forcing you to explicitly check or cast the type of the error before you use it. This helps ensure that you're not inadvertently assuming that every error is an object of a certain shape, like an AWS error object, which might not always be the case.

To fix this issue, you can use a type assertion to narrow the unknown type to a more specific type. In the case of AWS SDK errors, they typically have a code property you can use to identify the type of error. Here's how you can modify your catch block to handle this:

} catch (headErr: unknown) {
  // First, assert headErr as an AWS error with a code property
  const error = headErr as AWS.AWSError;

  if (error.code === 'NoSuchKey') {
    // Handle the NoSuchKey error case
    return {
      statusCode: 404,
      body: JSON.stringify({ message: 'Profile does not exist yet' }),
    };
  } else {
    // If it's a different kind of error, you might want to log or handle it differently
    console.error('S3 headObject error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ message: 'Internal Server Error' }),
    };
  }
}

In this snippet, headErr is cast to AWS.AWSError, which is a type that includes a code property among others. This allows TypeScript to recognize that error.code is a valid property access.

However, it's important to note that this type assertion tells TypeScript "I'm sure headErr is of this type," which bypasses TypeScript's type checking. If there's any chance that headErr might not be an AWS.AWSError (for example, if other kinds of errors could be thrown in the try block), you should add additional runtime checks to ensure safety. For instance, you might want to check whether error actually has a code property before trying to access it:

if ('code' in error && error.code === 'NoSuchKey') {
  // Handle the NoSuchKey error case
}

This additional check makes your code more robust and guards against potential runtime errors if the error object doesn't match the expected shape.


User at 2:21 pm, Jul 2, 2024

Ok, now I need another server-side lambda function called setUser. It will do the same thing as getUser but it will write to the same S3 file.

These to files are both part of an AWS HTTP API CDK deployment, so it might be good to think about abstracting out the auth process. I don't know if that should be a custom authorizer.


Codeium at 2:21 pm, Jul 2, 2024

Creating a new server-side Lambda function named setUser to write data to the same S3 file used by getUser can follow a similar pattern to your existing getUser function, with the primary difference being the S3 operation used to write data instead of reading it. Before diving into the setUser function, let's briefly discuss the idea of abstracting out the authentication process.

Abstracting Authentication

Abstracting the authentication process in AWS Lambda functions, especially for APIs built with AWS API Gateway, can indeed be done using a custom authorizer. A custom authorizer is a separate Lambda function that handles authentication and authorization for your API Gateway resources. It can validate tokens (like JWTs from Cognito), ensuring that your business logic Lambdas (getUser, setUser, etc.) focus on their core responsibilities rather than handling auth directly.

Using a custom authorizer has several benefits:

  • Centralized Authentication Logic: You manage your authentication logic in one place, making it easier to maintain and update.
  • Reusability: The same authorizer can be used across multiple APIs or routes.
  • Security: Your backend services only get invoked after successful authentication, providing an additional layer of security.

For your scenario, implementing a custom authorizer would involve creating a new Lambda function that checks the Authorization header of incoming requests, validates the token using Cognito, and returns an IAM policy allowing or denying the request. This setup would require adjustments in your API Gateway configuration to use the authorizer.

setUser Function

Now, let's look at the setUser Lambda function. It will receive data (e.g., user profile information) and write it to an S3 object. Assuming authentication has been handled (either in the function itself or via a custom authorizer), here's a basic implementation:

import * as AWS from 'aws-sdk';

const s3 = new AWS.S3({ region: 'us-east-1' });
const cognito = new AWS.CognitoIdentityServiceProvider({ region: 'us-east-1' });

export const handler = async (event: any) => {
  try {
    // Assuming event.body contains the data to write, and userId is passed as part of the request
    // For simplicity, not showing the authentication part here
    const { userId, content } = JSON.parse(event.body);

    const objectKey = `user_data/${userId}/_index.md`;

    // Write the content to the S3 object
    await s3.putObject({
      Bucket: 'handterm',
      Key: objectKey,
      Body: content, // Make sure this is in the correct format (e.g., a string or Buffer)
    }).promise();

    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'User data updated successfully' }),
    };
  } catch (err) {
    console.error('Error:', err);
    return { statusCode: 500, body: JSON.stringify(err) };
  }
};

Moving Forward

  • If you decide to proceed with a custom authorizer for token validation, you'd remove the authentication logic from both getUser and setUser, relying on the authorizer to ensure that only authenticated requests reach these functions.
  • The example assumes you're directly passing a userId and the content to update in the body of the request. Adjust this as necessary to fit your application's data model and security requirements, especially considering how you identify users and authorize updates to their data.
  • Remember, the structure and logic might further evolve based on how you implement the authentication abstraction and what data setUser is expected to handle.

User at 2:39 pm, Jul 2, 2024

Ok, how would I write the Authorizer?

And how would I assign it in the CDK?

  // Define the HTTP API
  const httpApi = new HttpApi(this, 'HandTermApi', {
   apiName: 'HandTermService',
   description: 'This service serves authentication requests.',
   // CORS configuration if needed
   corsPreflight: {
    allowOrigins: ['http://localhost:5173', 'https://handterm.com'],
    allowMethods: [CorsHttpMethod.GET, CorsHttpMethod.POST, CorsHttpMethod.OPTIONS],
    allowHeaders: ['Content-Type', 'Authorization'],
    allowCredentials: true,
   },
  });

  // Cognito Identity Pool
  const identityPool = new cognito.CfnIdentityPool(this, 'HandTermIdentityPool', {
   identityPoolName: 'HandTermIdentityPool',
   allowUnauthenticatedIdentities: false,
   cognitoIdentityProviders: [{
    clientId: userPoolClient.userPoolClientId,
    providerName: userPool.userPoolProviderName,
   }],
  });

  // S3 Bucket for User Logs
  const logsBucket = new s3.Bucket(this, 'HandTermHistoryBucket', {
   bucketName: ENDPOINTS.aws.s3.bucketName,
   blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
   encryption: s3.BucketEncryption.S3_MANAGED,
  });

  // Lambda Execution Role
  const lambdaExecutionRole = new iam.Role(this, 'LambdaExecutionRole', {
   assumedBy: new iam.CompositePrincipal(
    new iam.ServicePrincipal('lambda.amazonaws.com'),
    new iam.ServicePrincipal('edgelambda.amazonaws.com')
   ),
   managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
    iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3FullAccess'),
   ],
  });

Codeium at 2:40 pm, Jul 2, 2024

To create and use a custom authorizer with AWS CDK for your HTTP API in API Gateway, you can follow these steps. This example demonstrates how to set up a Lambda-based custom authorizer that validates JWT tokens with Cognito.

Step 1: Write the Custom Authorizer Lambda Function

First, you need a Lambda function that acts as the custom authorizer. This function will extract the JWT token from the Authorization header, validate it, and return an IAM policy document indicating whether the invocation is allowed.

// Lambda Authorizer
import * as AWS from 'aws-sdk';
const cognito = new AWS.CognitoIdentityServiceProvider({ region: 'us-east-1' });

exports.handler = async (event) => {
  try {
    const token = event.headers.authorization.split(' ')[1]; // Assuming "Bearer <token>"
    // Here you would validate the token. For simplicity, this example skips the actual validation.
    // For JWT, you could use a library like jsonwebtoken (npm install jsonwebtoken) to validate the token against your Cognito User Pool.
    
    // If the token is valid, return an allow policy
    return generatePolicy('user', 'Allow', event.methodArn);
  } catch (error) {
    console.log('Authorization failed', error);
    throw 'Unauthorized'; // Important: throwing 'Unauthorized' will signal API Gateway to deny access
  }
};

// Helper function to generate an IAM policy
function generatePolicy(principalId, effect, resource) {
  const authResponse = {};
  authResponse.principalId = principalId;
  if (effect && resource) {
    const policyDocument = {};
    policyDocument.Version = '2012-10-17';
    policyDocument.Statement = [];
    const statementOne = {};
    statementOne.Action = 'execute-api:Invoke';
    statementOne.Effect = effect;
    statementOne.Resource = resource;
    policyDocument.Statement[0] = statementOne;
    authResponse.policyDocument = policyPolicyDocument;
  }
  return authResponse;
}

In a production environment, you would validate the token properly by checking its signature and claims according to your authentication requirements.

Step 2: Deploy the Custom Authorizer with CDK

Assuming you already have the infrastructure setup code as you've shown, you can add the custom authorizer Lambda function to your CDK stack:

import * as lambda from '@aws-cdk/aws-lambda';
import * as apigatewayv2 from '@aws-cdk/aws-apigatewayv2';
import * as apigatewayv2authorizers from '@aws-cdk/aws-apigatewayv2-authorizers';
import * as iam from '@aws-cdk/aws-iam';

// Define the custom authorizer Lambda function
const customAuthorizerLambda = new lambda.Function(this, 'CustomAuthorizerLambda', {
  runtime: lambda.Runtime.NODEJS_14_X,
  handler: 'authorizer.handler',
  code: lambda.Code.fromAsset('path/to/your/authorizer/lambda'),
  role: lambdaExecutionRole, // Assuming lambdaExecutionRole is defined as in your example
});

// Create an HTTP API as before
const httpApi = new apigatewayv2.HttpApi(this, 'HandTermApi', {
  apiName: 'HandTermService',
  description: 'This service serves authentication requests.',
  corsPreflight: {
    allowOrigins: ['http://localhost:5173', 'https://handterm.com'],
    allowMethods: [apigatewayv2.CorsHttpMethod.GET, apigatewayv2.CorsHttpMethod.POST, apigatewayv2.CorsHttpMethod.OPTIONS],
    allowHeaders: ['Content-Type', 'Authorization'],
    allowCredentials: true,
  },
});

// Define the custom authorizer for the HTTP API
const jwtAuthorizer = new apigatewayv2authorizers.HttpLambdaAuthorizer({
  authorizerName: 'JWTAuthorizer',
  handler: customAuthorizerLambda,
  identitySource: ['$request.header.Authorization'],
});

// Add routes that require authorization
httpApi.addRoutes({
  path: '/yourPath',
  methods: [apigatewayv2.HttpMethod.GET], // Adjust methods as needed
  integration: new integrations.HttpLambdaIntegration('YourIntegration', yourLambdaFunctionHandler),
  authorizer: jwtAuthorizer,
});

This example assumes you have a Lambda function (yourLambdaFunctionHandler) that you want to protect with the custom authorizer. Make sure to replace 'path/to/your/authorizer/lambda' and other placeholders with your actual values.

Final Considerations

This setup adds a layer of authentication to your HTTP API, using a custom authorizer to validate JWT tokens against your authentication mechanism (e.g., Cognito). Remember that the actual token validation logic in the custom authorizer Lambda function must be implemented according to your authentication provider's requirements.

Adjust the identitySource and other parameters as necessary


User at 3:48 pm, Jul 2, 2024

In the authorizer, I get a bunch of errors like this in the generatePolicy

Property 'principalId' does not exist on type '{}'.ts(2339)
 Error(TS2339)  | 
Property principalId does not exist on type {}.

TypeScript expects everything to be typed.

In the CDK, it looks like the latest docs (2.147.3) say to declare it more like this:

import { HttpJwtAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers';

const issuer = 'https://test.us.auth0.com';
const authorizer = new HttpJwtAuthorizer('DefaultAuthorizer', issuer, {
  jwtAudience: ['3131231'],
});

const api = new apigwv2.HttpApi(this, 'HttpApi', {
  defaultAuthorizer: authorizer,
  defaultAuthorizationScopes: ['manage:books'],
});

Codeium at 3:49 pm, Jul 2, 2024

Based on the TypeScript error you've encountered, it seems like you need a more defined structure for the authResponse object within your generatePolicy function. This structure is crucial when working with TypeScript, as it requires explicit definitions for object properties.

Before addressing the TypeScript issue, let's briefly touch on the use of HttpJwtAuthorizer from AWS CDK, as mentioned in your follow-up.

Using HttpJwtAuthorizer with AWS CDK

When using AWS CDK (version 2.x onwards), HttpJwtAuthorizer offers a straightforward way to add JWT authorizers to your HTTP APIs, especially if you're integrating with known identity providers like Auth0, Amazon Cognito, or others that issue JWT tokens.

Your example shows how to set up an HttpJwtAuthorizer with an issuer URL and audience, which are typical JWT validation parameters. This approach is cleaner for standard JWT validation scenarios and significantly reduces the amount of boilerplate code you need to write and maintain.

Here’s a bit more detail on how you might integrate it, assuming you're using an external JWT provider like Auth0, Cognito, etc.:

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { HttpApi } from 'aws-cdk-lib/aws-apigatewayv2';
import { HttpJwtAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers';

class MyStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // JWT token issuer
    const issuer = 'https://cognito-idp.{region}.amazonaws.com/{userPoolId}';
    
    // Audience (Client ID)
    const audience = ['yourAudienceValue'];

    // Define JWT Authorizer
    const jwtAuthorizer = new HttpJwtAuthorizer('JWTAuthorizer', issuer, {
      jwtAudiences: audience,
    });

    // Define your HTTP API
    const api = new HttpApi(this, 'HttpApi', {
      defaultAuthorizer: jwtAuthorizer,
      defaultAuthorizationScopes: ['scopesIfRequired'], // Define scopes if necessary
    });

    // Add more configurations as necessary
  }
}

Addressing the TypeScript Error

For the TypeScript error within your custom authorizer's generatePolicy function, if you still need to use a custom Lambda authorizer for more complex authorization scenarios not covered by HttpJwtAuthorizer, you should define a type for your response object. Here's a quick fix:

interface AuthResponse {
  principalId: string;
  policyDocument?: {
    Version: string;
    Statement: Array<{
      Action: string;
      Effect: string;
      Resource: string;
    }>;
  };
}

// Helper function to generate an IAM policy
function generatePolicy(principalId: string, effect: string, resource: string): AuthResponse {
  const authResponse: AuthResponse = {
    principalId: principalId,
  };

  if (effect && resource) {
    authResponse.policyDocument = {
      Version: '2012-10-17',
      Statement: [{
        Action: 'execute-api:Invoke',
        Effect: effect,
        Resource: resource,
      }],
    };
  }

  return authResponse;
}

This explicit typing ensures that TypeScript knows exactly what structure authResponse is supposed to have, eliminating the mentioned error.

Conclusion

For most standard JWT validation needs, leveraging HttpJwtAuthorizer from AWS CDK simplifies the setup and maintenance of your authorizers. It's a powerful tool that integrates seamlessly with AWS services and scales with your application. For custom authorization logic that goes beyond JWT validation (like additional permission checks or complex business rules), implementing a custom Lambda authorizer and addressing TypeScript's type safety requirements would be the way to go.


User at 5:37 pm, Jul 2, 2024

I think you are saying that if I use the HttpJwtAuthorizer, then I no long need my custom Jwt authorizer. Is that correct? That would be wonderful.

Then, later, if I want to create a working HttpOnly Cookies custom authorizer, I can attach it to the HTTP API in a similar fashion at that point, perhaps. That would also be nice.

With the HttpJwtAuthorizer, my setUser will be passed a userId which can be parsed like this:

  const { userId, content } = JSON.parse(event.body);

Codeium at 5:38 pm, Jul 2, 2024

Yes, that's correct. Using HttpJwtAuthorizer means you can rely on a built-in mechanism provided by AWS CDK for JWT validation, eliminating the need for a custom authorizer Lambda function for most standard JWT use cases. This simplifies your setup and maintenance significantly. The HttpJwtAuthorizer is designed to work out of the box with JWT tokens, making it straightforward to secure your HTTP APIs with minimal additional coding.

If you later decide to use HttpOnly cookies for authorization or need more complex authorization logic beyond what JWT tokens provide, you can indeed create a custom authorizer Lambda and attach it to your HTTP API in a similar fashion. This custom authorizer would then be responsible for extracting the session or authentication details from the cookies or any other part of the request and making the decision to allow or deny the request.

For working with HttpJwtAuthorizer and ensuring your Lambda functions, like setUser, receive the necessary context (such as a userId), you typically rely on the claims present within the validated JWT token. However, parsing the userId directly from the request body as you've shown:

const { userId, content } = JSON.parse(event.body);

is a viable approach when the client application is responsible for sending this data in the request's payload. Just ensure that the client is authorized and trusted to provide accurate and secure userId information, as the setUser function appears to use this userId to perform operations that could affect user data.

For scenarios where the userId should be extracted from the JWT token itself (a common practice for APIs secured with JWTs, where the userId or a similar identifier is embedded within the token), you would parse the token within your Lambda function to extract the userId. This operation is usually done automatically when using a custom Lambda authorizer, as it can inject these details into the request context after validating the token. However, with HttpJwtAuthorizer and the standard AWS setup, your Lambda function receives a request context enriched with the principal's identity, but parsing the token is not required as the authorizer handles validation.

In summary, using HttpJwtAuthorizer simplifies JWT validation, and for custom authentication or authorization needs, including those based on HttpOnly cookies, a custom Lambda authorizer can be developed and integrated as needed. Your approach to handling userId and content in setUser based on the request body is dependent on your application's specific authentication and data submission logic.


User at 5:57 pm, Jul 2, 2024

How do I do string interpolation in TypeScript?

  const issuer = "https://cognito-idp.${region}.amazonaws.com/${userPoolId}";

When I said I could use this:

const { userId, content } = JSON.parse(event.body);

I was under the impression that the JwtAuthorizer wouldi ntercept the the HTTP Request, parse the JWT and use it to look up the user, and pass the userId in the event to my getUser and setUser lambda handlers.

Is that not the case?


Codeium at 5:57 pm, Jul 2, 2024

String Interpolation in TypeScript

To perform string interpolation in TypeScript (and JavaScript), you need to use backticks (`) instead of quotes (" " or ' '). This allows you to embed expressions within your string literals using ${expression} syntax. Here's how you can correctly interpolate your string with region and userPoolId:

const region = 'your-region'; // Example: us-east-1
const userPoolId = 'your-userpool-id'; // Example: us-east-1_ABCdeFGHi
const issuer = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`;

JWT Authorizer and User Context

Regarding the HttpJwtAuthorizer and how it handles user context: The HttpJwtAuthorizer validates the JWT token and allows the request to proceed if the token is valid. However, it does not automatically parse the JWT and insert user details, such as userId, into the Lambda event that your getUser or setUser functions receive.

Instead, the authorizer can pass context information based on the content of the JWT token, but you need to configure which parts of the token are included in the request context. In AWS API Gateway v2 (for HTTP APIs), you can access these details via the $context.authorizer.jwt.claims property in integration templates, but this is more relevant when using direct integrations or when manipulating request/response templates, not directly within Lambda functions.

To have the userId or similar information passed directly to your Lambda functions, you have a few options:

  1. Extract userId in the Client: Continue sending userId in the request body or as a path/query parameter, as you're already considering. This approach is straightforward but relies on the client to include this information.

  2. Decode the JWT in Lambda: If the JWT is sent with the request (e.g., in the Authorization header), your Lambda function can decode it to extract the userId or other claims. This approach requires additional coding and handling within each Lambda function.

  3. Custom Lambda Authorizer: Create a custom authorizer Lambda function that extracts the userId from the JWT and passes it to your backend Lambda functions in the context or as a transformed part of the request. This approach centralizes the parsing logic but requires more setup.

  4. Payload Format Version 2.0: When using HTTP APIs, you can configure the payload format version to 2.0, which includes a $context.authorizer.jwt field containing the JWT claims directly in the Lambda event object. This requires configuring your route integration to use the 2.0 payload format and accessing the claims programmatically within your Lambda function.

Unfortunately, the process is not as automatic as it might initially seem. You need to decide on an approach that best fits your security model and application architecture. For many applications, decoding the JWT within the Lambda function (if the token is forwarded with the request) or using a custom authorizer for more granular control is a common solution.


User at 6:01 pm, Jul 2, 2024

How do I do this:

"Custom Lambda Authorizer: Create a custom authorizer Lambda function that extracts the userId from the JWT and passes it to your backend Lambda functions in the context or as a transformed part of the request. This approach centralizes the parsing logic but requires more setup."


Codeium at 6:02 pm, Jul 2, 2024

Creating a custom Lambda authorizer that extracts the userId from a JWT and passes it to your backend Lambda functions involves a few steps. Here's a breakdown of the process:

1. Write the Custom Authorizer Lambda Function

This Lambda function will be triggered by API Gateway upon receiving an API request. It should validate the JWT, extract the userId, and return an IAM policy along with the context that includes the userId.

First, ensure you have a way to validate the JWT. This might involve checking the signature against a secret or a public key, and possibly using a library such as jsonwebtoken for Node.js.

Here is a simplified example of what the Lambda function might look like:

import { verify } from 'jsonwebtoken';

// Dummy secret for JWT verification - replace with your mechanism, possibly fetching the public key from Cognito
const secret = 'yourSecretOrPublicKey';

exports.handler = async (event) => {
  try {
    const token = event.authorizationToken.split('Bearer ')[1];
    if (!token) throw new Error('Token not found');

    // Verify and decode the token. Replace 'secret' with your key or method of retrieving it.
    const decoded = verify(token, secret);
    const userId = decoded.sub; // Assuming 'sub' contains the userId. Adjust based on your token's claims.

    // Generate allow policy to forward the request to the backend Lambda
    return generatePolicy(userId, 'Allow', event.methodArn, { userId });
  } catch (error) {
    console.error('Authorization failed', error);
    throw 'Unauthorized'; // API Gateway specific: returning 'Unauthorized' string will deny access
  }
};

function generatePolicy(principalId, effect, resource, context) {
  return {
    principalId,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [{
        Action: 'execute-api:Invoke',
        Effect: effect,
        Resource: resource
      }]
    },
    context // Additional context to pass to the backend Lambda (contains the userId here)
  };
}

2. Deploy the Custom Authorizer Lambda Function

Use AWS CLI, AWS Management Console, or infrastructure as code tools like AWS CDK or Serverless Framework to deploy your Lambda function.

3. Configure API Gateway to Use the Custom Authorizer

Using AWS Management Console:

  • Open the Amazon API Gateway console.
  • Create or navigate to your existing HTTP API.
  • Under "Routes", select the route you want to secure with the authorizer.
  • Choose "Attach authorizer" and then "Create and attach an authorizer".
  • Fill in the details of your authorizer, including the Lambda function you created.
  • Save your settings.

Using AWS CDK:

If you're working with an HTTP API, the AWS CDK doesn't directly support Lambda authorizers for HTTP APIs as of my last knowledge update. You would typically use JWT authorizers directly as shown in previous examples. For REST APIs, you can attach Lambda authorizers directly.

For more advanced scenarios or to work around limitations, you might consider deploying the API Gateway resources through CloudFormation resources within CDK or adjusting your application to fit within the supported feature set.

4. Access userId in Your Backend Lambda Functions

With the custom authorizer in place, your backend Lambda function can access the userId from the context:

exports.handler = async (event) => {
  const userId = event.requestContext.authorizer.userId;
  // Now you can use userId in your business logic
};

This setup centralizes your JWT validation and user identification in one place, making your overall backend more secure and easier to manage. Adjustments may be needed based on the specifics of your JWT structure and the AWS services you're using.

⚠️ **GitHub.com Fallback** ⚠️