API Gateway with Custom Lambda Authorizer and Amazon Cognito by example

Protected_Api

Offloading authentication and authorization logic from your application to AWS API Gateway (APIGW) is a pretty cool feature that a lot of companies are looking into nowadays. It has a few undeniable benefits:

  • Authentication logic consolidation – Authentication/Authorization logic is decoupled from your Application and can be updated/changed in one place.
  • Caching can be configured and in turn it will help to reduce load on your Identity Provider (IdP)
  • Repeatable downstream backend integration protection

Why Custom Lambda Authorizers:

  • Can be used with single or multiple backends
  • Can be used when APIGW is configured as a proxy to other AWS sercices (Like S3 or DynamoDB etc.)
  • Can run from a central “Security” account – Centralizing your AuthN and AuthZ functionality in case of multi-account architecture

A few months ago I was looking for examples of end-to-end implementation of API Gateway with Custom Lambda Authorizer and Amazon Cognito. For some of you that aren’t familiar with Amazon Cognito please read about it here.
In this example we’ll be using Amazon Cognito User Pools as our user directory. In the Enterprize setup I would advise to use Cognito coupled with external IdP (Examples of external IdPs – Okta, AD, Auth0) – I’m planning to write another post on Amazon Cognito with AD integration in one of our next blog posts and look at pros and cons in using Amazon Cognito by itself vs Amazon Cognito with IdP.

Before we start diving into it I’d like to mention a couple of useful blogs and give credit where credit’s due – I recommend reading at least those 2 articles to get a general feel as to why I’m configuring things the way I do and to get a good background info on Custom Authorizers and OAuth 2.0 grants:

Let’s create our resources and see how it all hangs together.

  1. Cognito User Pool – cognito-userpool.yaml
    We will configure a few standard attributes and a custom attribute (custom:upload_folder) as an example of custom attribute, let’s say we want each user to have an “upload folder” – prefix in the S3 bucket.
<span class="hljs-attribute">AWSTemplateFormatVersion</span>: <span class="hljs-string">'2010-09-09'</span>
<span class="hljs-attribute">Description</span>: <span class="hljs-string">'Cognito User Pool Blog Example'</span>

<span class="bash">Mappings:
  <span class="hljs-comment"># Per acc config</span>
  AccountConfig:
    <span class="hljs-string">'123456789123'</span>:
      ExtZone: example.com
      ExtZoneId: ZZZU3243HZZZZZ
      DefaultCert: <span class="hljs-string">"arn:aws:acm:us-east-1:123456789123:certificate/87a97ab2-64a5-4090-9041-1234567891234"</span>

Resources:

