2024 07 02 aws cdk implementing custom authorizer for http api with lambda - bizkite-co/handterm GitHub Wiki


original file name: Codeium Chat - NElckcUYos5AF9Ea0pOyfn9br72TtJYv.md summary: The user is working on an AWS CDK project for an HTTP API and Lambda functions. They need help implementing a custom authorizer that validates a Bearer token using Amazon Cognito and retrieves the user's ID. The conversation goes on to discuss the steps involved in creating the custom authorizer Lambda function, defining it in the CDK stack, and integrating it into the HTTP API. The user encounters errors related to incorrect code snippets provided earlier and seeks assistance in resolving these issues. date: 2024-07-02T18:08

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 6:08 pm, Jul 2, 2024

I have a AWS CDK project that builds out a HTTP API and several Lambda functions:

// cdk/lib/cdk-stack.ts
import { ENDPOINTS } from '../lambda/cdkshared/endpoints';
import {
 aws_cognito as cognito,
 aws_s3 as s3,
 aws_lambda as lambda,
 aws_iam as iam,
 aws_apigatewayv2 as apigatewayv2,
 aws_apigatewayv2_integrations as apigatewayv2Integrations,
 App,
 CfnOutput,
 Stack,
 aws_apigatewayv2_authorizers as apigatewayv2authorizers,
 StackProps
} from "aws-cdk-lib";
import { Construct } from 'constructs';
import { HttpMethod, HttpApi, CorsHttpMethod } from 'aws-cdk-lib/aws-apigatewayv2';
import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; // This path is illustrative and likely incorrect
import { HttpJwtAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers';

const nodeRuntime = lambda.Runtime.NODEJS_16_X;
const region = "us-east-1";

export class HandTermCdkStack extends Stack {
 constructor(
  scope: Construct,
  id: string,
  props?: StackProps
 ) {
  super(scope, id, props);
  const allowHeaders = [
   'Content-Type',
   'X-Amz-Date',
   'Authorization',
   'X-Api-Key',
   'X-Requested-With',
   'sec-ch-ua',
   'sec-ch-ua-mobile',
   'sec-ch-ua-platform'
  ];
  // Cognito User Pool
  const userPool = new cognito.UserPool(this, 'HandTermUserPool', {
   userPoolName: 'HandTermUserPool',
   selfSignUpEnabled: true,
   userVerification: {
    emailSubject: 'Verify your email for our app!',
    emailBody: 'Hello {username}, Thanks for signing up to our app! Your verification code is {####}',
    emailStyle: cognito.VerificationEmailStyle.CODE,
   },
   signInAliases: {
    email: true
   },
   passwordPolicy: {
    minLength: 8,
    requireUppercase: true,
    requireLowercase: true,
    requireDigits: true,
    requireSymbols: true,
   },
   autoVerify: { email: true }
  });

  // TODO: Remove this before production. This is only to make signup easier during development
  const preSignupLambda = new lambda.Function(this, 'PreSignupLambda', {
   runtime: nodeRuntime,
   handler: 'preSignup.handler',
   code: lambda.Code.fromAsset('lambda/authentication'),
  });
  userPool.addTrigger(cognito.UserPoolOperation.PRE_SIGN_UP, preSignupLambda);

  // Cognito User Pool Client
  const userPoolClient = userPool.addClient('AppClient', {
   authFlows: {
    userSrp: true,
    userPassword: true // Enable USER_PASSWORD_AUTH flow
   },
   generateSecret: false,
   // Add your API Gateway endpoint URL to the list of callback URLs
  });

  // 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'),
   ],
  });

  // 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, {
   jwtAudience: audience,
  });

  // Define the HTTP API
  const httpApi = new HttpApi(this, 'HandTermApi', {
   apiName: 'HandTermService',
   defaultAuthorizer: jwtAuthorizer,
   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,
   },
  });

  const signUpLambda = new lambda.Function(this, 'SignUpFunction', {
   runtime: nodeRuntime,
   handler: 'signUp.handler',
   role: lambdaExecutionRole,
   code: lambda.Code.fromAsset('lambda/authentication'),
   environment: {
    COGNITO_APP_CLIENT_ID: userPoolClient.userPoolClientId,
   }
  });
  const signUpIntegration = new HttpLambdaIntegration('signup-integration', signUpLambda);
  httpApi.addRoutes({
   path: ENDPOINTS.api.SignUp,
   methods: [HttpMethod.POST],
   integration: signUpIntegration,
  })

  const signInLambda = new lambda.Function(this, 'SignInFunction', {
   runtime: nodeRuntime,
   handler: 'signIn.handler',
   role: lambdaExecutionRole,
   code: lambda.Code.fromAsset('lambda/authentication'),
   environment: {
    COGNITO_APP_CLIENT_ID: userPoolClient.userPoolClientId,
   }
  });
  httpApi.addRoutes({
   path: ENDPOINTS.api.SignIn,
   methods: [HttpMethod.POST],
   integration: new HttpLambdaIntegration(
    'post-user-signin',
    signInLambda
   ),
  })

  const changePasswordLambda = new lambda.Function(this, 'ChangePasswordFunction', {
   runtime: nodeRuntime,
   handler: 'changePassword.handler',
   role: lambdaExecutionRole,
   code: lambda.Code.fromAsset('lambda/authentication'),
   environment: {
    COGNITO_APP_CLIENT_ID: userPoolClient.userPoolClientId,
   }
  });
  const changePasswordIntegration = new HttpLambdaIntegration('change-password-integration', changePasswordLambda);
  httpApi.addRoutes({
   path: ENDPOINTS.api.ChangePassword,
   methods: [HttpMethod.POST],
   integration: changePasswordIntegration,
  })

  const getUserLambda = new lambda.Function(this, 'GetUserFunction', {
   runtime: nodeRuntime,
   handler: 'getUser.handler',
   role: lambdaExecutionRole,
   code: lambda.Code.fromAsset('lambda/authentication'),
   environment: {
    COGNITO_APP_CLIENT_ID: userPoolClient.userPoolClientId,
   }
  });
  const getUserIntegration = new HttpLambdaIntegration('get-user-integration', getUserLambda);
  httpApi.addRoutes({
   path: ENDPOINTS.api.GetUser,
   methods: [HttpMethod.GET],
   integration: getUserIntegration,
  })

  // Outputs
  new CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId });
  new CfnOutput(this, 'UserPoolClientId', { value: userPoolClient.userPoolClientId });
  new CfnOutput(this, 'IdentityPoolId', { value: identityPool.ref });
  new CfnOutput(this, 'BucketName', { value: logsBucket.bucketName });
  new CfnOutput(this, 'ApiEndpoint', { value: httpApi.url || '' });
 }
}

