Creating a Custom Authenticator using AWS Amplify, Cognito and React.js

Vaibhav Sethia
12 min readDec 3, 2020

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
  • aws-amplify-react: 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
  • Index.js (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
  • 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
  • SignOut: 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
  • ResendSignUp: Takes in the username as a param
  • ConfirmSignUp: 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
  • ForgotPassword: 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
  • AuthState.SignedOut: Content with limited components will be displayed
  • AuthState.SignIn: Login page will be displayed with choices to register a new user or reset password
  • AuthState.SignUp: Sign Up page will be displayed
  • AuthState.ConfirmSignUp: 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
  • 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/

--

--