O'Reilly logo

Full Stack Serverless by Nader Dabit

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Introduction to Authentication

Authentication and identity is a integral part of almost any application. Knowing who the user is, a unique identifier for the user, what permissions the user has, and whether or not they are signed in allow your application to render the correct views and return the proper data for the currently signed in user.

Most applications require mechanisms to sign users up, sign them in, handle password encryption and updating, as well as countless other tasks around identity management. Modern applications many times call for things like OAUTH (open authentication), MFA (multi-factor authentication), and TOTP (time-based on time passwords).

In the past, developers had to hand roll all of this authentication functionality from scratch. This task alone could take a team of developers weeks or even months to get right and do so securely. Luckily today there are fully managed authentication services like Auth0, Okta, and Amazon Cognito that handle all of this for us.

In this chapter you will learn how to properly and securely implement basic authentication in a React application using Amazon Cognito with AWS Amplify.

The app that you will be building is a basic application that requires authentication in order to be viewed and also has a profile page showing profile information about the signed in user.

If the user is signed in they can navigate between a public route, a profile route with an authentication form, and a protected route only viewable to signed in users.

If they are not signed in, they can only view the public route and authentication form on the profile route. If the user tries to access a protected route when they are not signed in, we want to redirect them to the authentication form so that they can sign in and then access the route once authenticated.

This app will also persist user state, so if they refresh the app or navigate away from it and come back they will stay signed in.

Introduction to Amazon Cognito

Amazon Cognito is a fully managed identity service from AWS. Cognito allows for simple and secure user sign-up, sign-in, access control, and user identity management. Cognito scales to millions of users and also supports sign-in with social identity providers, such as Facebook, Google, and Amazon.

How Amazon Cognito Works

Cognito has two main pieces: User Pools and Identity Pools.

User Pools provide a secure user directory that stores all your users and scales to hundreds of millions of users. It is a fully managed service. As a serverless technology, User Pools are easy to set up without having to worry about standing up any infrastructure. User Pools are what manage all of the users that sign up and in to the account and is the main part we will be focusing on for this chapter.

Identity pools allow you to authorize users that are signed in to your application to access various other AWS services. Say you wanted to allow a user to have access to a lambda function so that they could fetch data from another API; you could specify that while creating an Identity Pool. Where user pools comes in is that the source of these identities could be a Cognito User Pool or even Facebook or Google.

Cognito User Pools allows your application to invoke various methods against the service to manage all aspects of user identity including things like:

  • Signing up a user
  • Signing in a user
  • Signing out a user
  • Changing a user’s password
  • Resetting a user’s passsword
  • Confirming a MFA code

Amazon Cognito Integration with AWS Amplify

AWS Amplify has support for Amazon Cognito in various ways. First of all, you can create and configure Amazon Cognito services directly from the AWS Amplify CLI. Once you’ve created the authentication service via the CLI you can then call various methods (like signUp, signIn, and signOut) from your JavaScript application using the Amplify JavaScript client library.

Amplify also has pre-configured UI components that allow you to scaffold out entire authentication flows in just a couple of lines of code for frameworks like React, React Native, Vue, and Angular.

In this chapter you will be using a combination of the Amplify CLI, Amplify JS client, and Amplify React components to build an application that demonstrates routing, authentication, and protected routes. You’ll also be using React Router for routing and Ant design to give the application some basic design.

React with Routing and Authentication
Figure 4-1. React with Routing and Authentication

Creating the React App and Adding Amplify

The first thing you’ll do to get started is create the React application, install the necessary dependencies, and create the Amplify project.

To get started, open your terminal and create a new React application:

~ npx create-react-app basic-authentication

~ cd basic-authentication

Next, install the AWS Amplify, AWS Amplify React, React Router, and Ant Design libraries:

~ npm install aws-amplify aws-amplify-react antd react-router-dom

Next, initialize a new Amplify project:

~ amplify init

# Follow the steps to give the project a name, environment a name, set the default editor, and accept defaults for everything else.

Now that the Amplify project has been initialized we can create the authentication service. To do so, run the following command:

~ amplify add auth

? Do you want to use the default authentication and security configuration? Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? No, I am done.

Now the authentication service has been configured and you can deploy it using the amplify push command:

~ amplify push

? Are you sure you want to continue? Yes

Now, the authentication service has been deployed, we can begin testing it out.

Client authentication overview

Using Amplify, there are two main ways to implement authentication on the client now that the service is up and running.

Auth class

The Amplify client library exposes an Auth class with over 30 different methods that allow you to handle everything associated with user management. Some examples of the methods available are Auth.signUp, Auth.signIn, and Auth.signOut.

Using this class, you can create a completely custom authentication flow based on your application’s requirements.

Framework-specific authentication components