<span class="hljs-comment"># BEGIN - user pools</span>
    userPool:
      Type: AWS::Cognito::UserPool
      Properties:
        UserPoolName: !Sub <span class="hljs-string">"<span class="hljs-variable">${AWS::StackName}</span>"</span>
        AutoVerifiedAttributes:
          - email
        UsernameAttributes:
          - email
        AccountRecoverySetting:
          RecoveryMechanisms: 
            - 
              Name: admin_only
              Priority: <span class="hljs-number">1</span>
        VerificationMessageTemplate:
          DefaultEmailOption: CONFIRM_WITH_LINK
        EmailVerificationSubject: Verify your account
        EmailVerificationMessage: Your verification code is <b>{<span class="hljs-comment">####}</b>.</span>
        Policies:
          PasswordPolicy:
            MinimumLength: <span class="hljs-number">10</span>
            RequireLowercase: <span class="hljs-literal">false</span>
            RequireNumbers: <span class="hljs-literal">false</span>
            RequireSymbols: <span class="hljs-literal">false</span>
            RequireUppercase: <span class="hljs-literal">false</span>
        AdminCreateUserConfig:
          AllowAdminCreateUserOnly: <span class="hljs-literal">true</span>
        Schema:
          -
            Name: email
            Mutable: <span class="hljs-literal">true</span>
            Required: <span class="hljs-literal">true</span>
          -
            AttributeDataType: String
            Name: upload_folder
            Mutable: <span class="hljs-literal">true</span>
            Required: <span class="hljs-literal">false</span>
          -
            Name: family_name
            Mutable: <span class="hljs-literal">true</span>
            Required: <span class="hljs-literal">true</span>
          -
            Name: given_name
            Mutable: <span class="hljs-literal">true</span>
            Required: <span class="hljs-literal">true</span>
          -
            Name: preferred_username
            Mutable: <span class="hljs-literal">true</span>
            Required: <span class="hljs-literal">true</span>
<span class="hljs-comment"># END - user pools</span>

<span class="hljs-comment">## BEGIN - user pool domains - Finish testing and add Company's domain config</span>
    <span class="hljs-comment"># Unfortunately I can't create Alias in the Hosted Zone here as CFN doesn't support it yet</span>
    <span class="hljs-comment"># Bear in mind - You will need to have an A record for your root domain in the Public zone</span>
    <span class="hljs-comment"># You still need to create an Alias in your Hosted Zone to point to the cloudfront URL - get it from the console or cli - or do it via custom resource</span>
    <span class="hljs-comment"># https://github.com/aws-cloudformation/aws-cloudformation-coverage-roadmap/issues/356</span>
    <span class="hljs-comment"># https://github.com/aws-cloudformation/aws-cloudformation-coverage-roadmap/issues/58</span>
    userPoolDomain:
      Type: AWS::Cognito::UserPoolDomain 
      Properties:
        UserPoolId: !Ref userPool
        Domain: !Sub
          - <span class="hljs-string">"<span class="hljs-variable">${AWS::StackName}</span>.<span class="hljs-variable">${DNS_ZONE}</span>"</span>
          - {DNS_ZONE: !FindInMap [AccountConfig, !Ref <span class="hljs-string">'AWS::AccountId'</span>, ExtZone]}
        CustomDomainConfig:
             CertificateArn: !FindInMap [AccountConfig, !Ref <span class="hljs-string">'AWS::AccountId'</span>, DefaultCert]
<span class="hljs-comment"># END - user pool domains</span>

Outputs:
  StackName:
    Description: <span class="hljs-string">'Stack name.'</span>
    Value: !Sub <span class="hljs-string">'${AWS::StackName}'</span>

  userPoolId:
    Export:
      Name: !Sub <span class="hljs-string">'${AWS::StackName}-userPoolId'</span>
    Description: User Pool ID
    Value: !Ref userPool
</span>

Let’s deploy it – run this in cli:

REGION=<span class="hljs-string">"ap-southeast-2"</span>
DEPLOY_BUCKET=deployment-templates-bucket-private
STACK_NAME=<span class="hljs-string">"user-pool-blog"</span>

aws cloudformation package \
    --region <span class="hljs-string">"</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">REGION</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">"</span>  \
    --template-file cognito-userpool.yaml \
    -<span class="hljs-operator">-s</span>3-bucket <span class="hljs-string">"</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">DEPLOY_BUCKET</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">"</span> \
    -<span class="hljs-operator">-s</span>3-prefix <span class="hljs-string">"cloudformation/</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">STACK_NAME</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">/package/</span><span class="hljs-string"><span class="hljs-variable">$(</span></span><span class="hljs-string"><span class="hljs-variable">date +%s</span></span><span class="hljs-string"><span class="hljs-variable">)</span></span><span class="hljs-string">"</span> \
    --output-template-file cognito-userpool-packaged-template.yaml

aws cloudformation deploy --template-file cognito-userpool-packaged-template.yaml \
    --region <span class="hljs-string">"</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">REGION</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">"</span>  \
    --stack-name <span class="hljs-string">"</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">STACK_NAME</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">"</span> \
    --no-fail-on-empty-changeset \
    -<span class="hljs-operator">-s</span>3-bucket <span class="hljs-string">"</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">DEPLOY_BUCKET</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">"</span> \
    -<span class="hljs-operator">-s</span>3-prefix <span class="hljs-string">"cloudformation/</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">STACK_NAME</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">/template/</span><span class="hljs-string"><span class="hljs-variable">$(</span></span><span class="hljs-string"><span class="hljs-variable">date +%s</span></span><span class="hljs-string"><span class="hljs-variable">)</span></span><span class="hljs-string">"</span> \
    --capabilities CAPABILITY_IAM

I am using a Custom Domain with my User Pool, so I need to quickly jump into Route53 and create and ALIAS record: user-pool-blog.example.com -> d1h4chg8tp21la.cloudfront.net DomainName As you can see you’d need a certificate in us-east-1 (as it is a cloudfront distribution that sits in front of your User Pool)

  1. Cognito App Client – app-client.yaml
    We will be using “Implicit grant” AuthFlow so we could grab JWT token directly for our tests. In production you’d want to use “Authorization code grant” AuthFlow – Our Frontend UI will allow us to Sign-In, get the authorization code and exchange it for user pool token – this way tokens aren’t exposed to the user directly and there is less chance to be compromised.
    Set-up Application’s Callback URL(s) and Sign out URL(s) for your Frontend as per example below:
<span class="hljs-attribute">AWSTemplateFormatVersion</span>: <span class="hljs-string">'2010-09-09'</span>
<span class="hljs-attribute">Description</span>: <span class="hljs-string">'Cognito App Client Blog Example'</span>

<span class="javascript">Parameters:

  ClientName:
    Description: <span class="hljs-string">'Cognito ClientName'</span>
    Type: <span class="hljs-built_in">String</span>
    AllowedValues:
    - Blog-Client

Mappings:
  # Per env config
  EnvConfig:
    <span class="hljs-string">'Blog-Client'</span>:
      CallbackURLs: 
        - https:<span class="hljs-comment">//localhost</span>
        - https:<span class="hljs-comment">//localhost:3000/auth</span>
        - http:<span class="hljs-comment">//localhost:3000/signin</span>
        - https:<span class="hljs-comment">//blogapp.example.com/signin</span>
      LogoutURLs:
        - https:<span class="hljs-comment">//localhost</span>
        - https:<span class="hljs-comment">//localhost:3000/auth</span>
        - http:<span class="hljs-comment">//localhost:3000/signout</span>
        - https:<span class="hljs-comment">//blogapp.example.com/signout</span>
      UserPoolExportName: user-pool-blog-userPoolId

Resources:

# BEGIN - clients
    userPoolClient:
      Type: AWS::Cognito::UserPoolClient
      Properties: 
        AllowedOAuthFlows: 
          #- code
          - implicit
        AllowedOAuthFlowsUserPoolClient: <span class="hljs-literal">true</span>
        AllowedOAuthScopes: 
          - phone
          - email
          - openid
          - profile
        CallbackURLs: !FindInMap [EnvConfig, !Ref ClientName, CallbackURLs]
        ClientName: !Ref ClientName
        DefaultRedirectURI: https:<span class="hljs-comment">//localhost</span>
        GenerateSecret: <span class="hljs-literal">false</span>
        LogoutURLs: !FindInMap [EnvConfig, !Ref ClientName, LogoutURLs]
        PreventUserExistenceErrors: ENABLED
        UserPoolId: 
              Fn::ImportValue: !FindInMap [EnvConfig, !Ref ClientName, UserPoolExportName]
        ReadAttributes: 
          - preferred_username
          - given_name
          - family_name
          - custom:upload_folder
          - email
        WriteAttributes: 
          - preferred_username
          - given_name
          - family_name
          - custom:upload_folder
          - email
        SupportedIdentityProviders:
          - COGNITO
# END - clients

Outputs:
  StackName:
    Description: <span class="hljs-string">'Stack name.'</span>
    Value: !Sub <span class="hljs-string">'${AWS::StackName}'</span>

  clientId:
    Description: ID <span class="hljs-keyword">for</span> app client <span class="hljs-keyword">in</span> the User Pool
    Value: !Ref userPoolClient
</span>

Let’s deploy it – run this in cli:

REGION=<span class="hljs-string">"ap-southeast-2"</span>
DEPLOY_BUCKET=deployment-templates-bucket-private
ClientName=<span class="hljs-string">"Blog-Client"</span>
STACK_NAME=<span class="hljs-string">"cognito-appclient-blog"</span>

PROP_VALUES=<span class="hljs-string">"\
</span><span class="hljs-string">ClientName=</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">ClientName</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">"</span>

aws cloudformation package \
    --region <span class="hljs-string">"</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">REGION</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">"</span>  \
    --template-file app-client.yaml \
    -<span class="hljs-operator">-s</span>3-bucket <span class="hljs-string">"</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">DEPLOY_BUCKET</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">"</span> \
    -<span class="hljs-operator">-s</span>3-prefix <span class="hljs-string">"cloudformation/</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">STACK_NAME</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">/package/</span><span class="hljs-string"><span class="hljs-variable">$(</span></span><span class="hljs-string"><span class="hljs-variable">date +%s</span></span><span class="hljs-string"><span class="hljs-variable">)</span></span><span class="hljs-string">"</span> \
    --output-template-file app-client-packaged-template.yaml

aws cloudformation deploy --template-file app-client-packaged-template.yaml \
    --region <span class="hljs-string">"</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">REGION</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">"</span>  \
    --stack-name <span class="hljs-string">"</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">STACK_NAME</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">"</span> \
    --no-fail-on-empty-changeset \
    --parameter-overrides <span class="hljs-variable">${</span><span class="hljs-variable">PROP_VALUES</span><span class="hljs-variable">}</span> \
    -<span class="hljs-operator">-s</span>3-bucket <span class="hljs-string">"</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">DEPLOY_BUCKET</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">"</span> \
    -<span class="hljs-operator">-s</span>3-prefix <span class="hljs-string">"cloudformation/</span><span class="hljs-string"><span class="hljs-variable">${</span></span><span class="hljs-string"><span class="hljs-variable">STACK_NAME</span></span><span class="hljs-string"><span class="hljs-variable">}</span></span><span class="hljs-string">/template/</span><span class="hljs-string"><span class="hljs-variable">$(</span></span><span class="hljs-string"><span class="hljs-variable">date +%s</span></span><span class="hljs-string"><span class="hljs-variable">)</span></span><span class="hljs-string">"</span> \
    --capabilities CAPABILITY_IAM
  1. Now before getting to Custom Authorizer, let’s create a user in our User Pool, assign value to the custom attribute (custom:upload_folder) and check those JWT tokens: I’ll be using cli to do it quick and dirty:
  • Create a user
  • Set his email_verified status to true
  • Create uploader group
  • Add the user to uploader group
export USERNAME="john.doe@example.com"
export GIVEN_NAME="John"
export FAMILY_NAME="Doe"
export UPLOAD_FOLDER="johndoe-uploadfolder"
export POOL_ID="ap-southeast-2_qr7GA6s5T"

aws cognito-idp admin-<span class="hljs-operator"><span class="hljs-keyword">create</span>-<span class="hljs-keyword">user</span> \
<span class="hljs-comment">--user-pool-id ${POOL_ID} \</span>
<span class="hljs-comment">--username ${USERNAME} \</span>
<span class="hljs-comment">--user-attributes \</span>
Name=email,<span class="hljs-keyword">Value</span>=${USERNAME} \
Name=given_name,<span class="hljs-keyword">Value</span>=${GIVEN_NAME} \
Name=family_name,<span class="hljs-keyword">Value</span>=${FAMILY_NAME} \
Name=preferred_username,<span class="hljs-keyword">Value</span>=${USERNAME} \
Name=custom:upload_folder,<span class="hljs-keyword">Value</span>=${UPLOAD_FOLDER} \
<span class="hljs-comment">--region ap-southeast-2</span>

aws cognito-idp admin-<span class="hljs-keyword">update</span>-<span class="hljs-keyword">user</span>-attributes \
<span class="hljs-comment">--user-pool-id ${POOL_ID} \</span>
<span class="hljs-comment">--username ${USERNAME} \</span>
<span class="hljs-comment">--user-attributes \</span>
Name=email_verified,<span class="hljs-keyword">Value</span>=<span class="hljs-literal">true</span> \
Name=custom:upload_folder,<span class="hljs-keyword">Value</span>=${UPLOAD_FOLDER} \
<span class="hljs-comment">--region ap-southeast-2</span>

aws cognito-idp <span class="hljs-keyword">create</span>-<span class="hljs-keyword">group</span> \
<span class="hljs-comment">--user-pool-id ${POOL_ID} \</span>
<span class="hljs-comment">--group-name uploader \</span>
<span class="hljs-comment">--description "Allows file uploads" \</span>
<span class="hljs-comment">--region ap-southeast-2</span>

aws cognito-idp admin-<span class="hljs-keyword">add</span>-<span class="hljs-keyword">user</span>-<span class="hljs-keyword">to</span>-<span class="hljs-keyword">group</span> \
<span class="hljs-comment">--user-pool-id ${POOL_ID} \</span>
<span class="hljs-comment">--username ${USERNAME} \</span>
<span class="hljs-comment">--group-name uploader \</span>
<span class="hljs-comment">--region ap-southeast-2</span>
</span>

You should have received temporary password to your email adress (email address of user you’ve just created): Temp_Password

Now you can either go to the Cognito Console: “App integration”->”App client settings” and click on “Launch Hosted UI” or go to the following URL (Replace Domain and App Client Id with yours): https://user-pool-blog.example.com/login?response_type=token&client_id=3vf80uftfiegiqd1d8iaihfbq5&redirect_uri=https://localhost

Login and Change Password (You will be forced to change it on first login): UI_Login

And now we have our id_token and access_token: ID_Token

https://localhost/<span class="hljs-comment">#id_token</span><span class="hljs-comment">=</span><span class="hljs-comment">eyJraWQiOiJOcmp2S2RlV3hjVXNySFhjcUdUVXJ5OVB2N1RET2Vzek9HcWlERWs3czNNPSIsImFsZyI6IlJTMjU2In0.eyJhdF9oYXNoIjoiZ0p0RHBQOVE0S1dlWHFMcGF6UHRxUSIsInN1YiI6ImM2NDM2ZjRiLTEwMmUtNDNjNS1hMzFiLTM5YjM4MTgwZDg5MyIsImNvZ25pdG86Z3JvdXBzIjpbInVwbG9hZGVyIl0sImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC5hcC1zb3V0aGVhc3QtMi5hbWF6b25hd3MuY29tXC9hcC1zb3V0aGVhc3QtMl9xcjdHQTZzNVQiLCJjb2duaXRvOnVzZXJuYW1lIjoiYzY0MzZmNGItMTAyZS00M2M1LWEzMWItMzliMzgxODBkODkzIiwicHJlZmVycmVkX3VzZXJuYW1lIjoibGVvbi5rb2xjaGluc2t5QG1hbnRhbHVzLmNvbSIsImdpdmVuX25hbWUiOiJMZW9uIiwiYXVkIjoiM3ZmODB1ZnRmaWVnaXFkMWQ4aWFpaGZicTUiLCJldmVudF9pZCI6ImQwMzY3ZGQ5LTZjNzItNDVhYy05Njc1LWY1ZTM3NWM4YzAwYSIsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxNTkwMTI0ODE4LCJjdXN0b206dXBsb2FkX2ZvbGRlciI6Imxlb25rb2xjaGluc2t5LXVwbG9hZGZvbGRlciIsImV4cCI6MTU5MDEyODQxOCwiaWF0IjoxNTkwMTI0ODE4LCJmYW1pbHlfbmFtZSI6IktvbGNoaW5za3kiLCJlbWFpbCI6Imxlb24ua29sY2hpbnNreUBtYW50YWx1cy5jb20ifQ.EGJODxijBO03Y2tpffp8fmSLYVJiNkRhHW6rz4Yy4cC2hGFQVbiWM0nEAUKU6VSsC8zwvp-uYZPc1RA_qLWQ1kfSr-gpRI2wrx0FPPrhtuWt3qw2mpVMUbIxqgrKDKi6CeQOgGZtoN9GcKdEbEDViqo9dQMiqfgwglzw7X-VmGqAEel4eraKsjkP-Stqmdimd-TRsOBudj1QySI0MvgioywYRNFNnpRNOhB-_nTwO-vm9fxO8T7e767TQUP9QDRWnC6iNFVbh7CmYNjsLBHzV45nlFg60tJTSh01CD5oN2P6UNIJqxjEKasb_9Ek-A8bENH_wcvbLCUqlXMd0x3vYw&access_token</span><span class="hljs-comment">=</span><span class="hljs-comment">eyJraWQiOiJBbHV3YWVEU1wvWitMT3BtVEVzOXA0WDZcL2Iyb3cyYXB1OGJBVzFEVk1kaFE9IiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJjNjQzNmY0Yi0xMDJlLTQzYzUtYTMxYi0zOWIzODE4MGQ4OTMiLCJjb2duaXRvOmdyb3VwcyI6WyJ1cGxvYWRlciJdLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtc291dGhlYXN0LTIuYW1hem9uYXdzLmNvbVwvYXAtc291dGhlYXN0LTJfcXI3R0E2czVUIiwidmVyc2lvbiI6MiwiY2xpZW50X2lkIjoiM3ZmODB1ZnRmaWVnaXFkMWQ4aWFpaGZicTUiLCJldmVudF9pZCI6ImQwMzY3ZGQ5LTZjNzItNDVhYy05Njc1LWY1ZTM3NWM4YzAwYSIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoicGhvbmUgb3BlbmlkIHByb2ZpbGUgZW1haWwiLCJhdXRoX3RpbWUiOjE1OTAxMjQ4MTgsImV4cCI6MTU5MDEyODQxOCwiaWF0IjoxNTkwMTI0ODE4LCJqdGkiOiI3ZjVlMmYyZS00NWY0LTQ0NWYtYWQ1Ni0yYzU1MDk4MGI1NzciLCJ1c2VybmFtZSI6ImM2NDM2ZjRiLTEwMmUtNDNjNS1hMzFiLTM5YjM4MTgwZDg5MyJ9.Cu-rBjkFXn9TvXNYGjEuK2_QSndvPQroldeD4UI2VC8mDk2_38CffpsTdaVzFt1SXEoNGDxv8x28EuP3BjPI20S69ctLWA1Luod7zhvN9tGD6xZQMYmjm7Oa5gIe3nbFEFScWbbebYfNnAnEOdfdQ6djzNDtuQRo5h2eagjwyfvqcCYt7DD0QuSf-goLWT0AXS-ahrbajNSLUyoTZT18HrN8eRzdNfVqjzOSKfGcSyYxpE9LYtnLYR0Lj1HpKDSFFYrxp5mEE_E77fvUnbbIjuS1EkM618d8NoDg-R8mzS3n6lhTmmGSW55uiORFFJvXfXWlJ5oAvA2VRkCb1b0LQw&expires_in</span><span class="hljs-comment">=</span><span class="hljs-comment">3600&token_type</span><span class="hljs-comment">=</span><span class="hljs-comment">Bearer</span>

We don’t have refresh_token because we’ve used “Implicit Grant”.

Now let’s quickly go to https://jwt.io/ and decode our tokens: JWT_IO

id_token

{
  "<span class="hljs-attribute">at_hash</span>": <span class="hljs-value"><span class="hljs-string">"gJtDpP9Q4KWeXqLpazPtqQ"</span></span>,
  "<span class="hljs-attribute">sub</span>": <span class="hljs-value"><span class="hljs-string">"c6436f4b-102e-43c5-a31b-39b38180d893"</span></span>,
  "<span class="hljs-attribute">cognito:groups</span>": <span class="hljs-value">[
    <span class="hljs-string">"uploader"</span>
  ]</span>,
  "<span class="hljs-attribute">iss</span>": <span class="hljs-value"><span class="hljs-string">"https://cognito-idp.ap-southeast-2.amazonaws.com/ap-southeast-2_qr7GA6s5T"</span></span>,
  "<span class="hljs-attribute">cognito:username</span>": <span class="hljs-value"><span class="hljs-string">"c6436f4b-102e-43c5-a31b-39b38180d893"</span></span>,
  "<span class="hljs-attribute">preferred_username</span>": <span class="hljs-value"><span class="hljs-string">"john.doe@example.com"</span></span>,
  "<span class="hljs-attribute">given_name</span>": <span class="hljs-value"><span class="hljs-string">"John"</span></span>,
  "<span class="hljs-attribute">aud</span>": <span class="hljs-value"><span class="hljs-string">"3vf80uftfiegiqd1d8iaihfbq5"</span></span>,
  "<span class="hljs-attribute">event_id</span>": <span class="hljs-value"><span class="hljs-string">"d0367dd9-6c72-45ac-9675-f5e375c8c00a"</span></span>,
  "<span class="hljs-attribute">token_use</span>": <span class="hljs-value"><span class="hljs-string">"id"</span></span>,
  "<span class="hljs-attribute">auth_time</span>": <span class="hljs-value"><span class="hljs-number">1590124818</span></span>,
  "<span class="hljs-attribute">custom:upload_folder</span>": <span class="hljs-value"><span class="hljs-string">"johndoe-uploadfolder"</span></span>,
  "<span class="hljs-attribute">exp</span>": <span class="hljs-value"><span class="hljs-number">1590128418</span></span>,
  "<span class="hljs-attribute">iat</span>": <span class="hljs-value"><span class="hljs-number">1590124818</span></span>,
  "<span class="hljs-attribute">family_name</span>": <span class="hljs-value"><span class="hljs-string">"Doe"</span></span>,
  "<span class="hljs-attribute">email</span>": <span class="hljs-value"><span class="hljs-string">"john.doe@example.com"</span>
</span>}

access_token

{
  "<span class="hljs-attribute">sub</span>": <span class="hljs-value"><span class="hljs-string">"c6436f4b-102e-43c5-a31b-39b38180d893"</span></span>,
  "<span class="hljs-attribute">cognito:groups</span>": <span class="hljs-value">[
    <span class="hljs-string">"uploader"</span>
  ]</span>,
  "<span class="hljs-attribute">iss</span>": <span class="hljs-value"><span class="hljs-string">"https://cognito-idp.ap-southeast-2.amazonaws.com/ap-southeast-2_qr7GA6s5T"</span></span>,
  "<span class="hljs-attribute">version</span>": <span class="hljs-value"><span class="hljs-number">2</span></span>,
  "<span class="hljs-attribute">client_id</span>": <span class="hljs-value"><span class="hljs-string">"3vf80uftfiegiqd1d8iaihfbq5"</span></span>,
  "<span class="hljs-attribute">event_id</span>": <span class="hljs-value"><span class="hljs-string">"d0367dd9-6c72-45ac-9675-f5e375c8c00a"</span></span>,
  "<span class="hljs-attribute">token_use</span>": <span class="hljs-value"><span class="hljs-string">"access"</span></span>,
  "<span class="hljs-attribute">scope</span>": <span class="hljs-value"><span class="hljs-string">"phone openid profile email"</span></span>,
  "<span class="hljs-attribute">auth_time</span>": <span class="hljs-value"><span class="hljs-number">1590124818</span></span>,
  "<span class="hljs-attribute">exp</span>": <span class="hljs-value"><span class="hljs-number">1590128418</span></span>,
  "<span class="hljs-attribute">iat</span>": <span class="hljs-value"><span class="hljs-number">1590124818</span></span>,
  "<span class="hljs-attribute">jti</span>": <span class="hljs-value"><span class="hljs-string">"7f5e2f2e-45f4-445f-ad56-2c550980b577"</span></span>,
  "<span class="hljs-attribute">username</span>": <span class="hljs-value"><span class="hljs-string">"c6436f4b-102e-43c5-a31b-39b38180d893"</span>
</span>}

Pro tip:
As you can see id_token and access_token differ and quite a few of the user’s details are missing from access_token. Well, there is no rule of thumb that will dictate what token to use and when, but usually when you don’t need to pass user’s details to the downstream service you’d prefer to use access_token as a more secure option that doesn’t share user details, access_token is often used in case of service to service authentication. If you want to use access_token and still want to get a subset of user details in the JWT token you can use a nifty Cognito feature called “Triggers”. For example if you have Cognito+ADFS integration in place and your users have “custom:groups” attribute which value you’d want to add to access_token you could use “Pre Token Generation” workflow – Basically create a lambda that will inject “custom:groups” value into “cognito:groups” of your access_token before token is returned to your App/User. An example of such lambda can be found in this repo – /lambda/pretokengeneration

  1. Let’s create Custom Lambda Authorizer and then test it with a sample App. I’m using serverless framework and nodejs throughout this blogpost so you might want to quickly familiarize yourself with those.

To show you some of the flexibilty you have with Custom Authorizers – I will add a little twist in the Custom Lambda Authorizer – I’d also want to check if the user is a member in the “uploader” group – If he is not then he won’t be successfuly authenticated.

In the authlambda-blog folder:

.
├── auth.js
├── <span class="hljs-keyword">package</span>.json
└── serverless.yml

auth.js

<span class="hljs-pi">'use strict'</span>;

<span class="hljs-keyword">const</span> request = <span class="hljs-built_in">require</span>(<span class="hljs-string">'request'</span>);
<span class="hljs-keyword">const</span> jwkToPem = <span class="hljs-built_in">require</span>(<span class="hljs-string">'jwk-to-pem'</span>);
<span class="hljs-keyword">const</span> jwt = <span class="hljs-built_in">require</span>(<span class="hljs-string">'jsonwebtoken'</span>);
global.fetch = <span class="hljs-built_in">require</span>(<span class="hljs-string">'node-fetch'</span>);

<span class="hljs-keyword">const</span> appAccessGroup = process.env[<span class="hljs-string">'APP_ACCESS_GROUP'</span>];
<span class="hljs-keyword">const</span> UserPoolIdValue = process.env[<span class="hljs-string">'USER_POOL_ID'</span>]
<span class="hljs-keyword">const</span> ClientIdValue = process.env[<span class="hljs-string">'CLIENT_ID'</span>]

<span class="hljs-comment">// Pool Info</span>
<span class="hljs-keyword">const</span> poolData = {    
    UserPoolId : UserPoolIdValue, <span class="hljs-comment">// Your user pool id here    </span>
    ClientId : ClientIdValue <span class="hljs-comment">// Your client id here</span>
    }; 
<span class="hljs-keyword">const</span> pool_region = <span class="hljs-string">'ap-southeast-2'</span>;

<span class="hljs-comment">/**
  * Returns an IAM policy document for a given user and resource.
  *
  * @method buildIAMPolicy
  * @param {String} userId - user id
  * @param {String} effect  - Allow / Deny
  * @param {String} resource - resource ARN
  * @param {String} context - response context
  * @returns {Object} policyDocument
  */</span>
 <span class="hljs-keyword">const</span> buildIAMPolicy = (userId, effect, resource, context) => {
  <span class="hljs-built_in">console</span>.log(`buildIAMPolicy ${userId} ${effect} ${resource}`);
  <span class="hljs-keyword">const</span> policy = {
    principalId: userId,
    policyDocument: {
      Version: <span class="hljs-string">'2012-10-17'</span>,
      Statement: [
        {
          Action: <span class="hljs-string">'execute-api:Invoke'</span>,
          Effect: effect,
          Resource: resource,
        },
      ],
    },
    context,
  };

  <span class="hljs-built_in">console</span>.log(<span class="hljs-built_in">JSON</span>.stringify(policy));
  <span class="hljs-keyword">return</span> policy;
};
<span class="hljs-comment">//</span>
<span class="hljs-comment">// Reusable Authorizer function, set on `authorizer` field in serverless.yml</span>
<span class="hljs-built_in">module</span>.exports.authorize = (event, context, cb) => {

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Auth function invoked'</span>);
  <span class="hljs-keyword">if</span> (event.authorizationToken) {
    <span class="hljs-comment">// Remove 'bearer ' from token:</span>
    <span class="hljs-keyword">const</span> token = event.authorizationToken.substring(<span class="hljs-number">7</span>);
    <span class="hljs-comment">// Make a request to the iss + .well-known/jwks.json URL:</span>
    request({
      url: `https:<span class="hljs-comment">//cognito-idp.${pool_region}.amazonaws.com/${poolData.UserPoolId}/.well-known/jwks.json`,</span>
      json: <span class="hljs-literal">true</span>
    }, <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(error, response, body)</span> </span>{
          <span class="hljs-keyword">if</span> (!error && response.statusCode === <span class="hljs-number">200</span>) {
              <span class="hljs-keyword">var</span> pems = {};
              <span class="hljs-keyword">var</span> keys = body[<span class="hljs-string">'keys'</span>];
              <span class="hljs-keyword">for</span>(<span class="hljs-keyword">var</span> i = <span class="hljs-number">0</span>; i < keys.length; i++) {
                  <span class="hljs-comment">//Convert each key to PEM</span>
                  <span class="hljs-keyword">var</span> key_id = keys[i].kid;
                  <span class="hljs-keyword">var</span> modulus = keys[i].n;
                  <span class="hljs-keyword">var</span> exponent = keys[i].e;
                  <span class="hljs-keyword">var</span> key_type = keys[i].kty;
                  <span class="hljs-keyword">var</span> jwk = { kty: key_type, n: modulus, e: exponent};
                  <span class="hljs-keyword">var</span> pem = jwkToPem(jwk);
                  pems[key_id] = pem;
              }
              <span class="hljs-comment">//validate the token</span>
              <span class="hljs-keyword">var</span> decodedJwt = jwt.decode(token, {complete: <span class="hljs-literal">true</span>});
              <span class="hljs-keyword">if</span> (!decodedJwt) {
                  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Not a valid JWT token"</span>);
                  cb(<span class="hljs-string">'Unauthorized'</span>);
              }

              <span class="hljs-keyword">var</span> kid = decodedJwt.header.kid;
              <span class="hljs-keyword">var</span> pem = pems[kid];
              <span class="hljs-keyword">if</span> (!pem) {
                  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Invalid token'</span>);
                  cb(<span class="hljs-string">'Unauthorized'</span>);
              }

              jwt.verify(token, pem, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(err, payload)</span> </span>{
                  <span class="hljs-keyword">if</span>(err) {
                      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Invalid Token."</span>);
                      cb(<span class="hljs-string">'Unauthorized'</span>);
                  } <span class="hljs-keyword">else</span> {
                      <span class="hljs-comment">// START - appAccessGroup check - Adding appAccessGroup to allow initial access to the Api based on the group membership</span>
                      <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> payload[<span class="hljs-string">'cognito:groups'</span>] === <span class="hljs-string">'undefined'</span> || payload[<span class="hljs-string">'cognito:groups'</span>] === <span class="hljs-literal">null</span>) {
                        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"No cognito:groups in the payload hence can't look for APP_ACCESS_GROUP in the array of cognito:groups which is a list of user groups, hence returning UNAUTHORISED"</span>);
                        cb(<span class="hljs-string">'Unauthorized'</span>);
                      } <span class="hljs-keyword">else</span> {
                        <span class="hljs-keyword">var</span> customScope = payload[<span class="hljs-string">'cognito:groups'</span>];
                        <span class="hljs-keyword">if</span>(customScope.includes(appAccessGroup)) {
                          <span class="hljs-built_in">console</span>.log(appAccessGroup + <span class="hljs-string">" is in the customScope list "</span> + customScope);
                        } <span class="hljs-keyword">else</span> {
                          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Error! "</span> + appAccessGroup + <span class="hljs-string">" is NOT in the customScope list "</span> + customScope);
                          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Please make sure that the user is a member of APP_ACCESS_GROUP env variable and claim rule added to ADFS config to pass group membership into a custom  attribute - APP_ACCESS_GROUP is env variable for the App authoriser lambda"</span>);
                          cb(<span class="hljs-string">'Unauthorized'</span>);
                        }
                      }
                      <span class="hljs-comment">// END - appAccessGroup check</span>
                      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"methodArn:"</span>);
                      <span class="hljs-built_in">console</span>.log(event.methodArn);
                      <span class="hljs-comment">// Let's return the full payload in authorizerContext - this is really up to company's requirements</span>
                      <span class="hljs-keyword">const</span> authorizerContext = { token: <span class="hljs-built_in">JSON</span>.stringify(payload) };
                      <span class="hljs-comment">// You can replace * with event.methodArn if you don't want to manage user access in the backend app based on the user scope (and face the consequences ;)</span>
                      <span class="hljs-keyword">const</span> policyDocument = buildIAMPolicy(payload.sub, <span class="hljs-string">'Allow'</span>, <span class="hljs-string">'*'</span>, authorizerContext);
                      cb(<span class="hljs-literal">null</span>, policyDocument);
                  }
              });
          } <span class="hljs-keyword">else</span> {
              <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Error! Unable to download JWKs"</span>);
              cb(<span class="hljs-string">'Unauthorized'</span>);
          }
      });
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'No authorizationToken found in the header.'</span>);
    cb(<span class="hljs-string">'Unauthorized'</span>);
  }
};

