Customizing the AWS Amplify Authentication UI with Your Own React Components
Tags: aws, security, webdev, react
Published: 2020-03-31 21:47:47

---
title: Customizing the AWS Amplify Authentication UI with Your Own React Components
published: true
description: 
tags: #aws #security #webdev #react
canonical_url: https://blog.kylegalbraith.com/2020/03/31/customizing-the-aws-amplify-authentication-ui-with-your-own-react-components/
cover_image: https://blog.kylegalbraith.com/static/2e3e0fb186e22890c2bde3955fe24d80/e7401/customizing-the-aws-amplify-authentication-ui-with-your-own-react-components.webp
---
I have written before about [customizing the authentication UI that AWS Amplify](https://blog.kylegalbraith.com/2018/11/29/how-to-easily-customize-the-aws-amplify-authentication-ui/) gives you out of the box. But since writing that post I have received lots of questions around more robust ways to do this.

In my latest project [parler.io users can quickly convert written content into audio](https://www.parler.io/). Underneath the hood, parler makes use of a lot of Amplify functionality. Authentication being one.

In this post, we are going to leverage AWS Amplify authentication while still building the UI we want.

### Prerequisites
Seeing as this is a post about AWS and AWS Amplify, you should be set up with both of those. Don't have an AWS account yet? You can set one up [here](https://aws.amazon.com/). 

To interact with AWS Amplify you need to install the CLI via `npm`.

```bash
$ yarn global add @aws-amplify/cli
```

### Setting up our project
Before we can show how to build a custom UI using Amplify, we first need a project to work from. Let's use `create-react-app` to get a React app going.

```bash
$ npx create-react-app amplify-demo
$ cd amplify-demo
```

With our boilerplate project created we can now add the Amplify libraries we are going to need to it.

```bash
$ yarn add aws-amplify aws-amplify-react
```

Now we need to initialize Amplify and add authentication to our application. From the root of our new `amplify-demo` application, run the following commands with the following answers to each question.

```bash
$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project amplify-demo
? Enter a name for the environment prod
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building: javascript
? What javascript framework are you using react
? Source Directory Path:  src
? Distribution Directory Path: build
? Build Command:  npm run-script build
? Start Command: npm run-script start
```

```bash
$ amplify add auth
Using service: Cognito, provided by: awscloudformation
 
 The current configured provider is Amazon Cognito. 
 
 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.
Successfully added resource amplifydemobc1364f5 locally
```

Now that we have the default authentication via Amplify added to our application we can add the default login. To do that go ahead and update your `App` component located at `src/App.js` to have the following code.

```javascript
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import { withAuthenticator } from "aws-amplify-react";
import Amplify from "aws-amplify";
import awsconfig from "./aws-exports";

Amplify.configure(awsconfig);

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Internal Application behind Login</p>
      </header>
    </div>
  );
}

export default withAuthenticator(App);
```

The default Amplify authentication above leverages the higher-order component, `withAuthenticator`. We should now be able to see that our `App` component is behind a login. Go ahead and start the app up in development mode by running `yarn start`. We should see something like below.

![Default Amplify Authentication](https://dev-to-uploads.s3.amazonaws.com/i/ce5qqtwssm16paiq0r31.png)

### Customizing The Amplify Authentication UI
Now that we have the default authentication wired up it's time to customize it. In the [previous blog post](https://blog.kylegalbraith.com/2018/11/29/how-to-easily-customize-the-aws-amplify-authentication-ui/) we essentially inherited from the internal Amplify components like `SignIn`. This allowed us to leverage the functions already defined in that component.

But, this felt like the wrong abstraction and a bit of a hack for the long term. It was/is a valid way to get something working. But it required knowing quite a few of the implementation details implemented in the parent component.

Things like knowing how `handleInputChange` and `_validAuthStates` were getting used in `SignIn` were critical to making the brute force version below work as expected.

```javascript
import React from "react";
import { SignIn } from "aws-amplify-react";

export class CustomSignIn extends SignIn {
  constructor(props) {
    super(props);
    this._validAuthStates = ["signIn", "signedOut", "signedUp"];
  }

  showComponent(theme) {
    return (
      <div className="mx-auto w-full max-w-xs">
        <form className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
          <div className="mb-4">
            <label
              className="block text-grey-darker text-sm font-bold mb-2"
              htmlFor="username"
            >
              Username
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker leading-tight focus:outline-none focus:shadow-outline"
              id="username"
              key="username"
              name="username"
              onChange={this.handleInputChange}
              type="text"
              placeholder="Username"
            />
          </div>
          .....omitted.....
        </form>
      </div>
    );
  }
}
```

But in running with this brute force approach for a bit I was able to form up a better way to customize the Amplify authentication UI. The approach, as we are going to see, boils down to three changes.

1. Instead of using the higher-order component, `withAuthenticator`. We are going to instead use the `<Authenticator>` component instead. This is the component built into the framework that allows for more customization.
2. We are going to change our `App` component to make use of an `AuthWrapper` component that we will write. This is the component that can manage the various states of authentication we can be in.
3. Finally, we will write our own `CustomSignIn` component to have it's own UI and logic.

Let's go ahead and dive in with 1️⃣. Below is what our `App` component is going to look like now.

```javascript
import React from "react";
import { Authenticator } from "aws-amplify-react";
import "./App.css";
import Amplify from "aws-amplify";
import awsconfig from "./aws-exports";
import AuthWrapper from "./AuthWrapper";

Amplify.configure(awsconfig);

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Authenticator hideDefault={true} amplifyConfig={awsconfig}>
          <AuthWrapper />
        </Authenticator>
      </header>
    </div>
  );
}

export default App;
```

Notice that our `App` component is now an entry point into our application. It uses the `Authenticator` component provided by Amplify instead of the higher-order component. We tell that component to hide all the default authentication UI, we are going to create our own. Then inside of that, we make use of a new component we are going to create called `AuthWrapper`.

This new component is going to act as our router for the different authentication pieces we want to have. For this blog post, we are just going to implement the login workflow. But the idea is transferrable to other things like signing up and forgot password. Here is what `AuthWrapper` ends up looking like.

```javascript
import React, { Component } from "react";
import { InternalApp } from "./InternalApp";
import { CustomSignIn } from "./SignIn";

class AuthWrapper extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: ""
    };
    this.updateUsername = this.updateUsername.bind(this);
  }

  updateUsername(newUsername) {
    this.setState({ username: newUsername });
  }

  render() {
    return (
      <div className="flex-1">
        <CustomSignIn
          authState={this.props.authState}
          updateUsername={this.updateUsername}
          onStateChange={this.props.onStateChange}
        />
        <InternalApp
          authState={this.props.authState}
          onStateChange={this.props.onStateChange}
        />
      </div>
    );
  }
}

export default AuthWrapper;
```

Here we can see that `AuthWrapper` is a router for two other components. The first one is `CustomSignIn`, this is the custom login UI we can build-out. The second one is our `InternalApp` which is the application UI signed in users can access. Note that both components get the `authState` passed into them. Internally the components can use this state to determine what they should do.

Before taking a look at the `CustomSignIn` component, let's look at `InternalApp` to see how `authState` is leveraged.

```javascript
import React, { Component } from "react";
import logo from "../src/logo.svg";

export class InternalApp extends Component {
  render() {
    if (this.props.authState === "signedIn") {
      return (
        <>
          <img src={logo} className="App-logo" alt="logo" />
          <p>Internal Application behind Login</p>
        </>
      );
    } else {
      return null;
    }
  }
}
``` 
Notice that we are checking that `authState === "signedIn"` to determine if we should render the application UI. This is a piece of state that is set by the authentication components defined in `AuthWrapper`.

Now let's see what our customized authentication for the login prompt looks like. Here is what `CustomSignIn` looks like.

```javascript
import React, { Component } from "react";
import { Auth } from "aws-amplify";

export class CustomSignIn extends Component {
  constructor(props) {
    super(props);
    this._validAuthStates = ["signIn", "signedOut", "signedUp"];
    this.signIn = this.signIn.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleFormSubmission = this.handleFormSubmission.bind(this);
    this.state = {};
  }

  handleFormSubmission(evt) {
    evt.preventDefault();
    this.signIn();
  }

  async signIn() {
    const username = this.inputs.username;
    const password = this.inputs.password;
    try {
      await Auth.signIn(username, password);
      this.props.onStateChange("signedIn", {});
    } catch (err) {
      if (err.code === "UserNotConfirmedException") {
        this.props.updateUsername(username);
        await Auth.resendSignUp(username);
        this.props.onStateChange("confirmSignUp", {});
      } else if (err.code === "NotAuthorizedException") {
        // The error happens when the incorrect password is provided
        this.setState({ error: "Login failed." });
      } else if (err.code === "UserNotFoundException") {
        // The error happens when the supplied username/email does not exist in the Cognito user pool
        this.setState({ error: "Login failed." });
      } else {
        this.setState({ error: "An error has occurred." });
        console.error(err);
      }
    }
  }

  handleInputChange(evt) {
    this.inputs = this.inputs || {};
    const { name, value, type, checked } = evt.target;
    const check_type = ["radio", "checkbox"].includes(type);
    this.inputs[name] = check_type ? checked : value;
    this.inputs["checkedValue"] = check_type ? value : null;
    this.setState({ error: "" });
  }

  render() {
    return (
      <div className="mx-auto w-full max-w-xs">
        <div className="login-form">
          {this._validAuthStates.includes(this.props.authState) && (
            <form
              className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
              onSubmit={this.handleFormSubmission}
            >
              <div className="mb-4">
                <label
                  className="block text-grey-darker text-sm font-bold mb-2"
                  htmlFor="username"
                >
                  Username
                </label>
                <input
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker leading-tight focus:outline-none focus:shadow-outline"
                  id="username"
                  key="username"
                  name="username"
                  onChange={this.handleInputChange}
                  type="text"
                  placeholder="Username"
                />
              </div>
              <div className="mb-6">
                <label
                  className="block text-grey-darker text-sm font-bold mb-2"
                  htmlFor="password"
                >
                  Password
                </label>
                <input
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker mb-3 leading-tight focus:outline-none focus:shadow-outline"
                  id="password"
                  key="password"
                  name="password"
                  onChange={this.handleInputChange}
                  type="password"
                  placeholder="******************"
                />
              </div>
              <div className="flex items-center justify-between">
                <button
                  className="bg-indigo-400 text-white py-2 px-4 rounded focus:outline-none focus:shadow-outline"
                  type="submit"
                  onClick={this.handleFormSubmission}
                >
                  Login
                </button>
              </div>
            </form>
          )}
        </div>
      </div>
    );
  }
}
```

What we have defined up above is a React component that is leveraging the Amplify Authentication API. If we take a look at `signIn` we see many calls to `Auth` to sign a user in or resend them a confirmation code. We also see that `this._validAuthStates` still exists. This internal parameter to determines whether we should show this component inside of the `render` function. 

This is a lot cleaner and is not relying on knowing the implementation details of base components provided by Amplify. Making this not only more customizable but a lot less error-prone as well.

If you take a look at the class names inside of the markup you'll see that this component is also making use of [TailwindCSS](https://tailwindcss.com/). Speaking as a non-designer, Tailwind is a lifesaver. It allows you to build out clean looking interfaces with utility first classes. 

To add Tailwind into your own React project, complete these steps.

1. Run `yarn add tailwindcss --dev` in the root of your project.
2. Run `./node_modules/.bin/tailwind init tailwind.js` to initialize Tailwind in the root of your project.
3. Create a CSS directory `mkdir src/css`.
4. Add a tailwind source CSS file at `src/css/tailwind.src.css` with the following inside of it.
```
@tailwind base;
@tailwind components;
@tailwind utilities;
```

From there we need to update the `scripts` in our `package.json` to build our CSS before anything else.

```
"scripts": {
    "tailwind:css":"tailwind build src/css/tailwind.src.css -c  tailwind.js -o src/css/tailwind.css",
    "start": "yarn tailwind:css && react-scripts start",
    "build": "yarn tailwind:css && react-scripts build",
    "test": "yarn tailwind:css && react-scripts test",
    "eject": "yarn tailwind:css && react-scripts eject"
  }
```

Then it is a matter of importing our new Tailwind CSS file, `import "./css/tailwind.css";` into the root of our app which is `App.js`.

💥 We can now make use of Tailwind utility classes inside of our React components.

### Conclusion
AWS Amplify is gaining a lot of traction and it's not hard to see why. They are making it easier and easier to integrate apps into the AWS ecosystem. By abstracting away things like authentication, hosting, etc, folks are able to get apps into AWS at lightning speed.

But, with abstractions can come guard rails. Frameworks walk a fine line between providing structure and compressing creativity. They need to provide a solid foundation to build upon. But at the same time, they need to provide avenues for customization. 

As we saw in this post the default Amplify authentication works fine. But we probably don't want exactly that when it comes to deploying our own applications. With a bit of work and extending the framework into our application, we were able to add that customization.

### Want to check out my other projects?
I am a huge fan of the DEV community. If you have any questions or want to chat about different ideas relating to refactoring, [reach out on Twitter](https://twitter.com/kylegalbraith) or drop a comment below.

Outside of blogging, I created a [Learn AWS By Using It course](https://kylegalbraith.com/learn-aws/). In the course, we focus on learning Amazon Web Services by actually using it to host, secure, and deliver static websites. It's a simple problem, with many solutions, but it's perfect for ramping up your understanding of AWS. I recently added two new bonus chapters to the course that focus on Infrastructure as Code and Continuous Deployment.