Creating a authenticated lambda function using Cognito, API gateway, and the serverless framework.

0

Hi, I am trying to create a serverless.yml that instantiates a cognito serverless that protects a private API using JWT tokens.

Here is a snippet of the yml file file I have so far.

service: my-service
frameworkVersion: '3'
provider:
  name: aws
  stage: dev
  region: us-west-2
  profile: my-api
  runtime: python3.9
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - cognito-idp:AdminInitiateAuth
            - cognito-idp:AdminRespondToAuthChallenge
            - cognito-idp:AdminUserGlobalSignOut
            - cognito-idp:AdminUpdateUserAttributes
            - cognito-idp:ListUsers

functions:
  private_function:
    handler: private_function.lambda_handler

    events:
      - httpApi: 
          path: '/private_function'
          method: 'GET'
      -authorizer: CognitoAuthorizer #This is the part I think I am having trouble with
package:
  exclude:
    - node_modules/**
    - venv/**

plugins:
  - serverless-python-requirements

resources:
  Resources:
    CognitoUserPool:
      Type: AWS::Cognito::UserPool
      Properties:
        UserPoolName: ${self:service}_${sls:stage}_user_pool
        UsernameAttributes:
          - email
       
    CognitoUserPoolClient:
      Type: AWS::Cognito::UserPoolClient
      Properties:
        ClientName: ${self:service}_${sls:stage}_client
        UserPoolId:
          Ref: CognitoUserPool
        ExplicitAuthFlows:
          - ADMIN_NO_SRP_AUTH
        GenerateSecret: false

    CognitoAuthorizer:
      Type: AWS::ApiGatewayV2::Authorizer
      Properties:
        Name: CognitoAuthorizer
        ApiId:
            Fn::Sub: ${self:service}-ApiGatewayId-${self:provider.stage}
        IdentitySource: ["$request.header.Authorization"]
        AuthorizationScopes:
          - email
        AuthorizerType: JWT
        JwtConfiguration:
          issuer: 
            Fn::Sub: arn:aws:cognito-idp:${self:provider.region}:${AWS::AccountId}:userpool/${self:resources.Resources.CognitoUserPool.Properties.UserPoolName}

What I want to have is all of the following attributes created in the said yml file.

  1. Have the CognitoUserPool and Client to be created automatically
  2. Have an Authorizer built automatically
  3. Dictate which functions require said aurthizoer
  4. Have Cognito's Host web authentication redirect to a specified domain.
  5. Be able to pass 'Authorizer': Id_token into the API request that is protected by the authorizer.

If someone can help me with fixing the yml, that would be extremely helpful.

EDIT: Added based on Narnacle's advice

service: my-service
frameworkVersion: '3'
provider:
  name: aws
  stage: dev
  region: us-west-2
  profile: my-api
  runtime: python3.9
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - cognito-idp:AdminInitiateAuth
            - cognito-idp:AdminRespondToAuthChallenge
            - cognito-idp:AdminUserGlobalSignOut
            - cognito-idp:AdminUpdateUserAttributes
            - cognito-idp:ListUsers

functions:
  private_function:
    handler: private_function.lambda_handler

    events:
      - httpApi: 
          path: '/private_function'
          method: 'GET'
          authorizer:
            name: CognitoAuthorizer
            identitySourceHeader: Authorization #Maybe should be "type: jwt"
package:
  exclude:
    - node_modules/**
    - venv/**

plugins:
  - serverless-python-requirements

resources:
  Resources:
    CognitoUserPool:
      Type: AWS::Cognito::UserPool
      Properties:
        UserPoolName: ${self:service}_${sls:stage}_user_pool
        UsernameAttributes:
          - email
       
    CognitoUserPoolClient:
      Type: AWS::Cognito::UserPoolClient
      Properties:
        ClientName: ${self:service}_${sls:stage}_client
        UserPoolId:
          Ref: CognitoUserPool
        ExplicitAuthFlows:
          - ADMIN_NO_SRP_AUTH
        GenerateSecret: false
    MyApi:
      Type: AWS::ApiGateway::Api
      Properties:
        Name: MyApi
    CognitoAuthorizer:
      Type: AWS::ApiGateway::Authorizer
      Properties:
        Name: CognitoAuthorizer
        RestApiId: 
          Fn::GetAtt:
            - MyApi # Replace with the actual logical name of your API Gateway resource
            - Id
        Type: COGNITO_USER_POOLS
        IdentitySource: method.request.header.Authorization
        ProviderARNs:
          - Fn::Sub: arn:aws:cognito-idp:${self:provider.region}:${AWS::AccountId}:userpool/${self:resources.Resources.CognitoUserPool.Properties.UserPoolName}

However, I am still getting the Warning

Warning: Invalid configuration encountered
  at 'functions.dev_tool_model_call.events.0.httpApi.authorizer': unrecognized property 'identitySourceHeader'
Learn more about configuration validation here: http://slss.io/configuration-validation

and the error

Error: Event references not configured authorizer 'CognitoAuthorizer'
3 Answers
0

To set up AWS Cognito for user authentication and JWT authorization in your Serverless Framework serverless.yml file, you'll need to make a few modifications and additions. Here's an updated version of your serverless.yml file with comments explaining each section:

service: my-service
frameworkVersion: '3'
provider:
  name: aws
  stage: dev
  region: us-west-2
  profile: my-api
  runtime: python3.9
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - cognito-idp:AdminInitiateAuth
            - cognito-idp:AdminRespondToAuthChallenge
            - cognito-idp:AdminUserGlobalSignOut
            - cognito-idp:AdminUpdateUserAttributes
            - cognito-idp:ListUsers

functions:
  private_function:
    handler: private_function.lambda_handler
    events:
      - httpApi: 
          path: '/private_function'
          method: 'GET'
          authorizer:
            name: CognitoAuthorizer # Reference to your custom authorizer
            identitySource: "$request.header.Authorization" # The source of JWT tokens in the request

package:
  exclude:
    - node_modules/**
    - venv/**

plugins:
  - serverless-python-requirements

resources:
  Resources:
    CognitoUserPool:
      Type: AWS::Cognito::UserPool
      Properties:
        UserPoolName: ${self:service}_${sls:stage}_user_pool
        UsernameAttributes:
          - email
       
    CognitoUserPoolClient:
      Type: AWS::Cognito::UserPoolClient
      Properties:
        ClientName: ${self:service}_${sls:stage}_client
        UserPoolId:
          Ref: CognitoUserPool
        ExplicitAuthFlows:
          - ADMIN_NO_SRP_AUTH
        GenerateSecret: false

    CognitoAuthorizer:
      Type: AWS::ApiGateway::Authorizer # Use AWS::ApiGateway instead of AWS::ApiGatewayV2
      Properties:
        Name: CognitoAuthorizer
        RestApiId: # Add your API Gateway RestApiId here
        Type: COGNITO_USER_POOLS
        IdentitySource: "method.request.header.Authorization" # Adjust the source
        ProviderARNs:
          - Fn::Sub: arn:aws:cognito-idp:${self:provider.region}:${AWS::AccountId}:userpool/${self:resources.Resources.CognitoUserPool.Properties.UserPoolName}

  # Define a CognitoUserPoolDomain resource to configure the Cognito Hosted UI
  CognitoUserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: your-cognito-domain # Replace with your desired domain name
      UserPoolId:
        Ref: CognitoUserPool

Here are the changes and explanations:

  1. In the private_function definition, I added an authorizer property to specify that this function should use the CognitoAuthorizer you defined.
  2. I replaced the AWS::ApiGatewayV2::Authorizer type with AWS::ApiGateway::Authorizer. The authorizer type depends on the version of AWS API Gateway you are using. If you are using the first version of API Gateway, use AWS::ApiGateway::Authorizer.
  3. I added a RestApiId property to the CognitoAuthorizer definition. You need to specify the API Gateway ID to which this authorizer is associated.
  4. I modified the IdentitySource in the CognitoAuthorizer to match the expected format of the JWT token source.
  5. Added a CognitoUserPoolDomain resource to configure the Cognito Hosted UI with your desired domain name. Replace your-cognito-domain with the actual domain you want to use.

With these modifications, your serverless.yml should create a Cognito User Pool, User Pool Client, and a Cognito Authorizer for JWT tokens. It also associates the authorizer with the private_function, which requires authentication to access.

answered 7 months ago
  • Hi @Narnacle. I tried implementing the changes you made however

    1. identitySource is not showing up as an attribute: " Warning: Invalid configuration encountered at 'functions.dev_tool_model_call.events.0.httpApi.authorizer': unrecognized property 'identitySource'
    2. I get an error 'Event references not configured authorizer 'CognitoAuthorizer''

    Also for the RestApiID I want it to be set automatically to the one created by the private function: to do that I found resources that suggested using RestApiId: Fn::GetAtt: - ApiGatewayRestApi - RootResourceId

0

I apologize for the confusion. It seems there was an error in my previous response regarding the identitySource property. The correct property for specifying the source of JWT tokens in an HTTP event is identitySourceHeader. Here's the corrected serverless.yml snippet:

functions:
  private_function:
    handler: private_function.lambda_handler
    events:
      - httpApi: 
          path: '/private_function'
          method: 'GET'
          authorizer:
            name: CognitoAuthorizer
            identitySourceHeader: Authorization # Specify the source header for JWT tokens

Regarding the RestApiId, you can use the Fn::GetAtt function to retrieve the RestApiId dynamically. Here's how you can modify the CognitoAuthorizer section to use Fn::GetAtt:

CognitoAuthorizer:
  Type: AWS::ApiGateway::Authorizer
  Properties:
    Name: CognitoAuthorizer
    RestApiId: 
      Fn::GetAtt:
        - ApiGatewayRestApi # Replace with the actual logical name of your API Gateway resource
        - Id
    Type: COGNITO_USER_POOLS
    IdentitySource: method.request.header.Authorization
    ProviderARNs:
      - Fn::Sub: arn:aws:cognito-idp:${self:provider.region}:${AWS::AccountId}:userpool/${self:resources.Resources.CognitoUserPool.Properties.UserPoolName}

Make sure to replace "ApiGatewayRestApi" with the actual logical name of your API Gateway resource in the Fn::GetAtt function.

With these changes, your CognitoAuthorizer will use the RestApiId dynamically, and the identitySourceHeader property is set correctly.

answered 7 months ago
  • Hi Narnacle, thank you for helping me. However, I am still getting a warning and an error: Error: Event references not configured authorizer 'CognitoAuthorizer' I updated the question to include the changes I made.

0

I apologize for the confusion earlier. It appears that the Serverless Framework version you are using doesn't support the identitySourceHeader property directly in the serverless.yml file.

To set up a Cognito User Pool Authorizer for your HTTP API, you can use a separate AWS CloudFormation template (a .yaml or .json file) to define your API Gateway and the authorizer, and then reference that template in your serverless.yml. Here's how you can do it:

  1. Create a separate CloudFormation template, for example, api-gateway-template.yaml, with the following content:
AWSTemplateFormatVersion: '2010-09-09'
Description: API Gateway and Cognito User Pool Authorizer
Resources:
  MyApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: MyApi

  CognitoAuthorizer:
    Type: AWS::ApiGateway::Authorizer
    Properties:
      Name: CognitoAuthorizer
      RestApiId: !Ref MyApi
      Type: COGNITO_USER_POOLS
      IdentitySource: method.request.header.Authorization
      ProviderARNs:
        - Fn::Sub: arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${CognitoUserPoolId}

In this template:

  • MyApi is your API Gateway.
  • CognitoAuthorizer is the Cognito User Pool Authorizer. Note that ${CognitoUserPoolId} is a placeholder that you need to replace with the actual User Pool ID.
  1. In your serverless.yml, reference the CloudFormation template and pass the User Pool ID as a variable:
service: my-service
frameworkVersion: '3'
provider:
  name: aws
  stage: dev
  region: us-west-2
  profile: my-api
  runtime: python3.9
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - cognito-idp:AdminInitiateAuth
            - cognito-idp:AdminRespondToAuthChallenge
            - cognito-idp:AdminUserGlobalSignOut
            - cognito-idp:AdminUpdateUserAttributes
            - cognito-idp:ListUsers

functions:
  private_function:
    handler: private_function.lambda_handler

    events:
      - httpApi: 
          path: '/private_function'
          method: 'GET'
          authorizer:
            arn: !GetAtt CognitoAuthorizer.Arn

package:
  exclude:
    - node_modules/**
    - venv/**

plugins:
  - serverless-python-requirements

resources:
  - ${file(api-gateway-template.yaml)} # Include the external CloudFormation template

# Add a variable to pass the Cognito User Pool ID
custom:
  cognitoUserPoolId: ${self:resources.CognitoUserPool.Properties.UserPoolName}

In this serverless.yml file:

  • We include the external CloudFormation template using ${file(api-gateway-template.yaml)}.
  • We pass the Cognito User Pool ID as a custom variable using ${self:resources.CognitoUserPool.Properties.UserPoolName}.

This separation allows you to define the Cognito Authorizer separately and then reference it in your Serverless service.

answered 7 months ago

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Guidelines for Answering Questions