Chapter 5. Custom Authentication Strategies

In chapter four you learned how to use the withAuthenticator higher order component to create a pre-configured authentication form. You also learned how to use React Router and the Auth class to create public and protected routes based on the user’s signed in state.

While this lays the foundation for what can be done with Amplify and the basics around authentication and routing, we want to go one step further and build a completely custom authentication flow so we know exactly what is going on under the hood and understand the logic and state needed to manage a custom authentication form.

This means that we need to update our app to have custom forms for signing up, signing in, and resetting our password instead of using the withAuthenticator Higher Order Component.

We will also take the idea of protected routes one step further by creating a hook that we can reuse to wrap any component we are wanting protect with authentication (instead of rewriting the logic in each component).

The Auth class, with over 30 different methods, is very powerful and allows you to handle all of the authentication logic that most applications demand. By the end of this chapter, you will understand how to use the Auth class and React state to build and manage a custom authentication form.

Creating the protectedRoute hook

The first thing we will do is to create the custom protectedRoute hook that we will be using to protect any routes that require authentication.

This hook will check for the signed in user information and if the user is not signed in, will redirect them to the sign in page or any other specified route. If the user is signed in, the hook will return and render the component passed in as an argument.

By using this hook we can do away with any duplicate logic around authentication that we may need across multiple components.

In the src directory, create a new file called protectedRoute.js and add the following code:

import React, { useEffect } from 'react'
import { Auth } from 'aws-amplify'

const protectedRoute = (Comp, route = '/profile') => (props) => {
  async function checkAuthState() {
    try {
      await Auth.currentAuthenticatedUser()
    } catch (err) {
      props.history.push(route)
    }
  }
  useEffect(() => {
    checkAuthState()
  }, [])
  return <Comp {...props} />
}

export default protectedRoute

This component uses the useEffect hook when the component loads to check if the user is signed in. If the user is signed in, nothing happens and the component that is passed in as an argument gets rendered. If the user is not signed in, we do a redirect.

The redirect route can either be passed in as the second argument to the hook, or if no redirect route is passed in we set the default to be /profile. Now, we can use the hook to protect any component like this:

// default redirect route
protectedRoute(App)

// custom redirect route
protectedRoute(App, '/about-us')

Now that the protected route hook has been created, we can begin the refactor of our app. The next thing we may want to do is update the Protected component in our app to use this new protectedRoute hook. To do so, open Protected.js and update the component with this code:

import React from 'react';
import Container from './Container'
import protectedRoute from './protectedRoute'

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

export default protectedRoute(Protected)

Now this component is protected and users will continue to be redirected when trying to access it if they are not authenticated.

Creating the Form

The next thing we will want to do is create the main Form component. This component will hold all of the logic and UI for the following actions:

  • Signing up

  • Confirming sign up

  • Signing in

  • Resetting password

In the last chapter we used the withAuthenticator component that encapsulated most of this logic for us, but we will new be rewriting our own version of this from scratch. It is important to understand how to create and handle custom forms because most of the time you will be working with custom designs and business logic that may not be able to work with abstractions like the withAuthenticator component.

The first thing we will do is create the new component files that we will need. In the src directory, create the following files:

Button.js
Form.js
SignUp.js
ConfirmSignUp.js
SignIn.js
ForgotPassword.js
ForgotPasswordSubmit.js

Now that you have created these components, let’s continue by creating the reusable button we will be using as the submit button across all of the forms. In Button.js, add the following code:

import React from 'react'

export default function Button({ onClick, title }) {
  return (
    <button style={styles.button} onClick={onClick}>
      {title}
    </button>
  )
}

const styles = {
  button: {
    backgroundColor: '#006bfc',
    color: 'white',
    width: 316,
    height: 45,
    fontWeight: '600',
    fontSize: 14,
    cursor: 'pointer',
    border:'none',
    outline: 'none',
    borderRadius: 3,
    marginTop: '25px',
    boxShadow: '0px 1px 3px rgba(0, 0, 0, .3)',
  },
}

This component is a pretty basic button component that accepts two props: title and onClick. The onClick handler will call the function associated with the button and the title component will render the text for the button.

Next, open Form.js and add the following code that will be the beginnings of the form:

import React, { useState } from 'react'
import { Auth } from 'aws-amplify'
import SignIn from './SignIn'
import SignUp from './SignUp'
import ConfirmSignUp from './ConfirmSignUp'
import ForgotPassword from './ForgotPassword'
import ForgotPasswordSubmit from './ForgotPasswordSubmit'