const app = new App();
new HandTermCdkStack(app, 'HandTermCdkStack');

I have to change the JWT authorizer to a custom authorizer that parses the Bearer token and passes it to Cognito to validate the user and return the userId.

I will use the Cognito userId in an S3 bucket path to store and retrieve user data in a separate setUser and getUser set of lambda functions.

I need you to help me write that custom authorizer and to integrate it into the CDK.


Codeium at 6:09 pm, Jul 2, 2024

To create a custom authorizer that validates a Bearer token using Amazon Cognito and retrieves the userId, you will first need to write a Lambda function that performs the authentication logic. This Lambda will be used as the authorizer for your HTTP API in AWS CDK.

Here's an outline of the steps we'll follow:

  1. Write the Custom Authorizer Lambda Function: This Lambda function will use the AWS SDK to interact with Cognito for token validation.
  2. Define the Lambda Function in CDK: Add the authorizer Lambda function to your CDK stack.
  3. Create an HTTP API Lambda Authorizer: Use the authorizer Lambda function for your HTTP API.

Step 1: Custom Authorizer Lambda Function

This Lambda function will act as a custom authorizer. It will extract the Bearer token from the Authorization header, validate it using the Cognito GetCallerIdentity API, and return the principalId (userId) if validation is successful.

Create a new file for the authorizer Lambda, for example, lambda/authentication/authorizer.ts. The TypeScript code for the authorizer might look something like this:

import { APIGatewayTokenAuthorizerEvent, APIGatewayAuthorizerResult } from 'aws-lambda';
import { CognitoIdentityServiceProvider } from 'aws-sdk';

const cognito = new CognitoIdentityServiceProvider();