package.json

{
  "<span class="hljs-attribute">name</span>": <span class="hljs-value"><span class="hljs-string">"authlambda-blog"</span></span>,
  "<span class="hljs-attribute">version</span>": <span class="hljs-value"><span class="hljs-string">"1.0.0"</span></span>,
  "<span class="hljs-attribute">description</span>": <span class="hljs-value"><span class="hljs-string">"Custom Authoriser"</span></span>,
  "<span class="hljs-attribute">scripts</span>": <span class="hljs-value">{
    "<span class="hljs-attribute">deploy-lambda</span>": <span class="hljs-string">"node_modules/.bin/serverless deploy --force -v"</span>,
    "<span class="hljs-attribute">remove-lambda</span>": <span class="hljs-string">"node_modules/.bin/serverless remove -v"</span>
  }</span>,
  "<span class="hljs-attribute">author</span>": <span class="hljs-value"><span class="hljs-string">""</span></span>,
  "<span class="hljs-attribute">license</span>": <span class="hljs-value"><span class="hljs-string">"MIT"</span></span>,
  "<span class="hljs-attribute">dependencies</span>": <span class="hljs-value">{
    "<span class="hljs-attribute">jsonwebtoken</span>": <span class="hljs-string">"^8.5.1"</span>,
    "<span class="hljs-attribute">jwk-to-pem</span>": <span class="hljs-string">"^2.0.1"</span>,
    "<span class="hljs-attribute">node-fetch</span>": <span class="hljs-string">"^2.6.0"</span>,
    "<span class="hljs-attribute">request</span>": <span class="hljs-string">"^2.88.0"</span>
  }</span>,
  "<span class="hljs-attribute">devDependencies</span>": <span class="hljs-value">{
    "<span class="hljs-attribute">serverless</span>": <span class="hljs-string">"^1.67.0"</span>,
    "<span class="hljs-attribute">serverless-stack-termination-protection</span>": <span class="hljs-string">"^1.0.0"</span>
  }
</span>}