const initialFormState = {
  username: '', password: '', email: '', confirmationCode: ''
}

function Form(props) {
  const [formType, updateFormType] = useState('signIn')
  const [formState, updateFormState] = useState(initialFormState)
  function renderForm() {}
  return (
    <div>
      {renderForm()}
    </div>
  )
}

Here, we’ve imported the individual form components (that we will be writing shortly) and initialized some initial form state. The things that we will be keeping up with in the form state are the input fields (username, password, email, and confirmationCode) for the authentication flow.

There’s another piece of component state that keeps up with the type of form to be rendered, formType. Because the form components will be displayed all in one route, we will need to check what the current form state is and then render either the Sign Up form, Sign In form, or Reset Password form. The updateFormType will be the function that switches between different form types. Once a user successfully signs up, for example, we will call updateFormType('signIn') to render the SignIn component so that they can then sign in.

The renderForm method the is returned in the UI for the Form component will return the correct component based on the current formType.

Next, add the following styles and default export to Form.js. The styles for some of the elements will be shared among the components so we will be exporting both the component as well as the styling:

const styles = {
  container: {
    display: 'flex',
    flexDirection: 'column',
    marginTop: 150,
    justifyContent: 'center',
    alignItems: 'center'
  },
  input: {
    height: 45,
    marginTop: 8,
    width: 300,
    maxWidth: 300,
    padding: '0px 8px',
    fontSize: 16,
    outline: 'none',
    border: 'none',
    borderBottom: '2px solid rgba(0, 0, 0, .3)'
  },
  toggleForm: {
    fontWeight: '600',
    padding: '0px 25px',
    marginTop: '15px',
    marginBottom: 0,
    textAlign: 'center',
    color: 'rgba(0, 0, 0, 0.6)'
  },
  resetPassword: {
    marginTop: '5px',
  },
  anchor: {
    color: '#006bfc',
    cursor: 'pointer'
  }
}

export { styles, Form as default }

Next, let’s go ahead and create the individual form components.

SignIn component

The SignIn component will render the sign in form. This component will accept two props, one for updating the form state (updateFormState) and one for calling the signIn function.

import React from 'react'
import Button from './Button'
import { styles } from './Form'

function SignIn({ signIn, updateFormState }) {
  return (
    <div style={styles.container}>
      <input
        name='username'
        onChange={e => {e.persist();updateFormState(e)}}
        style={styles.input}
        placeholder='username'
      />
      <input
        type='password'
        name='password'
        onChange={e => {e.persist();updateFormState(e)}}
        style={styles.input}
        placeholder='password'
      />
      <Button onClick={signIn} title="Sign In" />
    </div>
  )
}

export default SignIn

SignUp component

The SignUp component will render the sign up form. This component will accept two props, one for updating the form state (updateFormState) and one for calling the signUp function.

import React from 'react'
import Button from './Button'
import { styles } from './Form'

function SignUp({ updateFormState, signUp }) {
  return (
    <div style={styles.container}>
      <input
        name='username'
        onChange={e => {e.persist();updateFormState(e)}}
        style={styles.input}
        placeholder='username'
      />
      <input
        type='password'
        name='password'
        onChange={e => {e.persist();updateFormState(e)}}
        style={styles.input}
        placeholder='password'
      />
      <input
        name='email'
        onChange={e => {e.persist();updateFormState(e)}}
        style={styles.input}
        placeholder='email'
      />
      <Button onClick={signUp} title="Sign Up" />
    </div>
  )
}

export default SignUp

ConfirmSignUp component

Once a user has signed up they will receive a confirmation code for multi-factor authentication (MFA). The ConfirmSignUp component holds the form that will handle and submit this MFA code.

This component will accept two props, one for updating the form state (updateFormState) and one for calling the confirmSignUp function.

import React from 'react'
import Button from './Button'
import { styles } from './Form'

function ConfirmSignUp(props) {
  return (
    <div style={styles.container}>
      <input
        name='confirmationCode'
        placeholder='Confirmation Code'
        onChange={e => {e.persist();props.updateFormState(e)}}
        style={styles.input}
      />
      <Button onClick={props.confirmSignUp} title="Confirm Sign Up" />
    </div>
  )
}

export default ConfirmSignUp

The next two forms will be for handling the resetting of a forgotten password. The first form (ForgotPassword) will take the user’s username as an input and send them a confirmation code. They can then use that confirmation code along with a new password to reset the password in the second form (ForgotPasswordSubmit).

The ForgotPassword component will accept two props, one for updating the form state (updateFormState) and one for calling the forgotPassword function.