export const handler = async (event: APIGatewayTokenAuthorizerEvent): Promise<APIGatewayAuthorizerResult> => {
    const token = event.authorizationToken.split("Bearer ")[1];
    const userPoolId = process.env.COGNITO_USER_POOL_ID;

    try {
        const response = await cognito.getUser({
            AccessToken: token
        }).promise();

        const userId = response.Username;

        return generatePolicy(userId, 'Allow', event.methodArn);
    } catch (error) {
        console.error(error);
        return generatePolicy('user', 'Deny', event.methodArn);
    }
};

function generatePolicy(principalId: string, effect: string, resource: string): APIGatewayAuthorizerResult {
    return {
        principalId,
        policyDocument: {
            Version: '2012-10-17',
            Statement: [{
                Action: 'execute-api:Invoke',
                Effect: effect,
                Resource: resource
            }]
        }
    };
}

Make sure you have the AWS SDK installed in your Lambda's package.json and that it's configured to be packaged with your deployment.

Step 2: Define the Lambda Function in CDK

In your CDK stack (cdk/lib/cdk-stack.ts), define the authorizer Lambda function:

const authorizerLambda = new lambda.Function(this, 'AuthorizerFunction', {
    runtime: nodeRuntime,
    handler: 'authorizer.handler',
    code: lambda.Code.fromAsset('lambda/authentication'),
    environment: {
        COGNITO_USER_POOL_ID: userPool.userPoolId
    }
});

Step 3: Create an HTTP API Lambda Authorizer

You will then replace the JWT authorizer with this custom Lambda authorizer in your HTTP API definition:

import { HttpLambdaAuthorizer, HttpLambdaAuthorizerProps, HttpAuthorizerType } from 'aws-cdk-lib/aws-apigatewayv2-authorizers';

const authorizerProps: HttpLambdaAuthorizerProps = {
    authorizerName: 'CustomAuthorizer',
    handler: authorizerLambda,
    identitySource: ['$request.header.Authorization'],
    type: HttpAuthorizerType.TOKEN,
};

const lambdaAuthorizer = new HttpLambdaAuthorizer(authorizerProps);

httpApi.addRoutes({
    path: ENDPOINTS.api.GetUser,
    methods: [HttpMethod.GET],
    integration: getUserIntegration,
    authorizer: lambdaAuthorizer,
});

// Repeat for other routes as needed

Final Step: Update the Environment and Permissions

Make sure your custom authorizer Lambda has permission to call Cognito APIs and access the User Pool. You've already set the environment variable COGNITO_USER_POOL_ID in the authorizer Lambda definition. Ensure the IAM role associated with the Lambda has the necessary permissions to call getUser on Cognito.

This approach allows you to validate the Bearer token with Cognito, retrieve the userId, and use it in your application logic, including setting and getting data from an S3 bucket path specific to the user.


User at 6:22 pm, Jul 2, 2024

Ok, I am getting just a couple of errors that I don't know how to handle:

Object literal may only specify known properties, and 'handler' does not exist in type 'HttpLambdaAuthorizerProps'.ts(2353)
Property 'TOKEN' does not exist on type 'typeof HttpAuthorizerType'.ts(2339)

And

Expected 2-3 arguments, but got 1.ts(2554)
lambda.d.ts(62, 29): An argument for 'handler' was not provided.
⚠ Error(TS2554)  | 
Expected 2-3 arguments, but got 1.
Codeium: Explain Problem
(alias) new HttpLambdaAuthorizer(id: string, handler: lambda.IFunction, props?: HttpLambdaAuthorizerProps): apigatewayv2authorizers.HttpLambdaAuthorizer
import HttpLambdaAuthorizer
Initialize a lambda authorizer to be bound with HTTP route.
@param id — The id of the underlying construct
@param pool — The lambda function handler to use for authorization
@param props — Properties to configure the authorizer

Codeium at 6:23 pm, Jul 2, 2024

The errors you're encountering are due to mismatches between the code snippets provided earlier and the actual API of the AWS CDK for TypeScript. Let's address each error:

Error 1: Object Literal May Only Specify Known Properties

The HttpLambdaAuthorizerProps object does not have a handler property. Instead, when you initialize an HttpLambdaAuthorizer, the second argument should be the Lambda function you wish to use as the authorizer.

Error 2: Property 'TOKEN' Does Not Exist

