How to Easily Customize The AWS Amplify Authentication UI

How to Easily Customize The AWS Amplify Authentication UI

📅 29 November, 2018 – Kyle Galbraith

For parler.io I have been experimenting with adding authentication to the project. This would allow conversions to be tied to users and enable a slew of other features as well.

During my experimenting I have been reading a lot about AWS Amplify. It is a library that wraps multiple AWS services and allows you to focus on building mobile and web applications at scale on Amazon Web Services.

It makes adding various categories of features much simpler. Need authentication? There is a module for that. What about storage? Yup, there is one for that as well.

Amplify is meant to make stitching together AWS services a seamless process. A simple command line call can provide all of the services you need in your AWS account to handle authentication.

The Amplify Framework makes creating scalable mobile and web applications in AWS a simplified process. In this post, I am going to walk through how I used AWS Amplify to add authentication to Parler and how I customized the user interface components to fit my needs.

Getting Started

Amplify is an AWS provided framework. To get started we must install and configure the CLI for Amplify.

$ npm install -g @aws-amplify/cli

If you don’t have the AWS CLI installed and configured you are going to need to configure the Amplify CLI. If you already have the AWS CLI configured you don’t need to configure the Amplify one as well.

# only run this configure if you don't have the AWS CLI
$ amplify configure

Once the Amplify CLI is installed we can begin adding modules to our mobile or web application.

For my project, I am using Gatsby to build out the web application. This is a modern static site generator that can be used to quickly create static websites, blogs, portfolios, and even web applications. Since Gatsby is built on top of React we can use all of the same ideas from React in Gatsby.

Let’s initialize and configure our initial Amplify setup for a React web application.

Initializing Amplify

Now that we have the CLI installed globally we can initialize Amplify inside of our React app with one command line call.

# run this from the root directory of your application
$ amplify init

This command will initialize our AWS configuration and create a configuration file at the root of our application. This command will not provision any services in our AWS account, but it lays the groundwork for us to do so.

Adding authentication to our application

Now that we have initialized the framework in our application we can start adding modules. For this blog post, we are going to add the authentication module to our application.

We can do this with another call on our command line.

$ amplify add auth

This command will walk us through a series of questions. Each question is configuring the authentication for our application. If you are unsure what configuration you need, go ahead and select Yes, use the default configuration for the first question. You can always come back and reconfigure these settings by running the command amplify update auth.

We now have the authentication module configured for our application. But, we still need to deploy this configuration to our AWS account. Lucky for us, this is handled by the Amplify CLI as well.

$ amplify push

This will create and deploy the necessary changes to our AWS account to support our authentication module. With the default settings, this will provision AWS Cognito to handle authentication into our application.

When the deployment is complete we will have a new file in our source directory, aws-exports.js. This file represents the infrastructure inside of our AWS account to support our Amplify project.

Using Amplify with React

The Amplify framework has been added, we configured authentication, and we provisioned the necessary AWS services to support our application. Now it’s time we set up our React/Gatsby application to leverage the framework.

For the purpose of this blog post, we are going to assume we have an App component that is the main entry point for our application. We are also going to assume that you can’t access the application without being authenticated first.

Here is what our initial App component is going to look like. It is served at the /app route via a Gatsby configuration. Right now it is wide open to the world, no authentication is needed.

import React from "react";

class App extends React.Component {
  constructor(props, context) {
    super(props, context);
  }

  render() {
    return (
      <div>
        <h1>Internal App</h1>
      </div>
    );
  }
}

export default App;

With me so far? Great. Now we want to put our application behind the authentication module we added via Amplify. To do that we install two more libraries in our project.

$ npm install aws-amplify aws-amplify-react

Now that we have added these two libraries we can quickly add authentication to our application. First, we need to configure Amplify inside our App component. Then we can use a higher order component (HOC), withAuthenticator, specifically created for React applications. This component adds all of the logic to put our App component behind authentication. It also includes all of the UI pieces we need to log users in, sign up new users, and handle flows like confirming an account and resetting a password.

Let’s take a look at what these changes look like in our App component.

import React from "react";
import Amplify from "aws-amplify";
import { withAuthenticator } from "aws-amplify-react";
import config from "../../aws-exports";
Amplify.configure(config);

class App extends React.Component {
  constructor(props, context) {
    super(props, context);
  }

  render() {
    return (
      <div>
        <h1>Internal App</h1>
      </div>
    );
  }
}

export default withAuthenticator(App, true);

Just like that we now have authentication added to our React application that is built with Gatsby. If we run gatsby develop from our command line and check out our changes locally we should be able to see the default login prompt provided by Amplify.

default Amplify UI

Pretty slick right? With a few command line operations, we have authentication incorporated into our application. All of the AWS services needed to support our app are provisioned and continuously maintained by the Amplify Framework.

This is all fantastic, but for Parler, I also wanted the ability to customize the UI pieces that Amplify provides. These pre-configured UI components are great for getting started but I wanted to add my own style to them using Tailwind CSS.

