Introduction

GitHub this note shows how to build and deploy a simple nextjs web app on amplify.

  • cognito userpool identity pool
  • get aws credentials from the identity pool
  • s3 client to upload image to s3 bucket
  • amplify hosting and envrionment variables
amplify nextjs hosting

Project Setup

Let create a new nextjs project

init project
npx create-next-app@latest

Install depdencies for cognito and s3 client

install depdencies
npm i @aws-sdk/client-s3 @aws-sdk/credential-providers

Create .env.local to store enviroment variables locally

.env.local
IDENTITY_POOL_ID=
REGION=
S3_REGION=
BUCKET=

Cognito UserPool

Let create a cdk project and the create a cognito userpool

CognitoUserPool
export class CognitoAuthorizer extends Stack {
  public readonly userPool: string;
  public readonly identityPool: aws_cognito.CfnIdentityPool;
 
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);
 
    const pool = new aws_cognito.UserPool(this, "UserPoolForWebEntest", {
      userPoolName: "UserPoolForWebEntest",
      selfSignUpEnabled: true,
      signInAliases: {
        email: true,
      },
      autoVerify: {
        email: true,
      },
      removalPolicy: RemovalPolicy.DESTROY,
    });
 
    const client = pool.addClient("WebClient", {
      authFlows: {
        userPassword: true,
        adminUserPassword: true,
        custom: true,
        userSrp: true,
      },
      userPoolClientName: "WebClient",
    });
 
    const cognitoIdentityPool = new aws_cognito.CfnIdentityPool(
      this,
      "IdentityPoolForWebEntest",
      {
        allowUnauthenticatedIdentities: true,
        identityPoolName: "IdentityPoolForWebEntest",
        cognitoIdentityProviders: [
          {
            clientId: client.userPoolClientId,
            providerName: pool.userPoolProviderName,
          },
        ],
        // supportedLoginProviders: [],
      }
    );
    this.userPool = pool.userPoolArn;
    this.identityPool = cognitoIdentityPool;
  }
}

Cognito IdentityPool

Let create an identity pool and attach

  • a role for authenticated user
  • a role for unauthenticated user
cognitoIdentityPool
interface CognitoAuthProps extends StackProps {
  cognitoIdentityPool: aws_cognito.CfnIdentityPool;
}
 
export class CognitoAuthRole extends Stack {
  constructor(scope: Construct, id: string, props: CognitoAuthProps) {
    super(scope, id, props);
 
    const role = new aws_iam.Role(this, "RoleForAuthenticatedUserWebEntest", {
      assumedBy: new aws_iam.FederatedPrincipal(
        "cognito-identity.amazonaws.com",
        {
          StringEquals: {
            "cognito-identity.amazonaws.com:aud": props.cognitoIdentityPool.ref,
          },
          "ForAnyValue:StringLike": {
            "cognito-identity.amazonaws.com:amr": "authenticated",
          },
        },
        "sts:AssumeRoleWithWebIdentity"
      ),
      roleName: "RoleForAuthenticatedUserWebEntest",
    });
 
    role.addToPolicy(
      new aws_iam.PolicyStatement({
        effect: aws_iam.Effect.ALLOW,
        actions: ["s3:PutObject", "s3:GetObject"],
        resources: ["arn:aws:s3:::BUCKET_NAME", "arn:aws:s3:::BUCKET_NAME/*"],
      })
    );
 
    // unauth role
    const unauthRole = new aws_iam.Role(
      this,
      "RoleForUnAuthenticatedUserWebEntest",
      {
        assumedBy: new aws_iam.FederatedPrincipal(
          "cognito-identity.amazonaws.com",
          {
            StringEquals: {
              "cognito-identity.amazonaws.com:aud":
                props.cognitoIdentityPool.ref,
            },
            "ForAnyValue:StringLike": {
              "cognito-identity.amazonaws.com:amr": "unauthenticated",
            },
          },
          "sts:AssumeRoleWithWebIdentity"
        ),
        roleName: "RoleForUnAuthenticatedUserWebEntest",
      }
    );
 
    unauthRole.addToPolicy(
      new aws_iam.PolicyStatement({
        effect: aws_iam.Effect.ALLOW,
        actions: ["s3:PutObject", "s3:GetObject"],
        resources: ["arn:aws:s3:::BUCKET_NAME", "arn:aws:s3:::BUCKET_NAME/*"],
      })
    );
 
    const attach = new aws_cognito.CfnIdentityPoolRoleAttachment(
      this,
      "RoleAttachmentForWebEntest",
      {
        identityPoolId: props.cognitoIdentityPool.ref,
        roles: {
          authenticated: role.roleArn,
          unauthenticated: unauthRole.roleArn,
        },
      }
    );
  }
}

Upload Form

Let create a simple upload form

demo/page.tsx
"use client";
 
import { useState } from "react";
import handleForm from "./action";
 
const FormPage = () => {
  const [status, setStatus] = useState<string | null>(null);
  const [message, setMessage] = useState<string | null>(null);
 
  const submit = async (data: FormData) => {
    try {
      const res = await handleForm(data);
      setMessage(res.message);
      setStatus("success");
    } catch (error) {
      console.log(error);
    }
  };
 
  return (
    <div className="dark:bg-slate-800 min-h-screen">
      <div className="mx-auto max-w-4xl px-5">
        {status ? (
          <div className="dark:text-white">Submitted {message}</div>
        ) : (
          <form action={submit}>
            <div className="grid gap-6 mb-6 grid-cols-1">
              <div>
                <label
                  htmlFor="first_name"
                  className="block mb-2 text-sm font-medium dark:text-white"
                >
                  First Name
                </label>
                <input
                  id="first_name"
                  type="text"
                  placeholder="haimtran"
                  name="username"
                  className="text-sm rounded-sm block w-full p-2.5 "
                ></input>
              </div>
              <div>
                <label
                  htmlFor="email"
                  className="block mb-2 text-sm font-medium dark:text-white"
                >
                  Email
                </label>
                <input
                  id="email"
                  type="text"
                  placeholder="hai@entest.io"
                  name="email"
                  className="text-sm rounded-sm w-full p-2.5 "
                ></input>
              </div>
              <div>
                <label
                  htmlFor="upload"
                  className="block mb-2 text-sm font-medium dark:text-white"
                >
                  Upload File
                </label>
                <input
                  id="upload"
                  type="file"
                  name="upload"
                  className="text-sm rounded-sm w-full p-2.5 cursor-pointer dark:bg-white"
                ></input>
              </div>
              <div>
                <button className="bg-orange-400 px-10 py-3 rounded-sm">
                  Submit
                </button>
              </div>
            </div>
          </form>
        )}
      </div>
    </div>
  );
};

Server Function

Let create a server function to handle the upload file

demo/action.ts
"use server";
 
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers";
import { writeFile } from "fs/promises";
import { join } from "path";
 
const s3Client = new S3Client({
  region: process.env.S3_REGION,
  credentials: fromCognitoIdentityPool({
    clientConfig: { region: process.env.REGION },
    identityPoolId: process.env.IDENTITY_POOL_ID as string,
  }),
});
 
const handleForm = async (data: FormData) => {
  const file: File | null = data.get("upload") as unknown as File;
 
  if (!file) {
    throw new Error("No file uploaded");
  }
 
  // file buffer
  const bytes = await file.arrayBuffer();
  const buffer = Buffer.from(bytes);
 
  // write to local
  const path = join("./", file.name);
  await writeFile(path, buffer);
  console.log(`open ${path} to see the upload file`);
 
  // write to s3
  await s3Client.send(
    new PutObjectCommand({
      Bucket: process.env.BUCKET,
      Key: `web-entest/${file.name}`,
      Body: buffer,
    })
  );
 
  return {
    status: "OK",
    message: `${file.name} '${process.env.BUCKET as string}`,
  };
};
 
export default handleForm;

Amplify Hosting

Please ensure to

  • update envrionment variables in Amplify console
  • update the build setting in Amplify console
  • configure custom domain from Amplify console to have your own domain

Here is content of the build.yaml for amplify build

build.yaml
version: 1
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - echo "IDENTITY_POOL_ID=$IDENTITY_POOL_ID" >> .env
        - echo "REGION=$REGION" >> .env
        - echo "S3_REGION=$S3_REGION" >> .env
        - echo "BUCKET=$BUCKET" >> .env
        - npm run build
  artifacts:
    baseDirectory: .next
    files:
      - "**/*"