The framework-specific libraries available in Amplify for frameworks like React, React Native, Vue, and Angular expose higher level abstractions for managing authentication. These components will render an entire (customizable) authentication flow with only a few lines of code.

In chapter one, you had a chance to try out the higher order component (HOC) from the AWS Amplify React library called withAuthenticator. In this chapter, you’ll be using this HOC along with routing to create protected routes and a profile view that can only be viewed if the user is signed in.

Building the app

The next step will be to go ahead and create the folder and file structure for the app.

Creating the file and folder structure

In your app, create the following files in the src directory:

Container.js
Nav.js
Profile.js
Protected.js
Public.js
Router.js
  • Container.js- This file will hold a component you will be using to apply a reusable container style to the other components.
  • Nav.js - In this component you will create a navigation UI.
  • Profile.js - This component will render profile information about the logged in user. This will also be the component where we add the authentication component for signing up and signing in.
  • Protected.js - This is the component we will be using as an example of how to create a protected route. If the user is signed in, they will be able to view this route. If they are not signed in, they will be redirected to the sign in form.
  • Public.js - This is a basic route that will be viewable whether or not the user is signed in.
  • Router.js - This file will hold the router and some logic to determine the current route name.

Not that these files have been created you have everything you need to begin writing some code.

Creating the first component

To start things off, let’s create the most simple component we will be using for the app - the Container component. This component is what we will be using to wrap all of our other component so that we can apply some reusable styles between the components.

/* src/Container.js */
import React from 'react'

const Container = ({ children }) => (
  <div style={styles.container}>
    { children }
  </div>
)

const styles = {
  container: {
    margin: '0 auto',
    padding: '50px 100px'
  }
}

export default Container

Using this component, you can now apply consistent styling across the entire app without having to rewrite your styles. You can then use it like this:

<Container>
  <h1>Hello World</h1>
</Container>

Anything that is a child of the Container component will be rendered with the styling set in the Container component. Doing this allows you to have a single place that you can control the styles. In case you want to make styling changes later, you only need to adjust one component.

Public component

This component simply renders the name of the route to the UI and can be accessed whether or not the user is signed in. In this component you will use the Container component to add some padding and margin.

/* src/Public.js */
import React from 'react'
import Container from './Container'

function Public() {
  return (
    <Container>
      <h1>Public route</h1>
    </Container>
  )
}

export default Public

Nav component

The Nav (navigation) component will be utilizing the Ant Design library and React Router. Ant Design will provide the Menu and Icon components to make a nice looking menu, and React Router will provide the Link component so that we can link and navigate to different parts of the app.

You’ll also notice that there is a current prop that is passed in to the component. This prop represents the name of the current route. For this application the value will either be home, profile, or protected. This value is used in the selectedKeys array of the Menu component to highlight the current route in the navigation bar. This value will be calculated in the Router component and passed down here as a prop.

/* src/Nav.js */
import React from 'react'
import { Link } from 'react-router-dom'
import { Menu, Icon } from 'antd'

const Nav = (props) => {
  const { current } = props
  return (
    <div>
      <Menu selectedKeys={[current]} mode="horizontal">
        <Menu.Item key='home'>
          <Link to={`/`}>
            <Icon type='home' />Home
          </Link>
        </Menu.Item>
        <Menu.Item key='profile'>
          <Link to='/profile'>
            <Icon type='profile' />Profile
          </Link>
        </Menu.Item>
        <Menu.Item key='protected'>
          <Link to='/protected'>
            <Icon type='protected' />Protected
          </Link>
        </Menu.Item>
      </Menu>
    </div>
  )
}

export default Nav

Protected component

The Protected component will be the protected, or private, route. If the user trying to access this route is signed in, they will be able to view this route. If they are not signed in, they will be redirected to the profile page to sign up or sign in.

In this component you will be using the useEffect hook from React and the Auth class from AWS Amplify.

  • useEffect - This is a React hook that allows you to perform side effects in function components. This hook accepts a function that is called when the function renders for the first time and, optionally, every additional time that it renders. By passing in an empty array as the second argument we are choosing to only fire the function once: when the component loads. If you have used componentDidMount in a React class, useEffect has similar characteristics and use cases.

  • *Auth - This is the class from AWS Amplify that handles user management. You can use this class to do everything from signing a user up and in to resetting their password. In this component we will be calling a method, Auth.currentAuthenticatedUser that will check if the user is currently signed in and, if so, return data about the signed in user.

/* src/Protected.js */
import React, { useEffect } from 'react';
import { Auth } from 'aws-amplify'
import Container from './Container'

function Protected(props) {
  useEffect(() => {
    Auth.currentAuthenticatedUser()
      .catch(() => {
        props.setCurrent('profile')
        props.history.push('/profile')
      })
  }, [])
  return (
    <Container>
      <h1>Protected route</h1>
    </Container>
  );
}

