Creating a Custom Authenticator using AWS Amplify, Cognito and React.js
By: Vaibhav Sethia
Tags: Login System, AWS, Authentication, Aws Amplify, React

The man of science has learned to believe in justification, not by faith, but by verification.

Why React?

React is one of the trendiest frameworks for building single-page UI-first applications. Its popularity is increasing for the second year in a row and there are reasons for that.

Getting started with React is easy both for beginners and experienced developers thankfully to the supportive community and detailed documentation — it covers pretty much every aspect of working with React — from basics to advanced concepts.

Why AWS Amplify?

AWS Amplify includes a wide variety of open-source libraries and drag-and-drop UI components developers can use as building blocks for their apps. It also has a built-in CLI you can use to build your backend. One of the pre-built components in Amplify is Authenticator which we are gonna use for making the Authentication system.

Getting Started

Create a react app and install the dependencies needed for the system

npx create-react-app yarn add aws-amplify-react @aws-amplify/ui-components aws-amplify

aws-amplify : For using Auth component that connects frontend with AWS Amplify backend and Cognito

: For using Auth component that connects frontend with AWS Amplify backend and Cognito aws-amplify-react : For importing pre-built Authenticator and SignIn and SignUp class

: For importing pre-built Authenticator and SignIn and SignUp class aws-amplify/ui-components: For using AuthState enum that contains different auth states

Import Tachyons.io for pre-defined CSS styles (Optional)

Setup File Structure

Apart from the App and Index file in src of the project, All the components that will be used will be placed in the src/Components folder

App.js (src/App.js): Will contain all the authentication and state checks

(src/App.js): Will contain all the authentication and state checks Index.js (src/Index.js): Root of the project that will contain the routing setup and call to the App component

(src/Index.js): Root of the project that will contain the routing setup and call to the App component Login Directory (src/Components/Login) : Will contain all the files related to login system i.e CustomSignUp.js, CustomSignIn.js, ForgotPassword.js, ResetPasword.js

(src/Components/Login) : Will contain all the files related to login system i.e CustomSignUp.js, CustomSignIn.js, ForgotPassword.js, ResetPasword.js Content Directory (src/Components/Content/index.js): Will contain all the content related to the project or component to call the content

Initialize Auth

Install amplify command line and initialize amplify the environment

// For installation

yarn global add @aws-amplify/cli // For Initialization

amplify init

Note: Make sure you have a verified AWS account and AWS Credentials beforehand.

Initializing amplify environment

Add Auth in your project and initialize it, Either you can initialize it with basic settings or AWS provides an option to edit the advanced settings like Recaptcha, OTP verification, etc which can be configured in the advanced settings option.

amplify add auth

Note: You will not be able to alter the choices you make while setting up auth

Initializing Auth

Run amplify status to check if the auth has been successfully added.

Index.js

import React from 'react';

import ReactDOM from 'react-dom';

import './index.css';

import App from './App';

import Amplify from 'aws-amplify'

import aws_config from './aws-exports'; Amplify.configure(aws_config) ReactDOM.render(

<React.StrictMode>

<App />

</React.StrictMode>,

document.getElementById('root'));

Auth Class

Functions that we will be using from the Auth Class are:

SignIn: Takes in the username and password as params

Takes in the username and password as params SignOut: Takes in no params and signs out the user

Takes in no params and signs out the user CurrentAuthenticatedUser: Takes in no params and returns a Cognito User object with user and client details

Takes in no params and returns a Cognito User object with user and client details ResendSignUp: Takes in the username as a param

Takes in the username as a param ConfirmSignUp: Takes in the username, and code(OTP) as a params

Takes in the username, and code(OTP) as a params SignUp: Takes an object with the username, password and attributes object that contains the user details that is asked while signing up as params

Takes an object with the username, password and attributes object that contains the user details that is asked while signing up as params ForgotPassword: Takes in the username as a param

Takes in the username as a param ForgotPasswordSubmit: Takes in the username, code(OTP), and password as params

App.js

Defining a Class component App that invokes different login system screens depending on the value of AuthState enum used. The Content component must have options to Sign In and Sign Out. For the article, we are making a system that will allow users to view certain elements of the website even if logged out but for getting access to all the elements users must be logged in.

Different Auth states and their usage are :

AuthState.SignedIn : Content with all components will be displayed

: Content with all components will be displayed AuthState.SignedOut : Content with limited components will be displayed

: Content with limited components will be displayed AuthState.SignIn : Login page will be displayed with choices to register a new user or reset password

: Login page will be displayed with choices to register a new user or reset password AuthState.SignUp: Sign Up page will be displayed

Sign Up page will be displayed AuthState.ConfirmSignUp : Mail verification page to verify user mail address using OTP

: Mail verification page to verify user mail address using OTP AuthState.ForgotPassword : Forgot password page that allows user to validates user by sending the user an OTP to the mail id of the registered and verified user

: Forgot password page that allows user to validates user by sending the user an OTP to the mail id of the registered and verified user AuthState.ResetPassword: Reset password page to enter, validate and change password

import React, { Component } from 'react'

import { Authenticator, SignIn, SignUp} from 'aws-amplify-react/lib/Auth';

import aws_config from './aws-exports'

import './App.css'

import Content from './Components/Content'

import { Auth } from 'aws-amplify';

import { AuthState } from '@aws-amplify/ui-components'; import CustomSignIn from './Components/Login/CustomSignIn'

import CustomSignUp from './Components/Login/CustomSignUp'

import ConfirmSignUp from './Components/Login/ConfirmSignUp';

import ForgotPassword from './Components/Login/ForgotPassword';

import ResetPassword from './Components/Login/ResetPassword'; export class App extends Component {

constructor(props) {

super(props)

this.state = {

AuthState: AuthState.SignedOut,

User: null,

SignUpUsername: '',

} this.SetUserName = this.SetUserName.bind(this);

this.SetAuthState = this.SetAuthState.bind(this);

} SetUserName(Val){

this.setState({

SignUpUsername: Val,

})

} SetUser(UserVals){

this.setState({

User: UserVals,

})

} async SetAuthState(Val){

if(Val === AuthState.SignedOut){

Auth.signOut();

this.SetUser(null);

}

this.setState({

AuthState: Val,

}) const User = await Auth.currentAuthenticatedUser();

this.SetUser(User);

} async componentDidMount(){

const User = await Auth.currentAuthenticatedUser();

if(User === null){

this.SetAuthState(AuthState.SignedOut)

} else{

this.SetAuthState(AuthState.SignedIn)

}

} render() {

if(this.state.AuthState === AuthState.SignedIn){

return(

<Content

AuthState = {this.state.AuthState}

User = {this.state.User}

SetAuthState = {this.SetAuthState}

/>

)

} else if(this.state.AuthState === AuthState.SignedOut){

return(

<Content

AuthState = {this.state.AuthState}

User = {null}

SetAuthState = {this.SetAuthState}

/>

)

} else if(this.state.AuthState === AuthState.ConfirmSignUp){

return(

<ConfirmSignUp

SetAuthState={this.SetAuthState}

Username={this.state.SignUpUsername}

/>

)

} else if(this.state.AuthState === AuthState.ForgotPassword){

return(

<ForgotPassword

SetAuthState={this.SetAuthState}

SetUserName={this.SetUserName}

/>

)

} else if(this.state.AuthState === AuthState.ResetPassword){

return(

<ResetPassword

SignUpUsername={this.state.SignUpUsername}

SetAuthState={this.SetAuthState}

/>

)

} else{

return(

<Authenticator hide={[SignIn, SignUp]} amplifyConfig={aws_config}>

<CustomSignIn

SetAuthState={this.SetAuthState}

/>

<CustomSignUp

AuthState = {this.state.AuthState}

SetAuthState={this.SetAuthState}

SetUserName={this.SetUserName}

/>

</Authenticator>

)

}

}

} export default (App);

CustomSignIn.js

Defining Class component CustomSignIn extending the SignIn Class so we can use the changeState function to change the state to ‘signUp’ to get redirected to the CustomSignUp page to register a new user when prompted. Auth.signIn function is called by passing username and password as params that can be taken from the user using a simple form. When extending Component class from react render function gets the statements that are supposed to be returned, In case of SignIn, a similar function called showComponent does the job.

Note: Various errors that can be caught in try-catch are logged in the console can also be used to pop on the screen as an alert.

import { SignIn } from 'aws-amplify-react/lib/Auth';

import React, { Component } from 'react'

import { AuthState } from '@aws-amplify/ui-components';

import { Auth } from 'aws-amplify'; export class CustomSignIn extends SignIn {

constructor(props) {

super(props)

this.state = {

Username : '',

Password : ''

}

this.signIn = this.signIn.bind(this);

this.handleFormSubmission = this.handleFormSubmission.bind(this);

} handleFormSubmission(evt) {

evt.preventDefault();

this.signIn();

} async signIn() {

const username = this.state.Username;

const password = this.state.Password;

try {

await Auth.signIn(username, password);

await this.props.SetAuthState(AuthState.SignedIn)

} catch (err) {

if (err.code === "UserNotConfirmedException") {

this.setState({ error: "Login failed." });

} else if (err.code === "NotAuthorizedException") {

this.setState({ error: "Login failed." });

} else if (err.code === "UserNotFoundException") {

this.setState({ error: "Login failed." });

} else {

this.setState({ error: "An error has occurred." });

console.error(err);

}

}

} showComponent(theme) {

return (

<div className="tc pt5">

<h2>Sign in to your Account</h2>

<div className="pa2">

<label for="username" className="pr3">UserName</label>

<input className="ba b--gray br2 pl2 shadow-2" type="text" placeholder="Username" onChange={(e) => this.setState({Username: e.target.value})}></input>

</div>

<div className="pa2">

<label for="password" className="pr3">Password</label>

<input className="ba b--gray br2 pl2 shadow-2" type="password" placeholder="Password" onChange={(e) => this.setState({Password: e.target.value})}></input>

</div>

<div className="pa2">

<a className="f6 link dim br-pill ba ph3 pv2 mb2 dib dark-green" onClick={this.handleFormSubmission} href="#0">Sign In</a

</div>

<div>

Not a User ? <a className="f5 fw6 dark-green link " onClick={() => super.changeState("signUp")}>Register Now !</a>

</div>

<div className="pa2">

Do not remember password ? <a className="f5 fw6 dark-green link " onClick={() => this.props.SetAuthState(AuthState.ForgotPassword)} href="#0">Forgot Password</a>

</div>

</div>

)

}

} export default CustomSignIn

CustomSignUp.js

Defining Class component CustomSignUp extending the SignUp Class. Auth.signUp function is called by passing an object containing the username, password, and attributes object containing custom and predefined attributes of the user as params that can be taken from the user using a simple form. Before calling the signUp function, It is optional to take in a password twice and later on validate its value.

Note: Various errors that can be caught in try-catch are logged in the console can also be used to pop on the screen as an alert.

import { SignUp } from 'aws-amplify-react/lib/Auth';

import React from 'react'

import { AuthState } from '@aws-amplify/ui-components';

import { Auth } from 'aws-amplify'; export class CustomSignUp extends SignUp {

constructor(props) {

super(props)

this.state = {

Username : '',

Password : '',

RePassword: '',

Mail: '',

User : null,

} this.signUp = this.signUp.bind(this);

this.handleFormSubmission = this.handleFormSubmission.bind(this);

} handleFormSubmission(evt) {

if(this.state.Password === this.state.RePassword && this.state.Password !== ''){

evt.preventDefault();

this.signUp();

} else{

console.log("Passwords did not match")

}

} async signUp() {

const username = this.state.Username;

const password = this.state.Password;

const email = this.state.Mail;



try {

const { user } = await Auth.signUp({

username, password,

attributes: {

email,

}

});

this.props.SetUserName(user['username'])

this.props.SetAuthState(AuthState.ConfirmSignUp)

} catch (err) {

if (err.code === "UsernameExistsException") { //Username already taken

//Shows signup error

}else if(err.code === ""){ //password too weak

// show the error

} else {

this.setState({ error: "An error has occurred." });

console.error(err);

this.props.SetAuthState(AuthState.ConfirmSignUp)

}

}

} showComponent(theme) {

return (

<div className="tc pt5">

<h2>Sign up with a new Account</h2>

<div className="pa2">

<label for="username" className="pr3">UserName</label>

<input className="ba b--gray br2 pl2 shadow-2" type="text" placeholder="Username" onChange={(e) => this.setState({Username: e.target.value})}></input>

</div>

<div className="pa2">

<label for="username" className="pr3">Email</label>

<input className="ba b--gray br2 pl2 shadow-2" type="email" placeholder="Email" onChange={(e) => this.setState({Mail: e.target.value})}></input>

</div>

<div className="pa2">

<label for="password" className="pr3">Password</label>

<input className="ba b--gray br2 pl2 shadow-2" type="password" placeholder="Password" onChange={(e) => this.setState({Password: e.target.value})}></input>

</div>

<div className="pa2">

<label for="password" className="pr3">Re-Type Password</label>

<input className="ba b--gray br2 pl2 shadow-2" type="password" placeholder="Re-Type Password" onChange={(e) => this.setState({RePassword: e.target.value})}></input>

</div>

<div className="pa2">

<a className="f6 link dim br-pill ba ph3 pv2 mb2 dib dark-green" onClick={this.handleFormSubmission} href="#0">Sign Up</a>

</div>

</div>

)

}

} export default CustomSignUp

ConfirmSignUp.js

Defining Class component ConfirmSignUp extending the Component Class. Upon Signing up the user will be prompted to verify the mailing address entered while signing up by validating the OTP sent on the registered address Auth.confirmSignUp function is called by passing username and valid OTP. An option to resend the OTP can also be used in case the user wants to get a new OTP.

Note: Incorrect OTP error can be caught in try-catch and is logged in the. It can also be used to pop on the screen as an alert.

import React, { Component } from 'react'

import { AuthState } from '@aws-amplify/ui-components';

import { Auth } from 'aws-amplify'; export class ConfirmSignUp extends Component {

constructor(props) {

super(props)

this.state = {

Code : ''

} this.confirmSignUp = this.confirmSignUp.bind(this);

this.handleFormSubmission = this.handleFormSubmission.bind(this);

} async resendConfirmationCode(){

try {

await Auth.resendSignUp(this.props.Username);

console.log('code resent successfully');

} catch (err) {

console.log('error resending code: ', err);

}

} handleFormSubmission(evt) {

evt.preventDefault();

this.confirmSignUp();

} async confirmSignUp() {

const username = this.props.Username;

const code = this.state.Code;

try {

await Auth.confirmSignUp(username, code);

this.props.SetAuthState(AuthState.SignIn)

} catch (error) {

console.log('error confirming sign up', error);

}

} render() {

return (

<div className="tc pt5">

<h2>Verify your Mail Address</h2>

<div className="pa2">

<label for="username" className="pr3">UserName</label>

<input className="ba b--gray br2 pl2 shadow-2" type="text" placeholder={this.props.Username} onChange={(e) => this.setState({Username: e.target.value})} disabled></input>

</div>

<div className="pa2">

<label for="code" className="pr3">Code</label>

<input className="ba b--gray br2 pl2 shadow-2" type="text" placeholder="Enter Code" onChange={(e) => this.setState({Code: e.target.value})}></input>

</div>

<div className="pa2">

<a className="f6 link dim br-pill ba ph3 pv2 mb2 dib dark-green" onClick={this.handleFormSubmission} href="#0">Verify Account</a>

</div>

<div>

Didn't get a Code ? <a className="f5 fw6 dark-green link " onClick={() => this.resendConfirmationCode} href="#0">Resend Code</a>

</div>

</div>

)

}

} export default ConfirmSignUp

Making Sure SignUp functionality is working

To make sure the sign-up functionality is working, log in to your AWS console and look for Cognito in the services section. In manage user pool, choose the user pool that is used with the Authentication system and you can view the user that you just registered.

ForgotPassword.js

Defining Class component ForgotPassword extending the Component Class. On Clicking on the forgot password option on the SignIn screen user will be redirected to this component. Auth.forgotPassword function is called by passing the username of the registered and verified user. An option to resend the OTP can also be used in case the user wants to get a new OTP.

import React, { Component } from 'react'

import { AuthState } from '@aws-amplify/ui-components';

import { Auth } from 'aws-amplify'; export class ForgotPassword extends Component {

constructor(props) {

super(props)

this.state = {

Username : '',

} this.forgotPassword = this.forgotPassword.bind(this);

this.handleFormSubmission = this.handleFormSubmission.bind(this);

} async resendConfirmationCode(){

try {

await Auth.resendSignUp(this.state.Username);

console.log('code resent successfully');

} catch (err) {

console.log('error resending code: ', err);

}

}



handleFormSubmission(evt) {

evt.preventDefault();

this.forgotPassword();

} async forgotPassword() {

const username = this.state.Username;

try{

await Auth.forgotPassword(username)

this.props.SetUserName(username)

this.props.SetAuthState(AuthState.ResetPassword)

} catch(err){

console.log(err)

}

} render() {

return (

<div className="tc pt5">

<h2>Forgot Password</h2>

<div className="pa2">

<label for="username" className="pr3">UserName</label>

<input className="ba b--gray br2 pl2 shadow-2" type="text" placeholder="Username" onChange={(e) => this.setState({Username: e.target.value})}></input>

</div>

<div className="pa2">

<a className="f6 link dim br-pill ba ph3 pv2 mb2 dib dark-green" onClick={this.handleFormSubmission} href="#0">Send OTP</a>.

</div>

</div>

)

}

} export default ForgotPassword

ResetPassword.js

Defining Class component ResetPassword extending the Component Class. On entering the correct username, the user will be redirected to this section. On Clicking on the forgot password option on the SignIn screen user will be redirected to this component. Auth.forgotPasswordSubmit function is called by passing the username, OTP, and a new password of the registered.

Note: Various errors that can be caught in try-catch are logged in the console can also be used to pop on the screen as an alert.

import React, { Component } from 'react'

import { AuthState } from '@aws-amplify/ui-components';

import { Auth } from 'aws-amplify'; export class ResetPassword extends Component {

constructor(props) {

super(props)

this.state = {

Username : '',

Password : '',

Code : '',

RePassword : '',

} this.resetPassword = this.resetPassword.bind(this);

this.handleFormSubmission = this.handleFormSubmission.bind(this);

} handleFormSubmission(evt) {

if(this.state.Password === this.state.RePassword && this.state.Password != ''){

evt.preventDefault();

this.resetPassword();

} else{

console.log("Passwords did not match")

}

} async resetPassword() {

const username = this.props.SignUpUsername;

const code = this.state.Code

const password = this.state.Password

try{

await Auth.forgotPasswordSubmit(username, code, password)

this.props.SetAuthState(AuthState.SignIn)

} catch(err){

console.log(err)

}

} render() {

return (

<div className="tc pt5">

<h2>Reset Password</h2>

<div className="pa2">

<label for="username" className="pr3">UserName</label>

<input className="ba b--gray br2 pl2 shadow-2" type="text" placeholder={this.props.SignUpUsername} onChange={(e) => this.setState({Username: e.target.value})} disabled></input>

</div>

<div className="pa2">

<label for="mail" className="pr3">Code</label>

<input className="ba b--gray br2 pl2 shadow-2" type="text" placeholder="Enter Code" onChange={(e) => this.setState({Code: e.target.value})}></input>

</div>

<div className="pa2">

<label for="password" className="pr3">Password</label>

<input className="ba b--gray br2 pl2 shadow-2" type="password" placeholder="Password" onChange={(e) => this.setState({Password: e.target.value})}></input>

</div>

<div className="pa2">

<label for="password" className="pr3">Re-Type Password</label>

<input className="ba b--gray br2 pl2 shadow-2" type="password" placeholder="Re-Type Password" onChange={(e) => this.setState({RePassword: e.target.value})}></input>

</div>

<div className="pa2">

<a className="f6 link dim br-pill ba ph3 pv2 mb2 dib dark-green" onClick={this.handleFormSubmission} href="#0">Reset Password</a>

</div>

</div>

)

}

} export default ResetPassword

Content/Index.js

Defining Class component Content extending the Component Class. It will contain all the content that will be displayed to the user and will have the check for the state passed and the user depending upon which it will return the component or call the components to be displayed.

Note: This component must have a call to Login and to Sign Out of the system

import React, { Component } from 'react'

import { AuthState } from '@aws-amplify/ui-components' export class index extends Component {

render() {

console.log(this.props.User)

if(this.props.AuthState === AuthState.SignedIn){

return(

<div className="tc">

<h2> AWS Authenticator Tutorial</h2>

{this.props.User === null ? <div> Loading User </div> :

<div className="tc w-100">

<p className=""> Name : {this.props.User['username']}</p>

<p className=""> Mail : {this.props.User['attributes']['email']}</p>

</div>

}

<a className="f6 link dim br-pill ba ph3 pv2 mb2 dib dark-pink" onClick={(e)=>this.props.SetAuthState(AuthState.SignedOut)} href="#0">Sign Out</a>

</div>

)

} else{

return(

<div className="tc w-100">

<h2> AWS Authenticator Tutorial</h2>

<p> No user Logged in !</p>

<a className="f6 link dim br-pill ba ph3 pv2 mb2 dib dark-green" onClick={(e)=>this.props.SetAuthState(AuthState.SignIn)} href="#0">Login</a>

</div>

)

}

}

} export default index

Content when Signed Out

Content when Signed In

Conclusion

This is the simplest and most efficient way to use AWS Authenticator and Cognito for a fully customizable authentication system. For the sake of keeping the code clean and the article small, a simple CSS has been used although it can be integrated with any UI or CSS framework. The only things that need to be taken care of are the AuthStates, function calls, and their order.

Project Github URL: https://github.com/vaibhavsethia/AWS-Authenticator

LinkedIn Profile: https://www.linkedin.com/in/vaibhav-sethia-4711b8145/