Logo

dev-resources.site

for different kinds of informations.

Using Amazon Cognito with the user-password flow

Published at
7/31/2024
Categories
aws
security
sam
cognito
Author
andmoredev
Categories
4 categories in total
aws
open
security
open
sam
open
cognito
open
Author
10 person written this
andmoredev
open
Using Amazon Cognito with the user-password flow

On my post called Secure API Gateway with Amazon Cognito using SAM I talked about different Auth terms and walked through a setup to use the Client Credentials Flow, but Cognito recently introduced pricing changes for machine-to-machine authentication that will make this cost us and my main goal is to do this while staying in the free tier for personal projects that will not be generating any income. That is why in this post I am going to setup Amazon Cognito using a different flow called user password-based authentication. With this type of authentication we are charged based on the Monthly Active Users (MAUs) and AWS gives you the first 50,000 MAUs for free, and in my case this will usually stay at 1 per project, so I should be fine.

How does the user-password flow work?

Initially I thought I could use Basic Auth where you provide the encoded username and password in the Authorization header but that is not the case. In the image below I have a picture that shows the interactions that happen to get an authenticated request using this flow.

Authentication flow interactions

Let's dive deeper into each interaction

  1. Caller makes a request with the username and password to your API.
  2. API makes a call to Cognito to get the Access, Id and refresh tokens and returns it to the user
  3. User provides Id token in Authorization header with each API call.
  4. API verifies token with Cognito to make sure it's valid.

Setting it all up with SAM

We are going to stick with the same architecture for the Amazon Cognito resources to simplify each APIs configuration and to be able to manage users from a single location.

CloudFormation Stack Architecture

Now let's see how each of these pieces are set up using SAM.

Auth Stack

This stack has the resources that will be consumed by our API stacks.
Full auth stack setup can be found in this GitHub repository

1. Updates to the User Pool

Unlike our M2M setup that didn't need any properties for the user pool, for this type of authentication we need to setup properties that are specifying the attributes to be hosted for the user such as email, names, etc.

  CognitoUserPool:
    Type: AWS::Cognito::UserPool
+   Properties:
+     UsernameAttributes:
+       - email
+     AutoVerifiedAttributes:
+       - email
+     VerificationMessageTemplate:
+       DefaultEmailOption: CONFIRM_WITH_LINK
+     EmailConfiguration:
+       EmailSendingAccount: COGNITO_DEFAULT
Enter fullscreen mode Exit fullscreen mode

The user pool attributes are:

  • UsernameAttributes - this is specifying what you allow as a user name. The options here are email or phone_number.
  • AutoVerifiedAttributes - attributes that you allow to be verified automatically by Cognito. For example we are using email this means Cognito will automatically send a verification email to the user. If we didn't set this attribute an administrator would have to manually verify users in Cognito.
  • VerificationMessageTemplate - This is where you can set your own template for the email that will get sent to users to verify. For this example we are using a default option provided by Cognito where they will confirm by clicking a link. The other option is CONFIRM_WITH_CODE where a user will receive a code and they have to enter it manually to verify.
  • EmailConfiguration - This property allows us to setup the configuration for the sender email for the verification or any other communications happening from Cognito. In our case we are using COGNITO_DEFAULT which reduces the amount of setup we need to get an Amazon SES verified email. The COGNITO_DEFAULT has some limits that you will need to consider if you are using it.

API Stack

Our API Stack is simplified when using this flow. Why? We do not need to create a resource server since we will not be using OAuth capabilities.

1. Delete UserPoolResourceServer

As mentioned before, we don't need this resource anymore. So let's get rid of it from our stack by removing it from the template.

2. User Pool Client Updates

Below is the definition for our user pool client.

  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      UserPoolId: !Ref CognitoUserPoolId
-     GenerateSecret: true
-     AllowedOAuthFlows:
-       - client_credentials
-     AllowedOAuthScopes:
-       - layerless-esbuild/api
-     AllowedOAuthFlowsUserPoolClient: true
+     SupportedIdentityProviders:
+       - COGNITO
+     ExplicitAuthFlows:
+       - ALLOW_USER_PASSWORD_AUTH
+       - ALLOW_REFRESH_TOKEN_AUTH
Enter fullscreen mode Exit fullscreen mode

A few changes need to be done in order for this authentication flow to work.

  • GenerateSecret - We first get rid of this property since it is not needed for this flow.
  • AllowedOAuthFlows - When using the User/Password auth flow we do not need to set this property.
  • AllowedOAuthScopes - Since we are not using an OAuth flow we do not need to set up scopes.
  • SupportedIdentityProviders - We are going to use COGNITO as our only provider. You can configure different identity providers to simplify your users sign in by using Google, Facebook or any of the supported providers.
  • ExplicitAuthFlows: this is where we will configure the ALLOW_USER_PASSWORD_AUTH which will allow us to authenticate using a username and a password. I've added ALLOW_REFRESH_TOKEN_AUTH since it's required but we will not be going to do token refreshes in this post.