ForgotPassword component

import React from 'react'
import Button from './Button'
import { styles } from './Form'

function ForgotPassword(props) {
  return (
    <div style={styles.container}>
      <input
        name='username'
        placeholder='Username'
        onChange={e => {e.persist();props.updateFormState(e)}}
        style={styles.input}
      />
      <Button onClick={props.forgotPassword} title="Reset password" />
    </div>
  )
}

export default ForgotPassword

ForgotPasswordSubmit component

The ForgotPasswordSubmit component will accept two props, one for updating the form state (updateFormState) and one for calling the forgotPassword function.

import React from 'react'
import Button from './Button'
import { styles } from './Form'

function ForgotPasswordSubmit(props) {
  return (
    <div style={styles.container}>
      <input
        name='confirmationCode'
        placeholder='Confirmation code'
        onChange={e => {e.persist();props.updateFormState(e)}}
        style={styles.input}
      />
      <input
        name='password'
        placeholder='New password'
        type='password'
        onChange={e => {e.persist();props.updateFormState(e)}}
        style={styles.input}
      />
      <Button onClick={props.forgotPasswordSubmit} title="Save new password" />
    </div>
  )
}

export default ForgotPasswordSubmit

Completing Form.js

Now that all of the individual form components have been created, we can update Form.js to use these new components.

The next thing we will do is open Form.js and create the functions that will interact with the authentication service. These functions, signIn, signUp, confirmSignUp, forgotPassword, and forgotPasswordSubmit, will be passed as props to the individual form components.

Below the last import, add the following code:

async function signIn({ username, password }, setUser) {
  try {
    const user = await Auth.signIn(username, password)
    const userInfo = { username: user.username, ...user.attributes }
    setUser(userInfo)
  } catch (err) {
    console.log('error signing up..', err)
  }
}

async function signUp({ username, password, email }, updateFormType) {
  try {
    await Auth.signUp({
      username, password, attributes: { email }
    })
    console.log('sign up success!')
    updateFormType('confirmSignUp')
  } catch (err) {
    console.log('error signing up..', err)
  }
}

async function confirmSignUp({ username, confirmationCode }, updateFormType) {
  try {
    await Auth.confirmSignUp(username, confirmationCode)
    updateFormType('signIn')
  } catch (err) {
    console.log('error signing up..', err)
  }
}

async function forgotPassword({ username }, updateFormType) {
  try {
    await Auth.forgotPassword(username)
    updateFormType('forgotPasswordSubmit')
  } catch (err) {
    console.log('error submitting username to reset password...', err)
  }
}

async function forgotPasswordSubmit({ username, confirmationCode, password }, updateFormType) {
  try {
    await Auth.forgotPasswordSubmit(username, confirmationCode, password)
    updateFormType('signIn')
  } catch (err) {
    console.log('error updating password... :', err)
  }
}

The signUp, confirmSignUp, forgotPassword, and forgotPasswordSubmit functions all take the same arguments, the form state and the updateFormType function to update the type of form that is displayed.

The signIn function is different than the other functions in that it takes in a setUser function that will be passed down as a prop from the Profile component. This setUser function will allow us to re-render the Profile component in order to show or hide the form once the user has successfully signed in.

In the previous chapter, the Profile.js component used the withAuthenticator component to render the form so we did not need to worry about rendering the proper UI ourselves. Now that we are handling our own form state, we will need to decide whether to render the Profile component or the Form component based on whether the user is authenticated or not.

You’ll notice that in these functions we are using different methods on the Auth class from AWS Amplify. These methods correspond with the naming of the functions we’ve created so that we know exactly what each of these functions are doing.

updateForm helper function

Next, let’s create a helper function for updating the form state. The initial form state variable that we created in Form.js looks like this:

const initialFormState = {
  username: '', password: '', email: '', confirmationCode: ''
}

This state is an object with values for each form that we will be using.

We then used this initialFormState variable to create the component state (as well a function to update the component state) using the useState hook:

const [formState, updateFormState] = useState(initialFormState)

The problem that we have now is that updateFormState is expecting a new object with all of these fields in order to update the form state, but a form handler only gives us the single form event that is being typed. How can we transform this input event into a new object for the state? We’ll do this by creating a helper function that we will use inside of the Form function.

In Form.js below the useState hooks inside of the Form function, add the following code:

function updateForm(event) {
  const newFormState = {
    ...formState, [event.target.name]: event.target.value
  }
  updateFormState(newFormState)
}