serverless.yml (Update UserPoolId and ClientId to your values):
UserPoolId:
dev: ‘ap-southeast-2_qr7GA6s5T’ # UserPoolId in the user-pool-blog
ClientId:
dev: “3vf80uftfiegiqd1d8iaihfbq5” # App client Blog-Client \

<span class="hljs-attribute">service</span>: <span class="hljs-string">authlambda-blog</span>

<span class="http"><span class="hljs-attribute">frameworkVersion</span>: <span class="hljs-string">">=1.1.0 <2.0.0"</span>

<span class="bash">plugins:
  - serverless-stack-termination-protection

custom:
  serverlessTerminationProtection:
    stages:
      - prod <span class="hljs-comment"># prod - Production</span>
  <span class="hljs-built_in">log</span>Retention:
    prod: <span class="hljs-number">90</span>
    default: <span class="hljs-number">7</span>
  UserPoolId:
      dev: <span class="hljs-string">'ap-southeast-2_qr7GA6s5T'</span> <span class="hljs-comment"># UserPoolId in the user-pool-blog</span>
  ClientId:
      dev: <span class="hljs-string">"3vf80uftfiegiqd1d8iaihfbq5"</span> <span class="hljs-comment"># App client Blog-Client</span>

  <span class="hljs-comment"># appname and app_access_group - variables passed on cli (if not passed - defaults are used as below)</span>
  default_appname: dummyapp
  appname: <span class="hljs-variable">${opt:appname, self:custom.default_appname}</span>
  default_app_access_group: uploader
  app_access_group: <span class="hljs-variable">${opt:app_access_group, self:custom.default_app_access_group}</span>

  tags:
    team: leons
    platform: it
    service: <span class="hljs-variable">${self:service}</span>
    environment: <span class="hljs-variable">${self:provider.stage}</span>
  stackTags:  
    team: leons
    platform: it
    service: <span class="hljs-variable">${self:service}</span>
    environment: <span class="hljs-variable">${self:provider.stage}</span>