4. Updates to API Gateway

The only thing that needs to change in the API Gateway is the removal of the AuthorizationScopes. For this flow this is not needed.

  Auth:
    DefaultAuthorizer: ClientCognitoAuthorizer
    Authorizers:
      ClientCognitoAuthorizer:
        UserPoolArn: !Ref CognitoUserPoolArn
-       AuthorizationScopes:
-         - layerless-esbuild/echo
Enter fullscreen mode Exit fullscreen mode

THAT'S IT!! We have now successfully setup our API to authenticate using the user-password flow.

You can find the full working example in this GitHub repository

Testing with Postman

1. Grab Client Id

We only need the client id when authenticating with the user-password flow, this is because we are going to enter a username and password to authenticate and that is where the token will be coming from. So we will get this value from the console by going into our new application client.

AWS Console showing an application client in Cognito where we can grab the client id

2. Requesting auth tokens

To request the tokens we need to call Amazon Cognito specifying we are doing an InitiateAuth command.
This will require us to make a POST call to https://cognito-idp.us-east-1.amazonaws.com. The region will change depending on where you created your User Pool. We specify the command by adding the X-Amz-Target, we also need to specify the Content-Type since it's an AWS specific type. Below is a screenshot that shows how the headers should look.

Postman request headers

The body requires the following parameters:

  • AuthFlow - This is where we specify that we want to use the user-password flow by setting a value of USER_PASSWORD_AUTH
  • ClientId - We will set the value to the one we grabbed in step 1.
  • AuthParameters - in the object we will provide the USERNAME and the PASSWORD of our user.

Postman request body

When you send this request you should get a response back with the AccessToken, IdToken, RefreshToken, TokenType and ExpiresIn. This also returns the ChallengeParameters but this flow does not require any challenge responses to be generated. (I've edited the full response from the picture to not expose the full keys).

Postman response body

5. Make request

Grabbing the IdToken from the response we got from the request above we can now take that and use it to authorize the request to the API endpoint as seen below.

Postman auth config and successful request

Authenticating automation calls

Whenever I change authentication methods I get stuck setting it all up for my test automation. This requires us to do the same thing we did in Postman but programmatically. I am doing this by running a small NodeJS script that will make the call as shown below.

const initiateAuthResponse = await axios({
    url: process.env.COGNITO_URL,
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-amz-json-1.1',
      'X-Amz-Target': 'AWSCognitoIdentityProviderService.InitiateAuth'
    },
    data: JSON.stringify({
      ClientId: process.env.CLIENT_ID,
      AuthFlow: 'USER_PASSWORD_AUTH',
      AuthParameters: {
        USERNAME: process.env.USERNAME,
        PASSWORD: process.env.PASSWORD
      }
    })
  });


  const token = initiateAuthResponse.data.AuthenticationResult.IdToken;
Enter fullscreen mode Exit fullscreen mode

If you look at the code above it should seem very familiar to what we did in Postman. Making a POST request with the necessary headers and body. Once you get the response you can do whatever you need with the tokens.

I've provided 2 examples on how to handle the user credentials for this script to use within a CI/CD pipeline.

  1. Create user programmatically - In this example we are creating a Cognito user programmatically and using the credentials to authenticate. We do this by setting the values as environment variables using the >> $GITHUB_ENV command. At the end of the run we delete the user so we don't have an endless amount of orphaned automation users.
  test-api-with-user-password-auth-inline-create-user:
    name: Run Portman With USER_PASSWORD_AUTH - Inline Create User
    runs-on: ubuntu-latest
    environment: ${{ inputs.ENVIRONMENT }}
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: us-east-1
          role-to-assume: ${{ secrets.PIPELINE_EXECUTION_ROLE }}
          role-session-name: create-test-user
          role-duration-seconds: 3600
          role-skip-session-tagging: true

      - name: Create Test User
        run: |
          username=$(uuidgen)@andmore.dev
          password=$(uuidgen)G1%
          echo "USERNAME=$username" >> $GITHUB_ENV;
          echo "PASSWORD=$password" >> $GITHUB_ENV;
          aws cognito-idp admin-create-user --user-pool-id ${{ inputs.COGNITO_USER_POOL_ID }} --username $username --message-action SUPPRESS
          aws cognito-idp admin-set-user-password --user-pool-id ${{ inputs.COGNITO_USER_POOL_ID }} --username $username  --password $password --permanent

      - name: Test API
        env:
          COGNITO_URL: ${{ secrets.COGNITO_URL }}
          CLIENT_ID: ${{ secrets.COGNITO_CLIENT_ID }}
        run: |
          npm ci

          node ./portman/get-auth-token/user-password-auth.mjs
          npx @apideck/portman --cliOptionsFile portman/portman-cli.json --baseUrl ${{ inputs.BASE_URL }}

      - name: Delete Test User
        run: |
          aws cognito-idp admin-delete-user --user-pool-id ${{ inputs.COGNITO_USER_POOL_ID }} --username $USERNAME
