TrackIt
TrackIt
Contact us
Blogs

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

Author

TrackIt

Date Published

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

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

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

Or:

#Yarn

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

yarn create is available in Yarn 0.25+

We will continue with Yarn

1cd aws-cognito-boilerplate
2
3yarn start
4
5

Tutorial


 

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:

1{
2"compilerOptions": {
3.
4"noEmit": true,
5"jsx": "react",
6"baseUrl": "src" // This is the new entry
7},
8"include": [
9"src"
10]
11}

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

1Import {something} from ‘shared/utils’
2

Instead of :

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

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

1yarn global add @aws-amplify/cli
2
3yarn 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:


By removing unneeded files for the moment:

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

Now refactor index.tsx:

1import React from "react";
2import ReactDOM from "react-dom";
3import App from "./App";
4import * as serviceWorker from "./serviceWorker";
5
6ReactDOM.render(
7
8
9 ,
10 document.getElementById("root")
11);
12serviceWorker.unregister();

And App.tsx

1import React from "react";
2
3const App = () => {
4 return (h1)You are logged In(/h1);
5};
6export default App;
7

Let’s initialize Amplify with the CLI now.

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

1~/.aws/credentials

Then run:

1amplify 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.


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

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

1cd src &amp;&amp; mkdir shared

Create an amplify config file:

1cd shared &amp;&amp; touch amplify.config.ts

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

1import "@aws-amplify/ui/dist/style.css";
2import { UsernameAttributes } from "aws-amplify-react";
3
4/**
5* You can create your own theme and use it to render Amplify components
6* Your custom theme must use the selectors from the following
7* template: https://github.com/aws-amplify/amplify-js/blob/master/packages/aws-amplify-react/src/Amplify-UI/Amplify-UI-Theme.tsx
8*/
9const amplifyTheme = {};
10
11/**
12* When using custom components you can hide default Amplify components here
13* by adding them the amplifyHiddenComponents array
14*/
15const amplifyHiddenComponents: [] = [];
16**
17* This object is used by Config.getInstance().init()
18* or Amplify.configure() method.
19* To add more fields check the following link: https://aws-amplify.github.io/docs/js/authentication#manual-setup
20*/
21export const amplifyConfig = {
22 Auth: {
23 identityPoolId: process.env.REACT_APP_IDENTITY_POOL_ID,
24 region: process.env.REACT_APP_REGION,
25 userPoolId: process.env.REACT_APP_USER_POOL_ID,
26 userPoolWebClientId: process.env.REACT_APP_USER_POOL_WEBCLIENT_ID,
27 },
28 language: "us",
29};
30
31/**
32* This object is used by Authenticator signUpConfig property
33* to configure signUp form. Custom field is a flag which indicates whether or not the field is ‘custom’ in the User Pool.
34* For more informations check the following link: https://aws-amplify.github.io/docs/js/react#signup-configuration
35*/
36export const signUpCustomConfig = {
37 hiddenDefaults: ["phone_number"],
38 signUpFields: [
39 {
40 label: "Email",
41 key: "email",
42 required: true,
43 displayOrder: 1,
44 type: "string",
45 custom: false,
46 placeholder: "Enter your email",
47 },
48 {
49 label: "Password",
50 key: "password",
51 required: true,
52 displayOrder: 2,
53 type: "password",
54 custom: false,
55 placeholder: "Enter your password",
56 },
57 ],
58};
59
60/**
61* Used by Authenticator component as configuration
62*/
63export const authenticatorConfig = {
64 theme: amplifyTheme,
65 usernameAttributes: UsernameAttributes.EMAIL,
66 hide: amplifyHiddenComponents,
67 signUpConfig: signUpCustomConfig,
68};
69

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

1touch utils.ts

And add the following:

1import Amplify from "aws-amplify";
2
3/**
4* This function is used to control if the received
5* state is equal to the signedIn value.
6*
7* TODO: We can use lazy loading based on the return of isAuthenticated function.
8* Until the user is not connected we don't load unecessary component,
9* basically the App components in this context.
10* @param {string} state authState returned by onStateChange
11*/
12export const isAuthenticated = (state: string) =&gt; {
13 return state === 'signedIn';
14};
15
16/**
17* This class is used for project configuration.
18* It follow a Singleton pattern.
19* @extends Amplify Extends Amplify class
20*/
21export class Config extends Amplify {
22 private static instance: Config;
23
24 private constructor() {
25 super();
26 }
27
28 /**
29 * This static method returns class instance if exist,
30 * otherwise it returns a new instance
31 */
32 public static getInstance(): Config {
33 if (!Config.instance) {
34 Config.instance = new Config();
35 }
36 return Config.instance;
37 }
38
39 /**
40 * This method initialize Amplify configuration
41 * @example
42 * Config.getInstance().init(amplifyConfig)
43 */
44 // @ts-ignore
45 public async init(amplifyConfig) {
46 try {
47 /**
48 * We unfornately have to apply this workaround because Webpack performs
49 * a static analyse at build time. It doesn't try to infer variables.
50 * For more information check this issue: https://github.com/webpack/webpack/issues/6680#issuecomment-370800037
51 */
52 const name = 'aws-exports'
53 const module = await import(`../${name}`);
54 Config.configure(module.default);
55 } catch (error) {
56 Config.configure(amplifyConfig);
57 Config.I18n.setLanguage(amplifyConfig.language || "us");
58 }
59 }
60}
61

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.

1import React from "react";
2import ReactDOM from "react-dom";
3import { Authenticator } from "aws-amplify-react";
4import * as serviceWorker from "./serviceWorker";
5import { Config, isAuthenticated } from "shared/utils";
6import { amplifyConfig, authenticatorConfig } from "shared/amplify.config";
7import App from "App";
8
9const Guard = ({ authState }: { authState?: string }) =&gt; {
10 if (isAuthenticated(authState || "")) {
11 return ;
12 }
13 return null;
14};
15
16const CognitoBoilerplate = () =&gt; {
17 return (
18
19
20
21
22
23 );
24};
25
26// Wait for Amplify configuration apply
27(async () =&gt; {
28 await Config.getInstance().init(amplifyConfig);
29 ReactDOM.render(, document.getElementById("root"));
30})();
31serviceWorker.unregister();
32

It will look like this now:

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

1
2 * {
3 box-sizing: border-box;
4 }
5 html,
6 body,
7 #root,
8 #root > div {
9 margin: 0;
10 padding: 0;
11 display: flex;
12 flex-direction: column;
13 min-height: 100vh;
14 font-family: 'Montserrat', sans-serif;
15 }
16
17

And edit the amplify.config.ts file:

1const amplifyTheme = {
2 container: {
3 backgroundColor: "grey",
4 display: "flex",
5 justifyContent: "center",
6 },
7};

It looks a little better:


Now you can create your own account and sign in:


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:

1mkdir -p components/Auth
2
3cd components/Auth

And create a custom SignIn component: 

1touch SignUp.tsx

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

1import React from "react";
2import { SignIn } from "aws-amplify-react";
3
4class CustomSignIn extends SignIn {
5 constructor(props: any) {
6 super(props);
7 }
8}
9
10export default CustomSignIn;

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

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

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:

1import React from "react";
2import ReactDOM from "react-dom";
3import { Authenticator } from "aws-amplify-react";
4import * as serviceWorker from "./serviceWorker";
5import { Config, isAuthenticated } from "shared/utils";
6import { amplifyConfig, authenticatorConfig } from "shared/amplify.config";
7import App from "App";
8import CustomSignIn from "shared/components/Auth/SignUp";
9
10const CognitoBoilerplate = () =&gt; {
11 return (
12
13
14
15
16
17
18 );
19};
20

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.

1import React from "react";
2import { ISignInProps } from "aws-amplify-react/lib-esm/Auth/SignIn";
3import { SignIn } from "aws-amplify-react";
4
5class CustomSignIn extends SignIn {
6 constructor(props: ISignInProps) {
7 super(props);
8 this._validAuthStates = ["signIn", "signedOut", "signedUp"];
9 this.state = {
10 loading: false,
11 };
12 }
13}
14
15export 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

1import React from "react";
2import { ISignInProps } from "aws-amplify-react/lib-esm/Auth/SignIn";
3
4import React from "react";
5import { ISignInProps } from "aws-amplify-react/lib-esm/Auth/SignIn";
6import { SignIn } from "aws-amplify-react";
7
8class CustomSignIn extends SignIn {
9 constructor(props: ISignInProps) {
10 super(props);
11 this._validAuthStates = ["signIn", "signedOut", "signedUp"];
12 this.state = {
13 loading: false,
14 };
15 }
16
17 showComponent(theme: any) {
18 return (
19 (form&gt;
20 (label style={{color: 'white', fontSize: '50px'}}&gt;FORM(/label&gt;
21 (/form&gt;
22 );
23 }
24}
25export default CustomSignIn;
26
(you have to replace "(" by "<" in the HTML code, the code embedder we use doesn't support that)


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

1import React, { MouseEvent } from "react";
2import { SignIn, UsernameAttributes } from "aws-amplify-react";
3import { ISignInProps } from "aws-amplify-react/lib-esm/Auth/SignIn";
4
5class CustomSignIn extends SignIn {
6 constructor(props: ISignInProps) {
7 super(props);
8 this._validAuthStates = ["signIn", "signedOut", "signedUp"];
9 this.state = {
10 loading: false,
11 };
12 }
13
14 showComponent(theme: any) {
15 return (
16 (form&gt;
17 (div&gt;
18 (label
19 style={{ color: "white", fontSize: "30px" }}
20 htmlFor={this.props.usernameAttributes}
21 &gt;
22 Email *
23 (/label&gt;
24 (br /&gt;
25
26 (input
27 autoFocus={true}
28 type={this.props.usernameAttributes}
29 id={this.props.usernameAttributes}
30 key={this.props.usernameAttributes}
31 name={this.props.usernameAttributes}
32 placeholder={
33 this.props.usernameAttributes === UsernameAttributes.EMAIL
34 ? "Your email address"
35 : "Your username"
36 }
37 onChange={this.handleInputChange}
38 /&gt;
39 (br /&gt;
40 (br /&gt;
41 (/div&gt;
42 (div&gt;
43 (label
44 style={{ color: "white", fontSize: "30px" }}
45 htmlFor="password"
46 &gt;
47 Password *
48 (/label&gt;
49
50 (br /&gt;
51 (input
52 type="password"
53 id="password"
54 key="password"
55 name="password"
56 placeholder="Your password"
57 onChange={this.handleInputChange}
58 /&gt;
59 (br /&gt;
60 (br /&gt;
61 (/div&gt;
62 (button
63 disabled={this.state.loading}
64 onClick={(ev: MouseEvent) =&gt; this.signIn(ev)}
65 &gt;
66 Sign in
67 (/button&gt;
68 (br /&gt;
69 (a
70 style={{ color: "white", fontSize: "15px" }}
71 onClick={() =&gt; this.changeState("signUp")}
72 &gt;
73 Don't have an account? Sign up
74 (/a&gt;
75 (/form&gt;
76 );
77 }
78}
79
80export default CustomSignIn;
81
82


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


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.

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


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

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


About TrackIt

https://www.youtube.com/watch?v=QBiJ156cA2I


TrackIt is an international AWS cloud consulting, systems integration, and software development firm headquartered in Marina del Rey, CA.

We have built our reputation on helping media companies architect and implement cost-effective, reliable, and scalable Media & Entertainment workflows in the cloud. These include streaming and on-demand video solutions, media asset management, and archiving, incorporating the latest AI technology to build bespoke media solutions tailored to customer requirements.

Cloud-native software development is at the foundation of what we do. We specialize in Application Modernization, Containerization, Infrastructure as Code and event-driven serverless architectures by leveraging the latest AWS services. Along with our Managed Services offerings which provide 24/7 cloud infrastructure maintenance and support, we are able to provide complete solutions for the media industry.