It seems there was a mistake in referring to HttpAuthorizerType.TOKEN, which does not exist. As of my last update, the AWS CDK's HttpAuthorizerType might not directly support a 'TOKEN' identifier. If you are using an authorizer, you typically specify its type when constructing it, often as part of the properties or in specific methods for different types of authorizers (JWT, Lambda, etc.).

Error 3: Expected 2-3 Arguments

This error is related to the incorrect instantiation of an HttpLambdaAuthorizer. The correct way to initialize it requires an ID and the Lambda function, with the properties passed as a separate argument.

Corrected Example

Given the errors, let's correct the instantiation of the HttpLambdaAuthorizer. Since the detailed API might vary slightly depending on the CDK version, the corrected approach (according to the errors) should look more like this:

import { HttpLambdaAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers';
import { IFunction } from 'aws-cdk-lib/aws-lambda';

// Ensure authorizerLambda is of type IFunction
const authorizerLambda: IFunction = new lambda.Function(this, 'AuthorizerFunction', {
    runtime: nodeRuntime,
    handler: 'authorizer.handler',
    code: lambda.Code.fromAsset('lambda/authentication'),
    environment: {
        COGNITO_USER_POOL_ID: userPool.userPoolId
    }
});

// Define the authorizer with the correct constructor arguments
const lambdaAuthorizer = new HttpLambdaAuthorizer('CustomAuthorizer', authorizerLambda, {
    identitySource: ['$request.header.Authorization']
});

// Associate the authorizer with your API route
httpApi.addRoutes({
    path: ENDPOINTS.api.GetUser,
    methods: [HttpMethod.GET],
    integration: getUserIntegration,
    authorizer: lambdaAuthorizer,
});

Please note:

  • The HttpLambdaAuthorizer constructor takes an ID ('CustomAuthorizer'), the Lambda function (authorizerLambda), and an optional properties object where you can specify the identitySource among other settings.
  • Ensure that your CDK library versions are up to date, as APIs can change between versions. The above code is based on the typical usage patterns seen in AWS CDK for creating custom Lambda authorizers for HTTP APIs.
  • The identitySource and other configurations should match your requirements for how the token is passed and validated.

Adjusting these parts of your code according to the actual APIs available in your version of the AWS CDK should resolve the errors.


User at 7:22 pm, Jul 2, 2024

Somehow, I seem to have broken my CDK.

This is what I see in the CloudWatch logs for signIn now:

INIT_REPORT Init Duration: 1492.89 ms	Phase: invoke	Status: error	Error Type: Runtime.ImportModuleError
START RequestId: d23d5a58-ce65-467a-ad85-f90aeb5b2cd6 Version: $LATEST
Unknown application error occurred
Runtime.ImportModuleError
END RequestId: d23d5a58-ce65-467a-ad85-f90aeb5b2cd6
REPORT RequestId: d23d5a58-ce65-467a-ad85-f90aeb5b2cd6	Duration: 1513.65 ms	Billed Duration: 1514 ms	Memory Size: 128 MB	Max Memory Used: 13 MB	
INIT_START Runtime Version: nodejs:16.v36	Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:30606511772fdc0f370d2d81900da3f8c7cd524f718237c6bebd61e4e1e853fa
2024-07-03T02:20:07.141Z	undefined	ERROR	Uncaught Exception 	
{
    "errorType": "Runtime.ImportModuleError",
    "errorMessage": "Error: Cannot find module 'signIn'\nRequire stack:\n- /var/runtime/index.mjs",
    "stack": [
        "Runtime.ImportModuleError: Error: Cannot find module 'signIn'",
        "Require stack:",
        "- /var/runtime/index.mjs",
        "    at _loadUserApp (file:///var/runtime/index.mjs:1087:17)",
        "    at async Object.UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)",
        "    at async start (file:///var/runtime/index.mjs:1282:23)",
        "    at async file:///var/runtime/index.mjs:1288:1"
    ]
}

Codeium at 7:22 pm, Jul 2, 2024

The error log from CloudWatch indicates that the Lambda runtime is unable to find the signIn module that your Lambda function is attempting to import or execute. This typically happens due to one of the following reasons:

  1. Incorrect Handler Path: The handler configuration for your Lambda function points to a file or module that doesn't exist in the deployment package. Ensure that the handler string in your Lambda function definition in CDK matches the file structure in your deployment package. For a handler defined as 'signIn.handler', AWS expects to find a file named signIn.js (or signIn.ts if you are using TypeScript and compiling to JavaScript) at the root of your deployment package with an exported function named handler.

  2. Deployment Package Issue: The deployment package (ZIP file) that was uploaded to AWS Lambda does not contain the signIn file. This can happen if the build process did not correctly include the file or if the wrong directory was packaged. Ensure that your build and deployment scripts correctly compile (if necessary) and include all the required files in the Lambda deployment package.

  3. File Naming or Export Error: There might be a typo in the file name or the file might not be correctly exporting a handler function. Ensure that your signIn module correctly exports a function named handler, like so:

    exports.handler = async (event) => {
        // Your signIn logic here
    };

To diagnose and fix the issue, you can:

  • Verify the Handler Configuration: Check your CDK script that defines the signIn Lambda function to ensure the handler property correctly points to the signIn file and its exported function. If your file is in a subdirectory, you need to include this in the path, e.g., 'subdirectory/signIn.handler'.

  • Inspect the Deployment Package: After building your project, inspect the generated ZIP file or the contents of the directory being deployed to Lambda to ensure the signIn file is present and correctly named.

  • Check the Build Process: If you're using TypeScript or another transpiled language, ensure that your build process is correctly compiling your files and that the output directory is the one being deployed to Lambda. You might have a build script in your package.json that looks something like "build": "tsc" for a TypeScript project. Ensure this script runs successfully and outputs the files to the expected location.

  • Test Locally: If possible, use tools like AWS SAM CLI or the aws-lambda-nodejs construct from the AWS CDK to test your Lambda function locally before deploying. This can help catch errors early.

If you've checked all the above and the issue persists, you might want to manually deploy a simple Lambda function through the AWS Console with the same handler configuration to ensure there isn't a deeper configuration issue.


User at 7:26 pm, Jul 2, 2024

Ok, wait a second, I didn't move the signIn.ts since I created it and I've deployed it many times, but what I did do is delete all the .js files that were spread throughout the cdk/, and I added this to the cdk/tsconfig.json compilerOptions:

  "outDir": "./cdk.out",

I bet that cause the problem.

How should the compiled output be organized for CDK projects?


Codeium at 7:27 pm, Jul 2, 2024

Yes, modifying the outDir in your tsconfig.json to ./cdk.out and deleting the .js files would indeed cause the problem you're experiencing. The AWS Lambda service expects the handler files to be in specific locations as defined in your Lambda function configurations. If the compiled JavaScript files are not in the expected location, Lambda will not be able to find your handlers.

For AWS CDK projects, especially those that include AWS Lambda functions written in TypeScript, it's crucial to organize the compiled output in a way that aligns with the Lambda function's handler configuration. Here's how you can organize and manage this:

Recommended Structure

  1. Lambda Functions: Keep your Lambda function source files (signIn.ts and others) in a dedicated directory, separate from your CDK infrastructure code. This is beneficial for clarity and helps manage the build process. For example, you could have a directory structure like this:

    /project-root
      /cdk
        /lib
        /bin
        cdk.json
        tsconfig.json
      /lambda
        signIn.ts
        ...
      tsconfig.json
    
  2. Compilation Output: When you compile your TypeScript Lambda functions, the JavaScript output should ideally be placed in a directory that's easily packageable for deployment to AWS Lambda. You can have an outDir for your Lambda functions separate from the outDir for your CDK code. This can be managed by having a separate tsconfig.json for your Lambda functions if they're not already separately configured. For example, your Lambda tsconfig.json might look something like this:

    {
      "compilerOptions": {
        "outDir": "./dist",
        ...
      }
    }

    And you'd adjust your CDK tsconfig.json to compile CDK TypeScript files without including Lambda source files, possibly like this:

    {
      "compilerOptions": {
        "outDir": "./cdk.out",
        ...
      },
      "include": [
        "lib/**/*.ts",
        "bin/**/*.ts"
      ]
    }
  3. CDK Lambda Function Definitions: When you define your Lambda functions in CDK, ensure you're pointing to the correct path for the compiled JavaScript files. For example:

    const signInLambda = new lambda.Function(this, 'SignInFunction', {
      runtime: lambda.Runtime.NODEJS_14_X, // Or whichever runtime you're using
      handler: 'signIn.handler', // Adjusted to where the compiled JS files are located
      code: lambda.Code.fromAsset(path.join(__dirname, '../../lambda/dist')),
    });

    In this example, path.join(__dirname, '../../lambda/dist') dynamically calculates the correct path to your dist directory containing the compiled JavaScript code, assuming the CDK stack is defined in /cdk/lib.

Conclusion

  • Separate your Lambda function source code (TypeScript) from your CDK code.
  • Use different outDir configurations for Lambda compilation and CDK compilation.
  • Make sure your CDK Lambda function definitions point to the correct location of the compiled JavaScript code.

This setup ensures that your compiled Lambda function code is organized in a way that AWS Lambda expects, based on the handler configurations in your CDK code.


User at 7:47 pm, Jul 2, 2024

Ok, I just removed the "outDir" and redeployed, for now.

I am console logging the whole event in the getUser and I don't see any clientId

2024-07-03T02:41:08.383Z	79882627-a74e-4b85-926d-9d89e4c62f77	INFO	event.headers:  {
  version: '2.0',
  routeKey: 'GET /getUser',
  rawPath: '/getUser',
  rawQueryString: '',
  headers: {
    accept: 'application/json, text/plain, */*',
    'accept-encoding': 'gzip, deflate, br, zstd',
    'accept-language': 'en-US,en;q=0.9',
    authorization: 'Bearer eyJraWQiOiJ2cG52eTk5ZVZlVTFJMU5YUWlcL3RlVmc5NSszbVwvc3pQUjloNzJQMm53ZFU9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJlNDE4MjRhOC0yMDYxLTcwOTMtYzk0OS02MDJkYjkyNmI1OTEiLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtZWFzdC0xLmFtYXpvbmF3cy5jb21cL3VzLWVhc3QtMV9UTDQ3T0xjeUkiLCJjbGllbnRfaWQiOiIxbGkxYWEwNTU4aDF1Z3Nob29jNGw2OXI2biIsIm9yaWdpbl9qdGkiOiI1MDg0ZjlmOC1mNjRkLTQzOGUtYWZlZS04YjBlNWEwYmViYjUiLCJldmVudF9pZCI6ImJmZmIyZWEyLWI2NTktNGE2Zi1iODhmLWE1MTJjNjk0YzhjMiIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYXdzLmNvZ25pdG8uc2lnbmluLnVzZXIuYWRtaW4iLCJhdXRoX3RpbWUiOjE3MTk5NzM4NTksImV4cCI6MTcxOTk3NzQ1OSwiaWF0IjoxNzE5OTczODU5LCJqdGkiOiIzNzI0YTIyNC1lYjM3LTRhZTUtYTViYS1iODY4OTg0ZGZmZWQiLCJ1c2VybmFtZSI6ImU0MTgyNGE4LTIwNjEtNzA5My1jOTQ5LTYwMmRiOTI2YjU5MSJ9.ZLz-EhSE3ty4P6dJQMmQ0YMGAkmS6VEo2EbzO5fIv8J6dYvJnuUJsl7A7QXoGQQ9uQ6bYDNqLAilmRKIiXsiN-fNYKM_Yfv7mxXIII7Y7dl3viH7ke0XEJErpRPdA_lTcj7G_Hku4otpJ8Yts7Ve3JnKhY76Aze2NAWNwWa0_WYFLlmjlH0fxrOixjnNwoxCHCqFx2aqm_3anPJtPjTEdcu-r6LN3eDKEaWd676SRnhM8IRGbhiQqN-T-FHw621nZzHnavY-CAcfgL1tsZO-w_JALj9HWYCpKYBxa29BuOkqpdwcYY3aY8oIDLHREqeJQPTzoRZ_xQwTkAMjDzZydg',
    '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-6684ba42-0ee205bd1ef547ab1e97dfca',
    'x-forwarded-for': '104.32.208.200',
    'x-forwarded-port': '443',
    'x-forwarded-proto': 'https'
  },
  requestContext: {
    accountId: '318364255844',
    apiId: 'x7jzrjfuv8',
    authorizer: { lambda: null },
    domainName: 'x7jzrjfuv8.execute-api.us-east-1.amazonaws.com',
    domainPrefix: 'x7jzrjfuv8',
    http: {
      method: 'GET',
      path: '/getUser',
      protocol: 'HTTP/1.1',
      sourceIp: '104.32.208.200',
      userAgent: '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'
    },
    requestId: 'aUIKbgoGIAMEPhw=',
    routeKey: 'GET /getUser',
    stage: '$default',
    time: '03/Jul/2024:02:41:06 +0000',
    timeEpoch: 1719974466483
  },
  isBase64Encoded: false
}

I thought the custom authorizer was supposed to inject the clientId into the request event.


Codeium at 7:47 pm, Jul 2, 2024

The custom authorizer you've implemented is expected to pass the userId (or any other identity-related information it extracts during the authorization process) to the downstream Lambda functions, such as your getUser Lambda. However, this data is not automatically placed in a clientId field; instead, it is typically added to the context or policy document returned by the authorizer.

When a custom authorizer (Lambda authorizer) is invoked, it should return a policy document and an optional context. The context can contain any key-value pairs that you wish to forward to the Lambda function that is being authorized. These key-value pairs are then accessible within the event.requestContext.authorizer object of the downstream Lambda's event.

In your authorizer function, you need to explicitly add the userId or any other data you wish to pass along into the context of the policy document. Here's an example of how you might modify the generatePolicy function to include this:

function generatePolicy(principalId: string, effect: string, resource: string, context: {[key: string]: any}): APIGatewayAuthorizerResult {
    return {
        principalId: principalId,
        policyDocument: {
            Version: '2012-10-17',
            Statement: [{
                Action: 'execute-api:Invoke',
                Effect: effect,
                Resource: resource
            }]
        },
        context // This line adds the context to the policy document
    };
}

And when you call this function:

const userId = response.Username;
const context = { userId: userId };
return generatePolicy(userId, 'Allow', event.methodArn, context);

Then, in your getUser Lambda function, you can access this userId from the event.requestContext.authorizer object:

exports.handler = async (event: any) => {
    console.log('UserId:', event.requestContext.authorizer.userId);
    // Rest of your function logic...
};

Please ensure your custom authorizer is correctly configured in the API Gateway and that it is indeed being invoked. You should also ensure that the authorizer is returning the expected response, including the context with the userId.

If the event.requestContext.authorizer object does not contain the expected data, verify that the policy document and context are correctly formatted in your authorizer's response. Also, check that the API Gateway configuration has not been altered to bypass or ignore the authorizer for the getUser route.


User at 7:57 pm, Jul 2, 2024

Ok, well my getUser starts like this:

export const handler = async (event: any) => {
  try {
    console.log('event: ', event);

And the log shows there is no policy document in that event.

And that makes me wonder why we are generating a new policy document? What is that used for.

I think my server-side lambdas will just write to the bucket based on the userId. I don't think we can create a new access policy for each bucket-key path.

Does that create a new policy on each request? Won't those have to be cleaned up?

Can't I just use the userId as the bucket key and only allow access through the app?


Codeium at 7:57 pm, Jul 2, 2024

The policy document you generate in a custom authorizer for API Gateway is not used for direct access control to resources like S3 buckets. Instead, it's used by API Gateway to grant or deny the invocation of the API endpoint based on the token validation performed by your custom authorizer. The policy document does not create IAM policies or resource policies on AWS resources; it solely controls access to the API Gateway routes.

Here's a breakdown of the concepts to clarify:

API Gateway Custom Authorizer Policy Document

  • Purpose: The policy document returned by a custom authorizer determines whether the caller is allowed to invoke the specified API Gateway method. It's an API Gateway control mechanism, not a broader AWS IAM policy.
  • Per Request Generation: Yes, it's created per request, but it's not an IAM policy that persists in your AWS environment. There's no need for cleanup because it's only used to authorize the current request, not to alter configurations or permissions on AWS resources.
  • Usage: It's used to implement token-based authorization strategies, where the API Gateway relies on your custom logic to decide if a request should proceed.

Handling userId for Access Control

If you're looking to use userId to control access to resources like S3 buckets, here's how you might approach it:

  1. Capture userId from Token: Continue to extract the userId in your custom authorizer (or directly in the Lambda function if you're ensuring your API's security through other means).

  2. Use userId in Your Application Logic: Use the userId to construct S3 bucket key paths, ensuring that users can only access their data. For example, you might use a key pattern like user-data/{userId}/file.txt to segregate user data.

  3. Direct S3 Access Control: Actual access control to S3 resources should be managed through IAM roles and policies attached to your Lambda functions, not through the API Gateway custom authorizer. Your Lambda function should have an IAM role that grants it permissions to write to or read from specific S3 paths, and you enforce user-specific access at the application logic level by constructing keys that include the userId.

Simplified Architecture

Given your requirements, if every server-side Lambda function will operate based on the userId extracted from the token, and assuming all access control logic to AWS resources (like S3) is handled within these Lambda functions, you can simplify your architecture:

  • API Gateway: Authenticates requests using your custom authorizer, which ensures that only valid requests proceed.
  • Lambda Functions: Perform operations based on the userId. The Lambda functions must be designed to only access or modify data that belongs to the authenticated user, using the userId to build resource paths or keys.
  • S3 Bucket Access: Controlled by the IAM role of the Lambda functions, with application logic ensuring access is scoped to data owned by the userId.

This setup relies on your application logic for fine-grained access control, while using AWS's built-in mechanisms (API Gateway for endpoint access, IAM for resource access) for broader access control policies.

No new policies are created on each request in the sense of IAM policies; the policy document from the authorizer is purely for the scope of that request's access to the API Gateway endpoint.


User at 8:01 pm, Jul 2, 2024

OK, but how do I pass the userId that''s already extracted in the custom authorizer to the getUser?


Codeium at 8:01 pm, Jul 2, 2024

To pass the userId extracted in the custom authorizer to the getUser Lambda function, you should utilize the context of the policy document generated by the authorizer. When you add custom key-value pairs to the context of the policy document, these are accessible in the downstream Lambda function through the event.requestContext.authorizer property.

Here’s how you can modify your custom authorizer to include the userId in its context, and then access it in your getUser Lambda function:

Custom Authorizer: Adding userId to the Context

When you generate the policy document in your custom authorizer, include the userId in the context. Assuming you have a function generatePolicy to generate the policy document, you can modify it to accept an additional context parameter. Here is how you can modify your policy generation to include context:

function generatePolicy(principalId, effect, resource, context = {}) {
    return {
        principalId,
        policyDocument: {
            Version: '2012-10-17',
            Statement: [
                {
                    Action: 'execute-api:Invoke',
                    Effect: effect,
                    Resource: resource,
                },
            ],
        },
        context, // Add this line to include the context in the response
    };
}

When calling generatePolicy, add the userId to the context:

const userId = response.Username; // Example of extracting userId
const policy = generatePolicy(userId, 'Allow', event.methodArn, { userId });
return policy;

Accessing userId in getUser Lambda Function

In your getUser Lambda function, you can access the userId from the event.requestContext.authorizer object. Here’s an example:

exports.handler = async (event) => {
    const userId = event.requestContext.authorizer.userId;
    console.log('UserId:', userId);

    // Now, you can use userId to perform operations specific to the user
};

This setup assumes that your API Gateway is correctly configured to use the Lambda authorizer and that the authorizer is returning the policy document along with the context containing userId.

If you’re using AWS CDK to set up your API Gateway and integrations, ensure that the authorizer is associated with the routes that require user-specific handling and that the authorizer’s Lambda function has the necessary permissions to execute and access user data from Cognito or any other identity provider you’re using.

Remember, the exact structure of the event object can vary depending on the type of API (REST API vs. HTTP API) and the payload format version used by your HTTP API. The above example assumes the use of an HTTP API with payload format version 2.0. If you're using a REST API or a different payload format, adjust the way you access the userId accordingly.


User at 8:49 pm, Jul 2, 2024

how to I cat the contents of an S3 file from the AWS CLI?


Codeium at 8:49 pm, Jul 2, 2024

To display (or "cat") the contents of a file stored in Amazon S3 from the AWS CLI, you can use the aws s3 cp command to copy the file from S3 to the standard output (stdout). Here is the basic syntax:

aws s3 cp s3://your-bucket-name/path/to/your-file - 

Note the dash (-) at the end of the command, which specifies that the destination of the copy operation is standard output.

For example, if you have a file named example.txt stored in a bucket named my-bucket in a folder named documents, the command would look like this:

aws s3 cp s3://my-bucket/documents/example.txt -

This command will print the contents of example.txt to your terminal.

Make sure you have the AWS CLI installed and configured with the necessary permissions to access the S3 bucket and file you want to view.

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