So far we’ve created the DynamoDB table, S3 bucket, and API parts of our serverless backend. Now let’s add auth into the mix. As we talked about in the previous chapter, we are going to use Cognito User Pool to manage user sign ups and logins. While we are going to use Cognito Identity Pool to manage which resources our users have access to.

Setting this all up can be pretty complicated in Terraform. SST has simple CognitoUserPool and CognitoIdentityPool components to help with this.

Create the Components

Change indicator Add the following to a new file in infra/auth.ts.

import { api } from "./api";
import { bucket } from "./storage";

const region = aws.getRegionOutput().name;

export const userPool = new sst.aws.CognitoUserPool("UserPool", {
  usernames: ["email"]
});

export const userPoolClient = userPool.addClient("UserPoolClient");

export const identityPool = new sst.aws.CognitoIdentityPool("IdentityPool", {
  userPools: [
    {
      userPool: userPool.id,
      client: userPoolClient.id,
    },
  ],
  permissions: {
    authenticated: [
      {
        actions: ["s3:*"],
        resources: [
          $concat(bucket.arn, "/private/${cognito-identity.amazonaws.com:sub}/*"),
        ],
      },
      {
        actions: [
          "execute-api:*",
        ],
        resources: [
          $concat(
            "arn:aws:execute-api:",
            region,
            ":",
            aws.getCallerIdentityOutput({}).accountId,
            ":",
            api.nodes.api.id,
            "/*/*/*"
          ),
        ],
      },
    ],
  },
});

Let’s go over what we are doing here.

  • The CognitoUserPool component creates a Cognito User Pool for us. We are using the usernames prop to state that we want our users to login with their email.

  • We are using addClient to create a client for our User Pool. You create one for each “client” that’ll connect to it. Since we only have a frontend we only need one. You can later add another if you add a mobile app for example.

  • The CognitoIdentityPool component creates an Identity Pool. The attachPermissionsForAuthUsers function allows us to specify the resources our authenticated users have access to.

  • We want them to access our S3 bucket and API. Both of which we are importing from api.ts and storage.ts respectively. We’ll look at this in detail below.

Securing Access

We are creating an IAM policy to allow our authenticated users to access our API. You can learn more about IAM here.

{
  actions: [
    "execute-api:*",
  ],
  resources: [
    $concat(
      "arn:aws:execute-api:",
      region,
      ":",
      aws.getcalleridentityoutput({}).accountid,
      ":",
      api.nodes.api.id,
      "/*/*/*"
    ),
  ],
},

This looks a little complicated but Amazon API Gateway has a format it uses to define its endpoints. We are building that here.

We are also creating a specific IAM policy to secure the files our users will upload to our S3 bucket.

// Policy granting access to a specific folder in the bucket
new iam.PolicyStatement({
  actions: ["s3:*"],
  effect: iam.Effect.ALLOW,
  resources: [
    bucket.bucketArn + "/private/${cognito-identity.amazonaws.com:sub}/*",
  ],
}),

Let’s look at how this works.

In the above policy we are granting our logged in users access to the path private/${cognito-identity.amazonaws.com:sub}/ within our S3 bucket’s ARN. Where cognito-identity.amazonaws.com:sub is the authenticated user’s federated identity id (their user id). So a user has access to only their folder within the bucket. This allows us to separate access to our user’s file uploads within the same S3 bucket.

One other thing to note is that, the federated identity id is a UUID that is assigned by our Identity Pool. This id is different from the one that a user is assigned in a User Pool. This is because you can have multiple authentication providers. The Identity Pool federates these identities and gives each user a unique id.

Add to the Config

Let’s add this to our sst.config.ts.

Change indicator Add this below the await import("./infra/api") line in your sst.config.ts.

const auth = await import("./infra/auth");

return {
  UserPool: auth.userPool.id,
  Region: aws.getRegionOutput().name,
  IdentityPool: auth.identityPool.id,
  UserPoolClient: auth.userPoolClient.id,
};

Here we are importing our new config and the return allows us to print out some useful info about our new auth resources in the terminal.

Add Auth to the API

We also need to enable authentication in our API.

Change indicator Add the following prop into the transform options below the handler: { block in infra/api.ts.

args: {
  auth: { iam: true }
},

So it should look something like this.

// Create the API
export const api = new sst.aws.ApiGatewayV2("Api", {
  transform: {
    route: {
      handler: {
        link: [table],
      },
      args: {
        auth: { iam: true }
      },
    }
  }
});

This tells our API that we want to use AWS_IAM across all our routes.

Deploy Our Changes

If you switch over to your terminal, you will notice that your changes are being deployed.

You should see that the new auth resources are being deployed.

+  Complete
   Api: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com
   ---
   IdentityPool: us-east-1:9bd0357e-2ac1-418d-a609-bc5e7bc064e3
   Region: us-east-1
   UserPool: us-east-1_TYEz7XP7P
   UserPoolClient: 3fetogamdv9aqa0393adsd7viv

Let’s create a test user so that we can test our API.

Create a Test User

We’ll use AWS CLI to sign up a user with their email and password.

Change indicator In your terminal, run.

$ aws cognito-idp sign-up \
  --region <COGNITO_REGION> \
  --client-id <USER_POOL_CLIENT_ID> \
  --username admin@example.com \
  --password Passw0rd!

Make sure to replace COGNITO_REGION and USER_POOL_CLIENT_ID with the Region and UserPoolClient from above.

Now we need to verify this email. For now we’ll do this via an administrator command.

Change indicator In your terminal, run.

$ aws cognito-idp admin-confirm-sign-up \
  --region <COGNITO_REGION> \
  --user-pool-id <USER_POOL_ID> \
  --username admin@example.com

Replace the COGNITO_REGION and USER_POOL_ID with the Region and UserPool from above.

Now that the auth infrastructure and a test user has been created, let’s use them to secure our APIs and test them.