provider:
  name: aws
  runtime: nodejs10.x
  stackName: <span class="hljs-variable">${self:service}</span>-<span class="hljs-variable">${self:provider.stage}</span>-<span class="hljs-variable">${self:custom.appname}</span>
  memorySize: <span class="hljs-number">128</span>
  stage: <span class="hljs-variable">${opt:stage, env:STAGE_NAME, 'poc' }</span>
  deploymentBucket: deployment-templates-bucket-private
  timeout: <span class="hljs-number">30</span>
  <span class="hljs-built_in">log</span>RetentionInDays: <span class="hljs-variable">${self:custom.logRetention.${self:provider.stage}</span>, self:custom.logRetention.default}
  region: ap-southeast-<span class="hljs-number">2</span>
  tracing:
    apiGateway: <span class="hljs-literal">true</span>
    lambda: <span class="hljs-literal">true</span> <span class="hljs-comment"># Optional, can be true (true equals 'Active'), 'Active' or 'PassThrough'</span>
  resultTtlInSeconds: <span class="hljs-number">3600</span>

package:
  exclude:
  - node_modules/aws-sdk/**

<span class="hljs-built_in">functions</span>:
  auth:
    handler: auth.authorize
    name: <span class="hljs-variable">${self:service}</span>-<span class="hljs-variable">${self:provider.stage}</span>-<span class="hljs-variable">${self:custom.appname}</span>-auth
    role: myCustRole
    cors: <span class="hljs-literal">true</span>
    environment:
      APP_ACCESS_GROUP: <span class="hljs-variable">${self:custom.app_access_group}</span>
      USER_POOL_ID: <span class="hljs-variable">${self:custom.UserPoolId.${self:provider.stage}</span>}
      CLIENT_ID: <span class="hljs-variable">${self:custom.ClientId.${self:provider.stage}</span>}

resources:
  Resources:
    <span class="hljs-comment"># Custom role RoleName is tied to "appname" due to the way serverless framework constructs "rolename" - ${self:service}-${self:provider.stage}-[region]-lambdaRole</span>
    <span class="hljs-comment"># Since we're going to have multiple lambdas of the same service and stage we need to make sure the role for each named differently</span>
    myCustRole:
      Type: AWS::IAM::Role
      Properties:
        Path: /cognito/authoriser/<span class="hljs-variable">${self:custom.appname}</span>/
        RoleName: <span class="hljs-variable">${self:service}</span>-<span class="hljs-variable">${self:provider.stage}</span>-<span class="hljs-variable">${self:custom.appname}</span>-auth <span class="hljs-comment"># required if you want to use 'serverless deploy --function' later on</span>
        AssumeRolePolicyDocument:
          Version: <span class="hljs-string">'2012-10-17'</span>
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action: sts:AssumeRole
        <span class="hljs-comment"># note that these rights are needed if you want your function to be able to communicate with resources within your vpc</span>
        <span class="hljs-comment"># Just to get rid of that annoying error message in the console for now - https://github.com/serverless/serverless/issues/6241 (there are links to open issues)</span>
        <span class="hljs-comment"># But to be honest serverless framework is doing a very similar thing with arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole, so it is a legit way :)</span>
        ManagedPolicyArns:
          - <span class="hljs-string">"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"</span>
        Policies:
          - PolicyName: xray-policy
            PolicyDocument:
              Version: <span class="hljs-string">'2012-10-17'</span>
              Statement:
                - Effect: Allow
                  Action:
                    - xray:PutTraceSegments
                    - xray:PutTelemetryRecords
                  Resource:
                    - <span class="hljs-string">"*"</span>

    <span class="hljs-comment"># List Lambda Permissions for all accounts you want to use this authoriser lambda with (to be invoked by Authorisers from different accounts)</span>
    <span class="hljs-comment"># Access for new accounts should be added below - meaning that authorizers from diff. accounts could invoke Authorizer function</span>
    ConfigLambdaPermission123456789123:
      Type: <span class="hljs-string">"AWS::Lambda::Permission"</span>
      Properties:
        Action: lambda:InvokeFunction
        FunctionName: !Ref AuthLambdaFunction
        Principal: apigateway.amazonaws.com
        SourceArn: !Sub <span class="hljs-string">"arn:aws:execute-api:ap-southeast-2:123456789123:*/authorizers/*"</span>
</span></span>

Let’s deploy Custom Authorizer ($ npm run deploy-lambda -- --stage ${stage} --appname ${appname} --app_access_group ${app_access_group}):

$ npm install
$ npm run deploy-lambda -- --stage dev --appname testapp --app_access_group uploader

And we’ve got ourselves a Lambda Authorizer:

<span class="hljs-constant">AuthLambdaFunctionQualifiedArn</span><span class="hljs-symbol">:</span> <span class="hljs-symbol">arn:</span><span class="hljs-symbol">aws:</span><span class="hljs-symbol">lambda:</span>ap-southeast-<span class="hljs-number">2</span><span class="hljs-symbol">:</span><span class="hljs-number">123456789123</span><span class="hljs-symbol">:function</span><span class="hljs-symbol">:authlambda-blog-dev-testapp-auth</span>
  1. OK, we’re almost there. Let’s just quickly create a demo App with one endpoint protected by Custom Authorizer and another unprotected endpoint.

In the cognito-auth-demo-app folder:

$ tree
.
├── package.json
├── serverless.yml
└── src
    ├── <span class="hljs-keyword">private</span>.js
    └── <span class="hljs-keyword">public</span>.js

public.js

<span class="hljs-pi">'use strict'</span>;

<span class="hljs-keyword">const</span> serverless = <span class="hljs-built_in">require</span>(<span class="hljs-string">'serverless-http'</span>);
<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">var</span> cors = <span class="hljs-built_in">require</span>(<span class="hljs-string">'cors'</span>);
<span class="hljs-keyword">const</span> app = express();

<span class="hljs-comment">// You can also enable pre-flight across-the-board</span>
<span class="hljs-comment">// https://expressjs.com/en/resources/middleware/cors.html</span>
app.options(<span class="hljs-string">'*'</span>, cors())

<span class="hljs-keyword">var</span> corsOptions = {
    origin: <span class="hljs-string">'*'</span>,
    methods: [ <span class="hljs-string">'GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS'</span> ],
    credentials: <span class="hljs-literal">true</span>,
    preflightContinue: <span class="hljs-literal">false</span>,
    optionsSuccessStatus: <span class="hljs-number">200</span>
  };

app.get(<span class="hljs-string">'/public'</span>, cors(corsOptions), <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(req, res)</span> </span>{
    res.send(<span class="hljs-keyword">new</span> Buffer(<span class="hljs-string">'<h5>Hello Mantalus Public Page!</h5>'</span>));
});

<span class="hljs-built_in">module</span>.exports.handler = serverless(app);

private.js

<span class="hljs-pi">'use strict'</span>;

<span class="hljs-keyword">const</span> serverless = <span class="hljs-built_in">require</span>(<span class="hljs-string">'serverless-http'</span>);
<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">var</span> cors = <span class="hljs-built_in">require</span>(<span class="hljs-string">'cors'</span>);
<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">var</span> bodyParser = <span class="hljs-built_in">require</span>(<span class="hljs-string">'body-parser'</span>);
<span class="hljs-keyword">var</span> cookieParser = <span class="hljs-built_in">require</span>(<span class="hljs-string">'cookie-parser'</span>);
<span class="hljs-keyword">const</span> CircularJSON = <span class="hljs-built_in">require</span>(<span class="hljs-string">'circular-json'</span>);

app.use(bodyParser.urlencoded({ extended: <span class="hljs-literal">false</span> }));
app.use(bodyParser.json());
app.use(cookieParser());

<span class="hljs-comment">// You can also enable pre-flight across-the-board</span>
<span class="hljs-comment">// https://expressjs.com/en/resources/middleware/cors.html</span>
app.options(<span class="hljs-string">'*'</span>, cors())

<span class="hljs-keyword">var</span> whitelist = [
    <span class="hljs-string">'http://localhost:3000'</span>,
    <span class="hljs-string">'http://localhost'</span>
]

<span class="hljs-keyword">var</span> corsOptions = {
  origin: <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(origin, callback)</span> </span>{
    <span class="hljs-keyword">if</span> (whitelist.indexOf(origin) !== -<span class="hljs-number">1</span> || !origin) {
      callback(<span class="hljs-literal">null</span>, <span class="hljs-literal">true</span>)
    } <span class="hljs-keyword">else</span> {
      callback(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Not allowed by CORS'</span>))
    }
  },
  methods: [ <span class="hljs-string">'GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS'</span> ],
  credentials: <span class="hljs-literal">true</span>,
  preflightContinue: <span class="hljs-literal">false</span>,
  optionsSuccessStatus: <span class="hljs-number">200</span>
}

app.use(<span class="hljs-string">'/private'</span>,<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(req, res, next)</span></span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"A new request received at "</span> + <span class="hljs-built_in">Date</span>.now());
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'%O'</span>, req);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'req:\n'</span> + CircularJSON.stringify(req));
  next();
});


app.get(<span class="hljs-string">'/private'</span>, cors(corsOptions), <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(req, res)</span> </span>{
    res.send(<span class="hljs-string">'<h5>Hello Mantalus Private Page!</h5>'</span>);
});


<span class="hljs-built_in">module</span>.exports.handler = serverless(app);

package.json

{
  "<span class="hljs-attribute">name</span>": <span class="hljs-value"><span class="hljs-string">"demoapp-auth-blog"</span></span>,
  "<span class="hljs-attribute">version</span>": <span class="hljs-value"><span class="hljs-string">"1.0.0"</span></span>,
  "<span class="hljs-attribute">description</span>": <span class="hljs-value"><span class="hljs-string">"Demo App"</span></span>,
  "<span class="hljs-attribute">scripts</span>": <span class="hljs-value">{
    "<span class="hljs-attribute">deploy-lambda</span>": <span class="hljs-string">"node_modules/.bin/serverless deploy --force -v"</span>,
    "<span class="hljs-attribute">remove-lambda</span>": <span class="hljs-string">"node_modules/.bin/serverless remove -v"</span>
  }</span>,
  "<span class="hljs-attribute">author</span>": <span class="hljs-value"><span class="hljs-string">""</span></span>,
  "<span class="hljs-attribute">license</span>": <span class="hljs-value"><span class="hljs-string">"MIT"</span></span>,
  "<span class="hljs-attribute">dependencies</span>": <span class="hljs-value">{
    "<span class="hljs-attribute">body-parser</span>": <span class="hljs-string">"^1.19.0"</span>,
    "<span class="hljs-attribute">circular-json</span>": <span class="hljs-string">"^0.5.9"</span>,
    "<span class="hljs-attribute">cookie-parser</span>": <span class="hljs-string">"^1.4.4"</span>,
    "<span class="hljs-attribute">cors</span>": <span class="hljs-string">"^2.8.5"</span>,
    "<span class="hljs-attribute">express</span>": <span class="hljs-string">"^4.17.1"</span>,
    "<span class="hljs-attribute">serverless-http</span>": <span class="hljs-string">"^2.3.1"</span>
  }</span>,
  "<span class="hljs-attribute">devDependencies</span>": <span class="hljs-value">{
    "<span class="hljs-attribute">serverless-pseudo-parameters</span>": <span class="hljs-string">"^2.5.0"</span>
  }
</span>}

serverless.yml
Notice the apiGatewayAuthorizer section – that’s there we reference our Custom Authorizer Lambda (that can live in this or any other account)

<span class="hljs-attribute">service</span>: <span class="hljs-string">demoapp-auth-blog</span>

<span class="http"><span class="hljs-attribute">frameworkVersion</span>: <span class="hljs-string">">=1.1.0 <2.0.0"</span>

<span class="ruby"><span class="hljs-symbol">plugins:</span>
  - serverless-pseudo-parameters

<span class="hljs-symbol">provider:</span>
  <span class="hljs-symbol">name:</span> aws
  <span class="hljs-symbol">runtime:</span> nodejs1<span class="hljs-number">0</span>.x
  <span class="hljs-symbol">memorySize:</span> <span class="hljs-number">128</span>
  <span class="hljs-symbol">stage:</span> <span class="hljs-variable">${</span><span class="hljs-symbol">opt:</span>stage, <span class="hljs-symbol">env:</span><span class="hljs-constant">STAGE_NAME</span>, <span class="hljs-string">'poc'</span> }
  <span class="hljs-symbol">deploymentBucket:</span> deployment-templates-bucket-private
  <span class="hljs-symbol">timeout:</span> <span class="hljs-number">30</span>
  <span class="hljs-symbol">logRetentionInDays:</span> <span class="hljs-variable">${</span><span class="hljs-symbol">self:</span>custom.logRetention.<span class="hljs-variable">${</span><span class="hljs-symbol">self:</span>provider.stage}, <span class="hljs-symbol">self:</span>custom.logRetention.default}
  <span class="hljs-symbol">region:</span> ap-southeast-<span class="hljs-number">2</span>
  <span class="hljs-symbol">tracing:</span>
    <span class="hljs-symbol">apiGateway:</span> <span class="hljs-keyword">true</span>
    <span class="hljs-symbol">lambda:</span> <span class="hljs-keyword">true</span> <span class="hljs-comment"># Optional, can be true (true equals 'Active'), 'Active' or 'PassThrough'</span>
  <span class="hljs-symbol">resultTtlInSeconds:</span> <span class="hljs-number">0</span> <span class="hljs-comment"># Adjust for caching purposes after initial testing is done</span>

  <span class="hljs-symbol">tags:</span>
    <span class="hljs-symbol">team:</span> leons
    <span class="hljs-symbol">platform:</span> it
    <span class="hljs-symbol">service:</span> <span class="hljs-variable">${</span><span class="hljs-symbol">self:</span>service}
    <span class="hljs-symbol">environment:</span> <span class="hljs-variable">${</span><span class="hljs-symbol">self:</span>provider.stage}
  <span class="hljs-symbol">stackTags:</span>  
    <span class="hljs-symbol">team:</span> leons
    <span class="hljs-symbol">platform:</span> it
    <span class="hljs-symbol">service:</span> <span class="hljs-variable">${</span><span class="hljs-symbol">self:</span>service}
    <span class="hljs-symbol">environment:</span> <span class="hljs-variable">${</span><span class="hljs-symbol">self:</span>provider.stage}

<span class="hljs-symbol">custom:</span>
  <span class="hljs-symbol">logRetention:</span>
    <span class="hljs-symbol">prod:</span> <span class="hljs-number">90</span>    
    <span class="hljs-symbol">default:</span> <span class="hljs-number">7</span>

<span class="hljs-symbol">package:</span>
  <span class="hljs-symbol">exclude:</span>
  - node_modules/aws-sdk/**

<span class="hljs-symbol">functions:</span>
  <span class="hljs-symbol">publicEndpoint:</span>
    <span class="hljs-symbol">handler:</span> src/public.handler
    <span class="hljs-symbol">events:</span>
      - <span class="hljs-symbol">http:</span>
          <span class="hljs-symbol">path:</span> /public
          <span class="hljs-symbol">method:</span> <span class="hljs-constant">GET</span>
          <span class="hljs-symbol">integration:</span> lambda
          <span class="hljs-symbol">cors:</span> <span class="hljs-keyword">true</span>

  <span class="hljs-symbol">privateEndpoint:</span>
    <span class="hljs-symbol">handler:</span> src/private.handler
    <span class="hljs-symbol">events:</span>
      - <span class="hljs-symbol">http:</span> 
          <span class="hljs-symbol">path:</span> /private
          <span class="hljs-symbol">method:</span> <span class="hljs-constant">GET</span>
          <span class="hljs-symbol">integration:</span> lambda
          <span class="hljs-symbol">authorizer:</span>
            <span class="hljs-symbol">type:</span> <span class="hljs-string">"CUSTOM"</span>
            <span class="hljs-symbol">authorizerId:</span>
              <span class="hljs-constant">Ref</span><span class="hljs-symbol">:</span> <span class="hljs-string">"apiGatewayAuthorizer"</span>
            <span class="hljs-symbol">resultTtlInSeconds:</span> <span class="hljs-variable">${</span><span class="hljs-symbol">self:</span>provider.resultTtlInSeconds}
          <span class="hljs-symbol">cors:</span> <span class="hljs-keyword">true</span>

<span class="hljs-symbol">resources:</span>
  <span class="hljs-constant">Resources</span><span class="hljs-symbol">:</span>
    <span class="hljs-constant">GatewayResponseDefault4XX</span><span class="hljs-symbol">:</span>
      <span class="hljs-constant">Type</span><span class="hljs-symbol">:</span> <span class="hljs-string">'AWS::ApiGateway::GatewayResponse'</span>
      <span class="hljs-constant">Properties</span><span class="hljs-symbol">:</span>
        <span class="hljs-constant">ResponseParameters</span><span class="hljs-symbol">:</span>
          gatewayresponse.header.<span class="hljs-constant">Access</span>-<span class="hljs-constant">Control</span>-<span class="hljs-constant">Allow</span>-<span class="hljs-constant">Origin</span><span class="hljs-symbol">:</span> <span class="hljs-string">"'*'"</span>
          gatewayresponse.header.<span class="hljs-constant">Access</span>-<span class="hljs-constant">Control</span>-<span class="hljs-constant">Allow</span>-<span class="hljs-constant">Headers</span><span class="hljs-symbol">:</span> <span class="hljs-string">"'*'"</span>
        <span class="hljs-constant">ResponseType</span><span class="hljs-symbol">:</span> <span class="hljs-constant">DEFAULT_4XX</span>
        <span class="hljs-constant">RestApiId</span><span class="hljs-symbol">:</span>
          <span class="hljs-constant">Ref</span><span class="hljs-symbol">:</span> <span class="hljs-string">'ApiGatewayRestApi'</span>

    <span class="hljs-constant">GatewayResponseDefault5XX</span><span class="hljs-symbol">:</span>
      <span class="hljs-constant">Type</span><span class="hljs-symbol">:</span> <span class="hljs-string">'AWS::ApiGateway::GatewayResponse'</span>
      <span class="hljs-constant">Properties</span><span class="hljs-symbol">:</span>
        <span class="hljs-constant">ResponseParameters</span><span class="hljs-symbol">:</span>
          gatewayresponse.header.<span class="hljs-constant">Access</span>-<span class="hljs-constant">Control</span>-<span class="hljs-constant">Allow</span>-<span class="hljs-constant">Origin</span><span class="hljs-symbol">:</span> <span class="hljs-string">"'*'"</span>
          gatewayresponse.header.<span class="hljs-constant">Access</span>-<span class="hljs-constant">Control</span>-<span class="hljs-constant">Allow</span>-<span class="hljs-constant">Headers</span><span class="hljs-symbol">:</span> <span class="hljs-string">"'*'"</span>
        <span class="hljs-constant">ResponseType</span><span class="hljs-symbol">:</span> <span class="hljs-constant">DEFAULT_5XX</span>
        <span class="hljs-constant">RestApiId</span><span class="hljs-symbol">:</span>
          <span class="hljs-constant">Ref</span><span class="hljs-symbol">:</span> <span class="hljs-string">'ApiGatewayRestApi'</span>

    <span class="hljs-symbol">apiGatewayAuthorizer:</span>
      <span class="hljs-constant">Type</span><span class="hljs-symbol">:</span> <span class="hljs-string">"AWS::ApiGateway::Authorizer"</span>
      <span class="hljs-constant">Properties</span><span class="hljs-symbol">:</span>
        <span class="hljs-constant">Name</span><span class="hljs-symbol">:</span> <span class="hljs-string">"authorizer"</span>
        <span class="hljs-comment"># Replace authlambda-blog-dev-testapp-auth with your Authoriser Lambda name</span>
        <span class="hljs-constant">AuthorizerUri</span><span class="hljs-symbol">:</span> <span class="hljs-string">"arn:aws:apigateway:<span class="hljs-subst">#{<span class="hljs-constant">AWS::Region</span>}</span>:lambda:path/2015-03-31/functions/arn:aws:lambda:<span class="hljs-subst">#{<span class="hljs-constant">AWS::Region</span>}</span>:123456789123:function:authlambda-blog-dev-testapp-auth/invocations"</span>
        <span class="hljs-constant">IdentityValidationExpression</span><span class="hljs-symbol">:</span>  ^<span class="hljs-constant">Bearer</span> +[-<span class="hljs-number">0</span>-<span class="hljs-number">9</span>a-zA-<span class="hljs-constant">Z</span>\.<span class="hljs-number">_</span>]*<span class="hljs-variable">$
</span>        <span class="hljs-constant">RestApiId</span><span class="hljs-symbol">:</span> !<span class="hljs-constant">Ref</span> <span class="hljs-string">"ApiGatewayRestApi"</span>
        <span class="hljs-constant">Type</span><span class="hljs-symbol">:</span> <span class="hljs-string">"TOKEN"</span>
        <span class="hljs-constant">IdentitySource</span><span class="hljs-symbol">:</span> <span class="hljs-string">"method.request.header.Authorization"</span>
      <span class="hljs-constant">DependsOn</span><span class="hljs-symbol">:</span>
        - <span class="hljs-string">"ApiGatewayRestApi"</span>
</span></span>

Let’s deploy this demo App (npm run deploy-lambda -- --stage ${stage}):

$ npm install
$ npm run deploy-lambda -- --stage dev

We’ve got our endpoints now:

endpoint<span class="hljs-variable">s:</span>
  GET - http<span class="hljs-variable">s:</span>//yijhzu5fig.<span class="hljs-keyword">execute</span>-api.ap-southeast-<span class="hljs-number">2</span>.amazonaws.<span class="hljs-keyword">com</span>/dev/public
  GET - http<span class="hljs-variable">s:</span>//yijhzu5fig.<span class="hljs-keyword">execute</span>-api.ap-southeast-<span class="hljs-number">2</span>.amazonaws.<span class="hljs-keyword">com</span>/dev/private

Let’s check those out:

$ curl https:<span class="hljs-comment">//yijhzu5fig.execute-api.ap-southeast-2.amazonaws.com/dev/public</span>
{<span class="hljs-string">"statusCode"</span>:<span class="hljs-number">200</span>,<span class="hljs-string">"headers"</span>:{<span class="hljs-string">"x-powered-by"</span>:<span class="hljs-string">"Express"</span>,<span class="hljs-string">"access-control-allow-origin"</span>:<span class="hljs-string">"*"</span>,<span class="hljs-string">"access-control-allow-credentials"</span>:<span class="hljs-string">"true"</span>,<span class="hljs-string">"content-type"</span>:<span class="hljs-string">"application/octet-stream"</span>,<span class="hljs-string">"content-length"</span>:<span class="hljs-string">"36"</span>,<span class="hljs-string">"etag"</span>:<span class="hljs-string">"W/\"24-TX3eE0StJ1bfkNdwhw4LuJZ/wHI\""</span>},<span class="hljs-string">"isBase64Encoded"</span>:<span class="hljs-keyword">false</span>,<span class="hljs-string">"body"</span>:<span class="hljs-string">"<h5>Hello Mantalus Public Page!</h5>"</span>}

$ curl https:<span class="hljs-comment">//yijhzu5fig.execute-api.ap-southeast-2.amazonaws.com/dev/private</span>
{<span class="hljs-string">"message"</span>:<span class="hljs-string">"Unauthorized"</span>}

OK, Let’s add Authorization header and check our private endpoint again:

$ curl --header <span class="hljs-string">"Authorization:Bearer eyJraWQiOiJOcmp2S2RlV3hjVXNySFhjcUdUVXJ5OVB2N1RET2Vzek9HcWlERWs3czNNPSIsImFsZyI6IlJTMjU2In0.eyJhdF9oYXNoIjoieWxRYmVjc0lVbF9BM1RHTU1Vb01EdyIsInN1YiI6ImM2NDM2ZjRiLTEwMmUtNDNjNS1hMzFiLTM5YjM4MTgwZDg5MyIsImNvZ25pdG86Z3JvdXBzIjpbInVwbG9hZGVyIl0sImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC5hcC1zb3V0aGVhc3QtMi5hbWF6b25hd3MuY29tXC9hcC1zb3V0aGVhc3QtMl9xcjdHQTZzNVQiLCJjb2duaXRvOnVzZXJuYW1lIjoiYzY0MzZmNGItMTAyZS00M2M1LWEzMWItMzliMzgxODBkODkzIiwicHJlZmVycmVkX3VzZXJuYW1lIjoibGVvbi5rb2xjaGluc2t5QG1hbnRhbHVzLmNvbSIsImdpdmVuX25hbWUiOiJMZW9uIiwiYXVkIjoiM3ZmODB1ZnRmaWVnaXFkMWQ4aWFpaGZicTUiLCJldmVudF9pZCI6ImQ0MmE4OWJkLWI4YjAtNDJjZi1iNmYxLWM0MjE5MGFmMjU4ZiIsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxNTkwMTMyNjE5LCJjdXN0b206dXBsb2FkX2ZvbGRlciI6Imxlb25rb2xjaGluc2t5LXVwbG9hZGZvbGRlciIsImV4cCI6MTU5MDEzNjIxOSwiaWF0IjoxNTkwMTMyNjE5LCJmYW1pbHlfbmFtZSI6IktvbGNoaW5za3kiLCJlbWFpbCI6Imxlb24ua29sY2hpbnNreUBtYW50YWx1cy5jb20ifQ.exrTYXDRrpDHuEyxXCIsXWyYQL6Jf0QEeVmDTWFGJNhL_MmL6Mf3pCZe1-nzYXg5Jp6cUgJ9oG67gHkNvCOO9Fa0cCgslaGJdLe9AMw4zrmSroBY5OtXbm9MUudjt40dG-Y8cgZ0sKydwhzJ6G4Gn78ExgPSklySYPiREKbptDVAIMAwnuU5yYja4-W5G3IlR7gYKJUSwOJSpb_Y-dHETOq1njibWlqc_DU9Aat7Xon84MTCBR51nbRA8mWtC6hbgbVVeiAvY0izodAcWhFVZX9NJ87aIkSeA2ocyKBBCgSw9sV2J99nPz1tLz6JQY5DKX4RhIxMgAm6GXFjCg4hWw"</span> https:<span class="hljs-comment">//yijhzu5fig.execute-api.ap-southeast-2.amazonaws.com/dev/private</span>
{<span class="hljs-string">"statusCode"</span>:<span class="hljs-number">200</span>,<span class="hljs-string">"headers"</span>:{<span class="hljs-string">"x-powered-by"</span>:<span class="hljs-string">"Express"</span>,<span class="hljs-string">"vary"</span>:<span class="hljs-string">"Origin"</span>,<span class="hljs-string">"access-control-allow-credentials"</span>:<span class="hljs-string">"true"</span>,<span class="hljs-string">"content-type"</span>:<span class="hljs-string">"text/html; charset=utf-8"</span>,<span class="hljs-string">"content-length"</span>:<span class="hljs-string">"37"</span>,<span class="hljs-string">"etag"</span>:<span class="hljs-string">"W/\"25-C67pbfBqyV1mc99UfDAOjwGwXWw\""</span>},<span class="hljs-string">"isBase64Encoded"</span>:<span class="hljs-keyword">false</span>,<span class="hljs-string">"body"</span>:<span class="hljs-string">"<h5>Hello Mantalus Private Page!</h5>"</span>}

Much better 🙂

Check out Custom Authorizer logs now – That’s how our generated Access policy looks like:

<span class="hljs-special">{</span>
    "principalId": "c6436f4b-102e-43c5-a31b-39b38180d893",
    "policyDocument": <span class="hljs-special">{</span>
        "Version": "2012-10-17",
        "Statement": <span class="hljs-special">[</span>
            <span class="hljs-special">{</span>
                "Action": "execute-api:Invoke",
                "Effect": "Allow",
                "Resource": "*"
            <span class="hljs-special">}</span>
        <span class="hljs-special">]</span>
    <span class="hljs-special">}</span>,
    "context": <span class="hljs-special">{</span>
        "token": "<span class="hljs-special">{</span><span class="hljs-command">\"</span>at_hash<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>ylQbecsIUl_A3TGMMUoMDw<span class="hljs-command">\"</span>,<span class="hljs-command">\"</span>sub<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>c6436f4b-102e-43c5-a31b-39b38180d893<span class="hljs-command">\"</span>,<span class="hljs-command">\"</span>cognito:groups<span class="hljs-command">\"</span>:<span class="hljs-special">[</span><span class="hljs-command">\"</span>uploader<span class="hljs-command">\"</span><span class="hljs-special">]</span>,<span class="hljs-command">\"</span>iss<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>https://cognito-idp.ap-southeast-2.amazonaws.com/ap-southeast-2_qr7GA6s5T<span class="hljs-command">\"</span>,<span class="hljs-command">\"</span>cognito:username<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>c6436f4b-102e-43c5-a31b-39b38180d893<span class="hljs-command">\"</span>,<span class="hljs-command">\"</span>preferred_username<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>john.doe@example.com<span class="hljs-command">\"</span>,<span class="hljs-command">\"</span>given_name<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>John<span class="hljs-command">\"</span>,<span class="hljs-command">\"</span>aud<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>3vf80uftfiegiqd1d8iaihfbq5<span class="hljs-command">\"</span>,<span class="hljs-command">\"</span>event_id<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>d42a89bd-b8b0-42cf-b6f1-c42190af258f<span class="hljs-command">\"</span>,<span class="hljs-command">\"</span>token_use<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>id<span class="hljs-command">\"</span>,<span class="hljs-command">\"</span>auth_time<span class="hljs-command">\"</span>:1590132619,<span class="hljs-command">\"</span>custom:upload_folder<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>johndoe-uploadfolder<span class="hljs-command">\"</span>,<span class="hljs-command">\"</span>exp<span class="hljs-command">\"</span>:1590136219,<span class="hljs-command">\"</span>iat<span class="hljs-command">\"</span>:1590132619,<span class="hljs-command">\"</span>family_name<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>Doe<span class="hljs-command">\"</span>,<span class="hljs-command">\"</span>email<span class="hljs-command">\"</span>:<span class="hljs-command">\"</span>john.doe@example.com<span class="hljs-command">\"</span><span class="hljs-special">}</span>"
    <span class="hljs-special">}</span>
<span class="hljs-special">}</span>

Tip: When testing protected endpoints from the browser and your origin domain is different from the App domain – make sure to whitelist your origin in your App similar to what I’ve done in private.js (Well it’s just an example but you get the drift):

<span class="hljs-keyword">var</span> whitelist = [
    <span class="hljs-symbol">'http</span>:<span class="hljs-comment">//localhost:3000',</span>
    <span class="hljs-symbol">'http</span>:<span class="hljs-comment">//localhost'</span>
]

It’s a basic stuff but easy to overlook with all the other Authentication parts you offload to APIGW and Custom Authorizer. Another reason why I’m bringing it up here is because I’ve seen it time and time again with Devs bringing this up – “But I have set the ‘Access-Control-Allow-Origin’ to * which should cover all cases…
Well not exactly. I understand why people overlook it as it’s natural to think that wildcard ‘*’ will cover all cases. But if you read the doco you’ll see that:

The value of “*” is special in that it does not allow requests to supply credentials, meaning it does not allow HTTP authentication, client-side SSL certificates, or cookies to be sent in the cross-domain request.

That means that you have to explicitly whitelist you origin, i.e. ‘Access-Control-Allow-Origin’: ‘http://localhost:3000’

Btw, if you don’t want to use “express framework” demo App above, you could use something as simple as this:

handler.js

<span class="hljs-pi">'use strict'</span>;

<span class="hljs-keyword">const</span> response = {
  statusCode: <span class="hljs-number">200</span>,
  headers: {
    <span class="hljs-string">'Access-Control-Allow-Origin'</span>: <span class="hljs-string">'*'</span>,
    <span class="hljs-string">'Access-Control-Allow-Credentials'</span>: <span class="hljs-literal">true</span>,
  },
  body: <span class="hljs-built_in">JSON</span>.stringify({
    cartoons: [
      {
        id: <span class="hljs-number">1</span>,
        name: <span class="hljs-string">'Hello Mantalus'</span>,
        address: <span class="hljs-string">'123 Grange Road'</span>,
      },
    ],
  }),
};

<span class="hljs-comment">// Public API</span>
<span class="hljs-built_in">module</span>.exports.publicEndpoint = (event, context, cb) => {
  <span class="hljs-built_in">console</span>.log(event);
  cb(<span class="hljs-literal">null</span>, response);
};

<span class="hljs-comment">// Private API</span>
<span class="hljs-built_in">module</span>.exports.privateEndpoint = (event, context, cb) => {
  <span class="hljs-built_in">console</span>.log(event);
  cb(<span class="hljs-literal">null</span>, response);
};

And in the serverless.yml demo App example replace functions’ definitions with:

functions:
  publicEndpoint:
    <span class="hljs-operator"><span class="hljs-keyword">handler</span>: <span class="hljs-keyword">handler</span>.publicEndpoint
    <span class="hljs-keyword">events</span>:
      - http:
          path: api/<span class="hljs-keyword">public</span>
          method: <span class="hljs-keyword">GET</span>
          integration: lambda
          cors: <span class="hljs-literal">true</span>

  privateEndpoint:
    <span class="hljs-keyword">handler</span>: <span class="hljs-keyword">handler</span>.privateEndpoint
    <span class="hljs-keyword">events</span>:
      - http: 
          path: api/private
          method: <span class="hljs-keyword">GET</span>
          integration: lambda
          authorizer:
            type: <span class="hljs-string">"CUSTOM"</span>
            authorizerId:
              Ref: <span class="hljs-string">"apiGatewayAuthorizer"</span>
            resultTtlInSeconds: ${self:provider.resultTtlInSeconds}
          cors: <span class="hljs-literal">true</span>
</span>

That’s all folks 🙂
All you have to do now is write an amazing “Front End UI” that will allow your users to login to your App via Amazon Cognito and fetch data from APIs protected by the Custom Authorizer. A lot of people use “aws-amplify” for that purpose instead of writing their own custom modules.

Hope this walkthrough will save you a few hours of googling and reading the docs.