Enter fullscreen mode Exit fullscreen mode
  1. Use GitHub Secrets - This is one of the simpler routes but requires to have a user already set up. All you need to do is store the credentials in GitHub secrets and reference them in the workflow.
  test-api-with-user-password-auth-github-secrets:
    name: Run Portman With USER_PASSWORD_AUTH - Load GitHub Secrets
    runs-on: ubuntu-latest
    environment: ${{ inputs.ENVIRONMENT }}
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: us-east-1
          role-to-assume: ${{ secrets.PIPELINE_EXECUTION_ROLE }}
          role-session-name: create-test-user
          role-duration-seconds: 3600
          role-skip-session-tagging: true

      - name: Test API
        env:
          COGNITO_URL: ${{ secrets.COGNITO_URL }}
          CLIENT_ID: ${{ secrets.COGNITO_CLIENT_ID }}
          USERNAME: ${{ secrets.COGNITO_USERNAME }}
          PASSWORD: ${{ secrets.COGNITO_PASSWORD }}
        run: |
          npm ci

          node ./portman/get-auth-token/user-password-auth.mjs
          npx @apideck/portman --cliOptionsFile portman/portman-cli.json --baseUrl ${{ inputs.BASE_URL }}
Enter fullscreen mode Exit fullscreen mode

There are two considerations to take into account here. If you are going to have a user per repository it might become a burden to maintain these users as your applications and repositories grow. On the other hand, if you share a single user with all your repos you might be introducing security vulnerabilities since it will be easier to have this password compromised.

Wrap Up

In this post we were able to update our authentication to use the user-password flow instead of M2M for our APIs, this allows us to stay within the Cognito free tier. We were able to verify that we can still authenticate with Postman and our test automations. Hopefully this shows the flexibility there is with Cognito and how you can configure it differently to satisfy your use cases. I will be exploring the other authentication flows so you can easily choose the one that makes more sense for you.

sam Article's
30 articles in total
Favicon
Running lambdas locally using Javascript/Node.js
Favicon
Cut Your AWS Lambda Logging Costs: Filter Logs with AWS SAM
Favicon
Building a "Real-Time" Data Integration Platform on AWS
Favicon
Using Amazon Cognito with the user-password flow
Favicon
SAM Registration and Maintenance Ensuring Your Business Stays Compliant
Favicon
Utilizing the System for Award Management SAM for Government Contracting Success
Favicon
Secure API Gateway with Amazon Cognito using SAM
Favicon
Resources and Properties for AWS SAM
Favicon
Adding Cognito Authentication to our Serverless Dash App
Favicon
Using YAML anchors and aliases in a SAM template
Favicon
First impressions of CloudFormation’s IaC generator and CDK migrate
Favicon
Building Scalable Serverless Applications with AWS SQS and Lambda using SAM
Favicon
How to add CI/CD to my SAM project
Favicon
How to create serverless applications with AWS SAM (Serverless Application Model)
Favicon
Introduction to AWS SAM (Serverless Application Model)
Favicon
Help! How do I set DeletionPolicy to Retain for production only?
Favicon
An efficient way to build your serverless microservices. Part 3. CI/CD with AWS SAM.
Favicon
Leveraging Infrastructure as Code (IaC) for AWS Lambda: A Comparative Analysis of AWS SAM, Terraform, and Serverless Framework
Favicon
AWS Lambda with Rust and SAM
Favicon
Deploying Lambdas with AWS SAM & GitHub Actions: Step by Step
Favicon
Speed up new serverless application development with customized SAM templates
Favicon
Streamline AWS Development with CI/CD, SAM, and GitHub Actions
Favicon
AWS sam #3: sam local + ApiGateway Lambda authorizer
Favicon
✨ Porting Lambda Functions to AWS SAM
Favicon
Store Thumbnails from Your Live Stream Using AWS SAM CLI to Set Up Lambda Function and API Gateway
Favicon
AWS sam #2: sam local + logs
Favicon
AWS sam #1: sam local + DynamoDB
Favicon
Event-driven file management using S3 Notifications and Step Functions
Favicon
Folding as a Service with AWS StepFunctions
Favicon
Elevating Your Serverless Development with AWS SAM

Featured ones: