How To Build a Login & Authentication Boilerplate From Scratch Using Amazon Cognito & AWS Amplify

In our recently published medium.com article titled ‘Cognito + Amplify Integration Framework’, we have shared a login & authentication boilerplate that enables our clients to kickstart deployments by quickly interfacing their applications with Cognito.

This is a detailed tutorial that describes how we’ve built this boilerplate from scratch.

First, we will create a new project. You don’t need to install or configure tools like webpack or Babel. All the tools you need are preconfigured (and hidden) so that you can focus on the code.

You’ll need to have Node >= 8.10 on your local development machine (but it’s not required on the server). You can use nvm (macOS/Linux) or nvm-windows to switch Node versions between different projects.

To create a new app, you may choose one of the following methods:

#npx

npx create-react-app aws-cognito-boilerplate --template typescript

(npx comes with npm 5.2+ and higher, see instructions for older npm versions)

Or:

#Yarn

yarn create react-app aws-cognito-boilerplate --template typescript

yarn create is available in Yarn 0.25+

We will continue with Yarn

cd aws-cognito-boilerplate

yarn start

Tutorial

react lauch

AWS Boilerplate Configuration

Now we will configure the project architecture and the tsconfig.json file.

To allow absolute imports and make those imports elegant, we will add a new entry to the compilerOptions of the tsconfig.json file:

{
"compilerOptions": {
….
"noEmit": true,
"jsx": "react",
"baseUrl": "src" // This is the new entry
},
"include": [
"src"
]
}

baseUrl allows us to execute shorter commands in the following format:

Import {something} from ‘shared/utils’

Instead of :

Import {something} from ‘../../shared/utils’

Next we will install the Amplify CLI and the Amplify library to our web application as follows:

yarn global add @aws-amplify/cli

yarn add aws-amplify aws-amplify-react

The Amplify Command Line Interface (CLI) is a unified toolchain to create, integrate, and manage the AWS cloud services for your app. We will use the CLI to initialize our app with Cognito service.

Let’s clean up our project tree:

project tree

By removing unneeded files for the moment:

  • Logo.svg
  • Index.css
  • App.test.tsx
  • App.css

Now refactor index.tsx:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
 
ReactDOM.render(
 
   
 ,
 document.getElementById("root")
);
serviceWorker.unregister();

And App.tsx

import React from "react";
 
const App = () => {
 return (h1)You are logged In(/h1); 
}; 
export default App;

Let’s initialize Amplify with the CLI now.

Before using the CLI, you need to have the credentials for your AWS account in

~/.aws/credentials

Then run:

amplify init

amplyfy init

After you answer the provided questions, you can use the command amplify help at any time to see the overall command structure, and amplify help <category> to see actions for a specific category.

The Amplify CLI uses AWS CloudFormation; you can add or modify configurations locally before you push them for execution in your account. To see the status of the deployment at any time, run amplify status.

You can add some features like enabling users to register for your site and log in. Run amplify add auth and select the Default configuration. This adds the auth resource configurations locally in your amplify/backend/auth directory.

Run amplify push to provision your auth resources in the cloud. The ./src/aws-exports.js file that’s created has all of the appropriate cloud resources defined for your application.

amplify push

Now it’s time to configure Amplify with our configuration file. 

Let’s create a shared folder inside our src folder:

cd src && mkdir shared

Create an amplify config file:

cd shared && touch amplify.config.ts

amplify.config.ts will be used as a global configuration. Add the following inside:

import "@aws-amplify/ui/dist/style.css";
import { UsernameAttributes } from "aws-amplify-react";
 
/**
* You can create your own theme and use it to render Amplify components
* Your custom theme must use the selectors from the following
* template: https://github.com/aws-amplify/amplify-js/blob/master/packages/aws-amplify-react/src/Amplify-UI/Amplify-UI-Theme.tsx
*/
const amplifyTheme = {};
 
/**
* When using custom components you can hide default Amplify components here
* by adding them the amplifyHiddenComponents array
*/
const amplifyHiddenComponents: [] = [];
**
* This object is used by Config.getInstance().init()
* or Amplify.configure() method.
* To add more fields check the following link: https://aws-amplify.github.io/docs/js/authentication#manual-setup
*/
export const amplifyConfig = {
 Auth: {
   identityPoolId: process.env.REACT_APP_IDENTITY_POOL_ID,
   region: process.env.REACT_APP_REGION,
   userPoolId: process.env.REACT_APP_USER_POOL_ID,
   userPoolWebClientId: process.env.REACT_APP_USER_POOL_WEBCLIENT_ID,
 },
 language: "us",
};
 
/**
* This object is used by Authenticator signUpConfig property
* to configure signUp form. Custom field is a flag which indicates whether or not the field is ‘custom’ in the User Pool.
* For more informations check the following link: https://aws-amplify.github.io/docs/js/react#signup-configuration
*/
export const signUpCustomConfig = {
 hiddenDefaults: ["phone_number"],
 signUpFields: [
   {
     label: "Email",
     key: "email",
     required: true,
     displayOrder: 1,
     type: "string",
     custom: false,
     placeholder: "Enter your email",
   },
   {
     label: "Password",
     key: "password",
     required: true,
     displayOrder: 2,
     type: "password",
     custom: false,
     placeholder: "Enter your password",
   },
 ],
};
 
/**
* Used by Authenticator component as configuration
*/
export const authenticatorConfig = {
 theme: amplifyTheme,
 usernameAttributes: UsernameAttributes.EMAIL,
 hide: amplifyHiddenComponents,
 signUpConfig: signUpCustomConfig,
};

Create a utils file. This is where we will write our configuration functions.

touch utils.ts

And add the following:

import Amplify from "aws-amplify";
 
/**
* This function is used to control if the received
* state is equal to the signedIn value.
*
* TODO: We can use lazy loading based on the return of isAuthenticated function.
* Until the user is not connected we don't load unecessary component,
* basically the App components in this context.
* @param {string} state authState returned by onStateChange
*/
export const isAuthenticated = (state: string) => {
 return state === 'signedIn';
};
 
/**
* This class is used for project configuration.
* It follow a Singleton pattern.
* @extends Amplify Extends Amplify class
*/
export class Config extends Amplify {
 private static instance: Config;
 
 private constructor() {
   super();
 }
 
 /**
  * This static method returns class instance if exist,
  * otherwise it returns a new instance
  */
 public static getInstance(): Config {
   if (!Config.instance) {
     Config.instance = new Config();
   }
   return Config.instance;
 }
 
 /**
  * This method initialize Amplify configuration
  * @example
  * Config.getInstance().init(amplifyConfig)
  */
 // @ts-ignore
 public async init(amplifyConfig) {
   try {
     /**
      * We unfornately have to apply this workaround because Webpack performs
      * a static analyse at build time. It doesn't try to infer variables.
      * For more information check this issue: https://github.com/webpack/webpack/issues/6680#issuecomment-370800037
      */
     const name = 'aws-exports'
     const module = await import(`../${name}`);
     Config.configure(module.default);
   } catch (error) {
     Config.configure(amplifyConfig);
     Config.I18n.setLanguage(amplifyConfig.language || "us");
   }
 }
}

With this class we can use the amplify configuration generated by the CLI or our custom configuration file object inside amplify.config.ts

Now it’s time to refactor our index.tsx file.

import React from "react";
import ReactDOM from "react-dom";
import { Authenticator } from "aws-amplify-react";
import * as serviceWorker from "./serviceWorker";
import { Config, isAuthenticated } from "shared/utils";
import { amplifyConfig, authenticatorConfig } from "shared/amplify.config";
import App from "App";
 
const Guard = ({ authState }: { authState?: string }) => {
 if (isAuthenticated(authState || "")) {
   return ;
 }
 return null;
};
 
const CognitoBoilerplate = () => {
 return (
   
     
       
     
   
 );
};
 
// Wait for Amplify configuration apply
(async () => {
 await Config.getInstance().init(amplifyConfig);
 ReactDOM.render(, document.getElementById("root"));
})();
serviceWorker.unregister();

It will look like this now:boilerplate first view

Let’s add some css inside our public/index.html file at the beginning of the <head> tag


And edit the amplify.config.ts file:

const amplifyTheme = {
 container: {
   backgroundColor: "grey",
   display: "flex",
   justifyContent: "center",
 },
};

It looks a little better:

boilerplate view 2 1 boilerplate view 3 boilerplate view 4

Now you can create your own account and sign in:

5th view

UI Customization

To customize the components and reuse the render logic along with the built-in methods, we need to extend the original components provided by Amplify using React inheritance.

Let’s create a new folder inside our shared folder:

mkdir -p components/Auth

cd components/Auth

And create a custom SignIn component: 

touch SignUp.tsx

Let’s first create a class that extends the desired component, in our case, the Sign In component:

import React from "react";
import { SignIn } from "aws-amplify-react";
 
class CustomSignIn extends SignIn {
 constructor(props: any) {
   super(props);
 }
}
 
export default CustomSignIn;

Now, to use our custom component, we have to edit our amplify.config.ts file:

import { ReactNode } from "react";
const amplifyHiddenComponents: ReactNode[] = [SignUp];

Authenticator is designed as a container for a number of Auth components. Each component does a single job, e.g., SignIn, SignUp, etc. By default, all of these elements are visible depending on the authentication state.

If you want to replace some or all of the Authenticator elements, you need to set hideDefault={true}, or if you want to hide only specific components, do it like we did previously so the component doesn’t render its default view. Then you can pass in your own set of child components that listen to authState and decide what to do.

Add your custom sign In component as a child au <Authenticator /> component like the example below:

import React from "react";
import ReactDOM from "react-dom";
import { Authenticator } from "aws-amplify-react";
import * as serviceWorker from "./serviceWorker";
import { Config, isAuthenticated } from "shared/utils";
import { amplifyConfig, authenticatorConfig } from "shared/amplify.config";
import App from "App";
import CustomSignIn from "shared/components/Auth/SignUp";
 
const CognitoBoilerplate = () => {
 return (
   
     
       
       
     
   
 );
};

The type of props is ISignInProps so we have to import it.

We initialize the state because loading is not initialized inside the parent component. We will use the loading state to disable the button when the request is sent.

this._validAuthStates = [‘signedIn’];

The options above are used to control the condition for the Authenticator component to render components.

import React from "react";
import { ISignInProps } from "aws-amplify-react/lib-esm/Auth/SignIn";
import { SignIn } from "aws-amplify-react";
 
class CustomSignIn extends SignIn {
 constructor(props: ISignInProps) {
   super(props);
   this._validAuthStates = ["signIn", "signedOut", "signedUp"];
   this.state = {
     loading: false,
   };
 }
}
 
export default CustomSignIn;

Our CustomSignIn component will be visible when the authState is equal to one of the valid auth states.

Due to inheritance, we have access to parent props, state and methods. Keep in mind that React has a powerful composition model, and it is recommended to use composition instead of inheritance to reuse code between components. In our case, however, we need to inherit aws-amplify-react components.

Now in the component’s constructor, implement showComponent(theme) {} in lieu of the typical render() {} method.

theme is not typed; you can see our custom interfaces at the end of this article by following the Github repository link

import React from "react";
import { ISignInProps } from "aws-amplify-react/lib-esm/Auth/SignIn";
 
import React from "react";
import { ISignInProps } from "aws-amplify-react/lib-esm/Auth/SignIn";
import { SignIn } from "aws-amplify-react";
 
class CustomSignIn extends SignIn {
 constructor(props: ISignInProps) {
   super(props);
   this._validAuthStates = ["signIn", "signedOut", "signedUp"];
   this.state = {
     loading: false,
   };
 }
 
 showComponent(theme: any) {
   return (
     (form>
       (label style={{color: 'white', fontSize: '50px'}}>FORM(/label>
     (/form>
   );
 }
}
export default CustomSignIn;
(you have to replace “(” by “<” in the HTML code, the code embedder we use doesn’t support that)

form view

Now create the custom form using the inherited methods from the parent:

import React, { MouseEvent } from "react";
import { SignIn, UsernameAttributes } from "aws-amplify-react";
import { ISignInProps } from "aws-amplify-react/lib-esm/Auth/SignIn";
 
class CustomSignIn extends SignIn {
 constructor(props: ISignInProps) {
   super(props);
   this._validAuthStates = ["signIn", "signedOut", "signedUp"];
   this.state = {
     loading: false,
   };
 }
 
 showComponent(theme: any) {
   return (
     (form>
       (div>
         (label
           style={{ color: "white", fontSize: "30px" }}
           htmlFor={this.props.usernameAttributes}
         >
           Email *
         (/label>
         (br />
 
         (input
           autoFocus={true}
           type={this.props.usernameAttributes}
           id={this.props.usernameAttributes}
           key={this.props.usernameAttributes}
           name={this.props.usernameAttributes}
           placeholder={
             this.props.usernameAttributes === UsernameAttributes.EMAIL
               ? "Your email address"
               : "Your username"
           }
           onChange={this.handleInputChange}
         />
         (br />
         (br />
       (/div>
       (div>
         (label
           style={{ color: "white", fontSize: "30px" }}
           htmlFor="password"
         >
           Password *
         (/label>
 
         (br />
         (input
           type="password"
           id="password"
           key="password"
           name="password"
           placeholder="Your password"
           onChange={this.handleInputChange}
         />
         (br />
         (br />
       (/div>
       (button
         disabled={this.state.loading}
         onClick={(ev: MouseEvent) => this.signIn(ev)}
       >
         Sign in
       (/button>
       (br />
       (a
         style={{ color: "white", fontSize: "15px" }}
         onClick={() => this.changeState("signUp")}
       >
         Don't have an account? Sign up
       (/a>
     (/form>
   );
 }
}
 
export default CustomSignIn;

new custom form 1

Now you can customize your component completely by retaining aws-amplify-react component logic.

boilerplate last view c

Here is an example of how you can customize your UI’s logo using React inheritance.

To add your logo, you need to display a logo on the Auth components. Often, this logo comes from a URL and thus you need to have a logoUrl property of type “string”. Since you cannot add new properties, you have to pass a component as a child of our component as a workaround.

{/**
        * As a workaround we use a children because we can't add
        * more props from the time that SignIn class is not a generic class.
        */}
       {this.props.children}

trackit customized view

GitHub link for the boilerplate: https://github.com/trackit/aws-cognito-boilerplate

You will find the project entirely typed along with styled-components.