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 CDK. SST has a simple Auth construct to help with this.

Create a Stack

Change indicator Add the following to a new file in stacks/AuthStack.ts.

import { ApiStack } from "./ApiStack";
import * as iam from "aws-cdk-lib/aws-iam";
import { StorageStack } from "./StorageStack";
import { Cognito, StackContext, use } from "sst/constructs";

export function AuthStack({ stack, app }: StackContext) {
  const { api } = use(ApiStack);
  const { bucket } = use(StorageStack);

  // Create a Cognito User Pool and Identity Pool
  const auth = new Cognito(stack, "Auth", {
    login: ["email"],
  });

  auth.attachPermissionsForAuthUsers(stack, [
    // Allow access to the API
    api,
    // 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}/*",
      ],
    }),
  ]);

  // Show the auth resources in the output
  stack.addOutputs({
    Region: app.region,
    UserPoolId: auth.userPoolId,
    UserPoolClientId: auth.userPoolClientId,
    IdentityPoolId: auth.cognitoIdentityPoolId,
  });

  // Return the auth resource
  return {
    auth,
  };
}

Let’s go over what we are doing here.

  • We are creating a new stack for our auth infrastructure. While we don’t need to create a separate stack, we are using it as an example to show how to work with multiple stacks.

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

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

  • This new AuthStack references the bucket resource from the StorageStack and the api resource from the ApiStack that we created previously.

  • And we want them to access our S3 bucket. We’ll look at this in detail below.

  • Finally, we output the ids of the auth resources that have been created and returning the auth resource so that other stacks can access this resource.

Securing Access to Uploaded Files

We are 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 App

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

Change indicator Replace the stacks function with this line that adds the AuthStack into our list of stacks.

stacks(app) {
  app.stack(StorageStack).stack(ApiStack).stack(AuthStack);
},

Change indicator And import the new stack at the top of the file.

import { AuthStack } from "./stacks/AuthStack";

Add Auth to the API

We also need to enable authentication in our API.

Change indicator Add the following prop into the defaults options above the function: { line in stacks/ApiStack.ts.

authorizer: "iam",

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 stack is being deployed.

✓  Deployed:
   StorageStack
   ApiStack
   ApiEndpoint: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com
   AuthStack
   IdentityPoolId: us-east-1:9bd0357e-2ac1-418d-a609-bc5e7bc064e3
   Region: us-east-1
   UserPoolClientId: 3fetogamdv9aqa0393adsd7viv
   UserPoolId: us-east-1_TYEz7XP7P

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 UserPoolClientId 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 UserPoolId 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.