The updateForm function will create a new state object using the existing state as well as the new values coming in from the event and then call updateFormState with this new form object. We can then reuse this function across all of our components.

renderForm function

Now that we have all of the form components created, the form state set up, and the authentication functions created, let’s update the renderForm function to render the current form. In Form.js, update renderForm with the following code:

function renderForm() {
  switch(formType) {
    case 'signUp':
      return (
        <SignUp
          signUp={() => signUp(formState, updateFormType)}
          updateFormState={e => updateForm(e)}
        />
      )
    case 'confirmSignUp':
      return (
        <ConfirmSignUp
          confirmSignUp={() => confirmSignUp(formState, updateFormType)}
          updateFormState={e => updateForm(e)}
        />
      )
    case 'signIn':
      return (
        <SignIn
          signIn={() => signIn(formState, props.setUser)}
          updateFormState={e => updateForm(e)}
        />
      )
    case 'forgotPassword':
      return (
        <ForgotPassword
        forgotPassword={() => forgotPassword(formState, updateFormType)}
        updateFormState={e => updateForm(e)}
        />
      )
    case 'forgotPasswordSubmit':
      return (
        <ForgotPasswordSubmit
          forgotPasswordSubmit={() => forgotPasswordSubmit(formState, updateFormType)}
          updateFormState={e => updateForm(e)}
        />
      )
    default:
      return null
  }
}

The renderForm function will check the current formType that is set in the state and render the proper form. As the formType changes, renderForm will be called and subsequently re-render the correct form based on the formType.

Form type toggles

The last thing we will need to do in this component is render the buttons that will allow us to manually toggle between different form states. The three main form states that we will want to toggle between are signIn, signUp, and forgotPassword.

To do this, let’s update the return statement from the Form function to also return some buttons that allow the user to toggle the form type:

return (
  <div>
    {renderForm()}
    {
      formType === 'signUp' && (
        <p style={styles.toggleForm}>
          Already have an account? <span
            style={styles.anchor}
            onClick={() => updateFormType('signIn')}
          >Sign In</span>
        </p>
      )
    }
    {
      formType === 'signIn' && (
        <>
          <p style={styles.toggleForm}>
            Need an account? <span
              style={styles.anchor}
              onClick={() => updateFormType('signUp')}
            >Sign Up</span>
          </p>
          <p style={{ ...styles.toggleForm, ...styles.resetPassword}}>
            Forget your password? <span
              style={styles.anchor}
              onClick={() => updateFormType('forgotPassword')}
            >Reset Password</span>
          </p>
        </>
      )
    }
  </div>
)

The Form component will now different buttons based on the current form type and allow the user to toggle between signing in, signing up, and resetting their password.

Updating the Profile component

We now need to update the Profile component to use the new Form component. The main changes are that we will be that we will be rendering either the Form component or the user profile information based on whether there is a currently signed in user.

Amplify has a local eventing system called Hub. Amplify uses Hub for different categories to communicate with one another when specific events occur, such as authentication events like a user sign-in or notification of a file download.

In this component we will also be setting a Hub listener to listen for the signOut authentication event so that we can remove the user from the state and re-render the Profile component to show the authentication form.

Update Profile.js with the following code:

import React, { useState, useEffect } from 'react'
import { Button } from 'antd'
import { Auth, Hub } from 'aws-amplify'
import Container from './Container'
import Form from './Form'

function Profile() {
  useEffect(() => {
    checkUser()
    Hub.listen('auth', (data) => {
      const { payload } = data
      if (payload.event === 'signOut') {
        setUser(null)
      }
    })
  }, [])
  const [user, setUser] = useState(null)
  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))
  }
  if (user) {
    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>
    );
  }
  return <Form setUser={setUser} />
}

export default Profile

In this component we check to see if there is a user, and if so we return the profile information of the user. If there is no user, then we return the authentication form (Form). We pass in setUser as a prop to the Form component so that when a user signs in we can update the form state to re-render the component and show the profile information for that user.

Testing the app

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

npm start

Summary

Congratulations, you’ve built out a completely custom authentication flow!

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

  1. Use the Auth class for handling direct API calls to the Amazon Cognito authentication service.

  2. As you can see, handling custom form state can become verbose. Try to understand the tradeoffs between rolling your own authentication flow vs using something like the withAuthenticator higher order component.

  3. Authentication is complex. By using a managed identity service like Amazon Cognito we’ve abstracted away all of the back end code and logic. The only thing we have to know or understand is how to interact with the authentication APIs and then manage the local state.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.137.218