export default Protected

When the component is rendered, we check to see if the user is signed in to the app by calling Auth.currentAuthenticatedUser. If this API call is not successful, that means the user is not signed in and we need to redirect them. We redirect them by calling props.history.push('/profile').

In this component there is a prop called setCurrent. If the user is not signed in we want to also highlight the profile key in the navigation since we are redirecting them to the /profile route. We are able to set this value by calling setCurrent and passing in the key we’d like to set. The setCurrent function will be created in the Router component and passed down to this component as a prop.

Router component

The Router component will define the components we want to have available in the navigation.

This component will also be setting the current route name that will be used in the Nav component to highlight the current route based on the window.location.href property.

The components that you will be using from React Router are HashRouter, Switch, and Route.

  • HashRouter - This is a router that uses the hash portion of the URL (i.e. window.location.hash) to keep your UI in sync with the URL.

  • Switch - Switch renders the first child route that matches the location. This is different than the default functionality of just using the router, which may render multiple routes that match the location.

  • Route - This component allows you to define the component that you’d like to render based on a path parameter.

/* src/Router.js */
import React, { useState, useEffect } from 'react'
import { HashRouter, Switch, Route } from 'react-router-dom'

import Nav from './Nav'
import Public from './Public'
import Profile from './Profile'
import Protected from './Protected'

const Router = () => {
  const [current, setCurrent] = useState('home')
  useEffect(() => {
    setRoute()
    window.addEventListener('hashchange', setRoute)
    return () =>  window.removeEventListener('hashchange', setRoute)
  }, [])
  function setRoute() {
    const location = window.location.href.split('/')
    const pathname = location[location.length-1]
    setCurrent(pathname ? pathname : 'home')
  }
  return (
    <HashRouter>
      <Nav current={current} />
      <Switch>
        <Route exact path="/" component={Public}/>
        <Route exact path="/protected" component={Protected} />
        <Route exact path="/profile" component={Profile}/>
        <Route component={Public}/>
      </Switch>
    </HashRouter>
  )
}

export default Router

In the useEffect hook in this component component, we set the route name by calling setRoute. We also set up an event listener to call setRoute whenever the route changes.

When declaring a Route component, you can pass in the component you would like to render as a component prop.

Profile component

The last component we need to finish our app is the Profile component. This component will do a couple of things:

  • Render the authentication form if the user is not signed in.

  • Provide a sign out button.

  • Render the user’s profile information to the UI.

Just like in chapter 1 we are using the withAuthenticator HOC to render the authentication flow by wrapping the Profile component in the default export. This will show the sign up / sign in form if the user is not signed in, and if the user is signed in will show the UI with the user’s profile details.

To sign the user out, we use the Auth.signOut method. This will sign the user out and rerender the UI to show the authentication form.

To display the user profile data, we use the Auth.currentAuthenticatedUser method. If the user is signed in, this method will return the user profile data along with information about the session. The information that we are interested in using for the profile are the username and user attributes, which include the phone number, email, and any other information gathered when the user signed up.

/* src/Profile.js */
import React, { useState, useEffect } from 'react'
import { Button } from 'antd'
import { Auth } from 'aws-amplify'
import { withAuthenticator } from 'aws-amplify-react'
import Container from './Container'

function Profile() {
  useEffect(() => {
    checkUser()
  }, [])
  const [user, setUser] = useState({})
  async function checkUser() {
    try {
      const data = await Auth.currentUserPoolUser()
      const userInfo = { username: data.username, ...data.attributes, }
      setUser(userInfo)
    } catch (err) { console.log('error: ', err) }
  }
  function signOut() {
    Auth.signOut()
      .catch(err => console.log('error signing out: ', err))
  }
  return (
    <Container>
      <h1>Profile</h1>
      <h2>Username: {user.username}</h2>
      <h3>Email: {user.email}</h3>
      <h4>Phone: {user.phone_number}</h4>
      <Button onClick={signOut}>Sign Out</Button>
    </Container>
  );
}

export default withAuthenticator(Profile)

Configuring the app

Now the app is built. The last thing we need to do is update index.js to import the Router and add the Amplify configuration. We also want to import the necessary CSS for the ant design library.

/* src/index.js */
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Router from './Router';
import 'antd/dist/antd.css';

import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

ReactDOM.render(<Router />, document.getElementById('root'));

Testing the app

To run the app, we can now run the start command:

npm start

Summary

Congratulations, you’ve built out an authentication flow with routing and protected routes!

Here are a couple of things to keep in mind from this chapter:

  1. Use the withAuthenticator HOC to quickly get up and running with a preconfigured authentication flow.

  2. Use the Auth class for more fine grained control over authentication and to get data about the currently signed in user.

  3. Ant Design helps you get started with preconfigured designed without having to write any styles-specific code.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required