So now let’s explore how to customize the authentication UI of Amplify by overriding the default components like SignIn with our own CustomSignIn component.

Customizing the Amplify authentication UI

To customize the look and feel of the Amplify authentication module we need to define our own components for the UI pieces we want to change.

For example, the login UI is handled by a component inside of Amplify called SignIn, you can see the full source code of that module here.

What we are going to do next is define our own component, CustomSignIn, that is going to extend the SignIn component from Amplify. This allows us to use all of the logic already built into the parent component but define our own UI. Let’s take a look at what CustomSignIn looks like.

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>
          <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="******************"
            />
            <p className="text-grey-dark text-xs">
              Forgot your password?{" "}
              <a
                className="text-indigo cursor-pointer hover:text-indigo-darker"
                onClick={() => super.changeState("forgotPassword")}
              >
                Reset Password
              </a>
            </p>
          </div>
          <div className="flex items-center justify-between">
            <button
              className="bg-blue hover:bg-blue-dark text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
              type="button"
              onClick={() => super.signIn()}
            >
              Login
            </button>
            <p className="text-grey-dark text-xs">
              No Account?{" "}
              <a
                className="text-indigo cursor-pointer hover:text-indigo-darker"
                onClick={() => super.changeState("signUp")}
              >
                Create account
              </a>
            </p>
          </div>
        </form>
      </div>
    );
  }
}

With CustomSignIn we are extending the SignIn component from aws-amplify-react. This is so that we can override the showComponent method but still use the parent class functions like changeState and signIn.

Notice that we are not overriding the render method but showComponent instead. This is because the parent SignIn component defines the UI inside of that function. Therefore, to show our UI we need to override it in our component.

Inside of our constructor we see the following statement.

this._validAuthStates = ["signIn", "signedOut", "signedUp"];

Amplify uses authState to track which authentication state is currently active. The custom components we define can state which auth states are valid for this component. Since we are on the login/sign in view, we only want to render our custom UI if authState equals signIn, signedOut, or signedUp. That is all of the magic sauce happening to show our UI over the default Amplify provided UI.

We extend the SignIn component, override the showComponent function, check the authState and show our UI if the state is the one that we are looking for.

Pretty slick right?

Diving into the custom UI a bit we see the “Create Account” button makes a call to super.changeState("signUp") when its clicked. This is a function defined in the parent component we are extending. It updates the authState to signUp and the SignUp component is rendered. We could, of course, customize this component as well following the same process we used to create CustomSignIn.

The only other change we need to make now is back out in our App component. Instead of using the withAuthenticator HOC provided by Amplify we are going to use the Authenticator component directly.

To make things clearer we are going to define a new component, AppWithAuth, that wraps our App component and makes use of the Authenticator component directly.

import React from "react";
import { SignIn } from "aws-amplify-react";
import config from "../../aws-exports";
import { CustomSignIn } from "../Login";
import App from "../App";
import { Authenticator } from "aws-amplify-react/dist/Auth";

class AppWithAuth extends React.Component {
  constructor(props, context) {
    super(props, context);
  }

  render() {
    return (
      <div>
        <Authenticator hide={[SignIn]} amplifyConfig={config}>
          <CustomSignIn />
          <App />
        </Authenticator>
      </div>
    );
  }
}

export default AppWithAuth;

Now our App component will receive the authState, just like our other components, inside of its render method. If we check the state inside of that method we can show our App component only when we are signed in. Let’s take a look at our new App component code.

import React from "react";

class App extends React.Component {
  constructor(props, context) {
    super(props, context);
  }

  render() {
    if (this.props.authState == "signedIn") {
      return (
        <div>
          <h1>Internal App</h1>
        </div>
      );
    } else {
      return null;
    }
  }
}

export default App;

Now our App component is very minimal. In fact, the only notion we have of Amplify here is checking our authState which determines whether or not we should render this component.

parler.io authentication with Amplify

Just like that, we have added authentication to our application using the Amplify Framework. We have also customized the components of Amplify to give our own look, feel, and logic if we need it.

Conclusion

The Amplify Framework is an awesome new tool in our AWS toolbox. We demonstrated here that we can add authentication to any web or mobile application with just a few CLI commands. We can then deploy the AWS services that back modules like authentication with a simple push call.

But sometimes we want to add our own style to these types of frameworks. Not a problem. We showed that we can extend the base components inside of Amplify to create our user interfaces as well as hide the ones we don’t care about.

Amplify continues to evolve and consists of many more modules like hosting, api, auth, and even storage. All key modules and AWS services that are important to most web applications. In addition, they also just announced Amplify Console which contains a global CDN to host your applications as well as a CI/CD Pipeline.

Are you hungry to learn even more about Amazon Web Services?

If you are looking to begin your AWS journey but feel lost on where to start, consider checking out my course. We focus on hosting, securing, and deploying static websites on AWS. Allowing us to learn over 6 different AWS services as we are using them. After you have mastered the basics there we can then dive into two bonus chapters to cover more advanced topics like Infrastructure as Code and Continous Deployment.