Auth flow with Aws Amplify, ReactJs, and Tailwind CSS
Tags: aws, react, tailwindcss, javascript
Published: 2021-03-24 21:24:51

> Source code for this post can be found [here](https://github.com/Rolando-Barbella/my-aws-login-app)

Not too long ago, writing a full authentication flow for an app, was a task that required a lot of effort and care, nowadays with tools like [Aws amplify](https://docs.amplify.aws/cli/auth/overview) and modern UI libraries ( exp: ReactJS ), we are just hours away from building this essential functionality.

Let's start by creating an app with [create react app] (https://github.com/facebook/create-react-app)

> npx create-react-app my-login-app

## Environment & prerequisites
Before we begin with all the other setups, make sure you have the following installed:

- Node.js v10.x or later installed
- A valid and confirmed AWS account

## Installing and Initializing an AWS Amplify Project

>NPM
>$ npm install -g @aws-amplify/cli

>cURL (Mac & Linux)
>curl -sL https://aws-amplify.github.io/amplify-cli/install | >bash && $SHELL

>cURL (Windows)
>curl -sL https://aws-amplify.github.io/amplify-cli/install->win -o install.cmd && install.cmd

Let's now configure the CLI with our credentials.

If you'd like to see a walkthrough of this configuration process, [Nader Dabit](https://dev.to/dabit3) has a video that show's how [here](https://www.youtube.com/watch?v=fWbM5DLh25U).

> $ amplify configure

```
- Specify the AWS Region: us-east-1 || us-west-2 || eu-central-1
- Specify the username of the new IAM user: your-user-name
> In the AWS Console, click Next: Permissions, Next: Tags, Next: Review, & Create User to create the new IAM user. Then return to the command line & press Enter.
- Enter the access key of the newly created user:   
? accessKeyId: (<YOUR_ACCESS_KEY_ID>)  
? secretAccessKey: (<YOUR_SECRET_ACCESS_KEY>)
- Profile Name: your-user-name
```

Let's initialize a new amplify setup by running: >$ amplify init

```
? Enter a name for the project myloginapp
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? 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
Using default provider  awscloudformation

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html

? Do you want to use an AWS profile? (Y/n) y
? Please choose the profile you want to use: your-user-name
```
Now, let's open the `src/index.js` file and add the following lines:

```js
import App from './App';
....
import Amplify from 'aws-amplify';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);

```
We now need to add our authentication service, Amplify uses Amazon Cognito as the main authentication provider, which gives all the tools to handle registration, authentication, account recovery & other operations.

Let's follow the next steps:

> amplify add auth

```
❯ Default configuration 
  Default configuration with Social Provider (Federation) 
  Manual configuration 
  I want to learn more. 

 How do you want users to be able to sign in? (Use arrow keys)
❯ Username 
  Email 
  Phone Number 
  Email or Phone Number 
  I want to learn more. 

 Do you want to configure advanced settings? (Use arrow keys)
❯ No, I am done. 
  Yes, I want to make some additional changes. 

```
Finally, we can push our progress to our account so that AWS can know about it, by doing `amplify push`, this will build all your local backend resources and provision them in the cloud.

## Tailwind CSS

We want to have a nice looking design without spending too much time on the CSS side, let's install [Tailwind CSS](https://tailwindcss.com/) quickly by running:

> npm install -D tailwindcss postcss autoprefixer

Then, let's do:

> npx tailwindcss init -p

Inside the `package.json` file let's add the following lines inside the `scripts` object:

```json
  "scripts": {
    "build:tailwind": "tailwindcss build src/tailwind.css -o src/tailwind.generated.css",
    "prestart": "npm run build:tailwind",
    "prebuild": "npm run build:tailwind",
.....
```
Let's create a new file inside our src folder `src/tailwind.css`, where we are going to be importing tailwind default styles, add the following lines:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
These two steps will generate a new `tailwind.generated.css` file which we then need to import into the `App.js` file in order to be able to use tailwind classes across the whole app.

```js
import './App.css';
import './tailwind.generated.css';
```

Finally, let's clean our `App.css` file by just leaving this amount of code:

```css
.App {
  text-align: center;
}

.App-header {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
}
```

## React router

We are going to need the help of a router "system" to manage a few screens, lest's install react-router:

> npm i react-router && react-router-dom

Let's also update the `App.js` for the following:

```js
import React from 'react';
import {
  BrowserRouter as Router,
  Switch,
  Route,
} from "react-router-dom";
import './App.css'

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Router>
          <Switch>
            <div>
              My app
            </div>
         </Switch>
        </Router>
      </header>
    </div>
  )
}

export default App

```
Time to create our first screen for the auth flow, on a `component/Register.js` file, add the following code:

```js
import React, { useState } from 'react';
import { Auth } from 'aws-amplify';
import Input from '../../common/Input';
import { Link, useHistory } from 'react-router-dom';

const Register = () => {
  let history = useHistory();
  const [user, setUser] = useState({ username: '', password: '', });

  const handleInputChange = (event, keyName) => {
    event.persist();
    setUser((user) => {
      return { ...user, [keyName]: event.target.value }
    })
  }

  const signUp = async () => {
    try {
      await Auth.signUp({
        username: user.username,
        password: user.password,
        attributes: {
          email: user.username,
        }
      });
      history.push("/confirm-register");
    } catch (error) {
      console.log('error', error);
    }
  }

  return (
    <div className="container w-4/12 w-medium">
      <div className="bg-white shadow-xl rounded px-12 pt-6 pb-8 mb-4">
        <h3 className="text-lg text-gray-700">Register</h3>
        <Input
          labelName='Email:'
          value={user.username}
          handleInputChange={(e) => handleInputChange(e, 'username')}
        />
        <Input
          labelName='Password:'
          type="password"
          value={user.password}
          handleInputChange={(e) => handleInputChange(e, 'password')}
        />
        <div className="flex items-center justify-between">
          <button
            className="mt-4 mb-4 w-full sm:w-auto border border-transparent px-6 py-3 text-base font-semibold leading-snug bg-gray-900 text-white rounded-md shadow-md hover:bg-gray-800 focus:outline-none focus:bg-gray-800 transition ease-in-out duration-150 hover:bg-gray-600"
            type="button"
            onClick={() => signUp()}
          >
            Send
          </button>
        </div>
        <div className="w-full">
          <hr />
          <p className="text-gray-700 pb-2 pt-2 text-sm">You already habe an account?</p>
          <Link
            to={{
              pathname: '/log-in'
            }}
            className="pt-2 text-sm text-blue-500 hover:text-blue-600"
          >
            Long in
          </Link>
        </div>
      </div>
    </div>
  )
}

export default Register;
```
Create a `common/Input.js` file for our small but practical input component. 
```js

import React from 'react';

const Input =({ labelName, value, type="text", handleInputChange }) => {
  return (
    <div className="pb-15">
      <label className="block text-gray-700 text-sm font-bold mb-2">{labelName}</label>
      <input
        type={type}
        className="account-input bg-white focus:outline-none focus:shadow-outline border border-gray-300 rounded-sm py-2 px-2 block w-full appearance-none leading-normal"
        value={value}
        onChange={handleInputChange}
      />
    </div>
  )
}

export default Input;

```

We are still not ready to test our app, after users add their register details (email and password), they will receive a confirmation email with a unique code to activate their account. Let's create a `component/ConfirmRegister` screen for this step.

```js
import { Auth } from 'aws-amplify';
import React, { useState } from 'react';
import Input from '../../common/Input';
import { Link, useHistory } from "react-router-dom";

const ConfirmRegister = () => {
  let history = useHistory();
  const [user, setUser] = useState({ username: '', authenticationCode: '', });

  const handleInputChange = (event, keyName) => {
    event.persist();
    setUser((user) => {
      return { ...user, [keyName]: event.target.value }
    })
  }

  const confirmSignUp = async () => {
    try {
      await Auth.confirmSignUp(user.username, user.authenticationCode);
      console.log('success confirm sign up');
      history.push('./log-in')
    } catch (error) {
      console.log('error', error);
    }
  }

  return (
    <div className="container w-4/12 w-medium">
      <div className="bg-white shadow-xl rounded px-12 pt-6 pb-8 mb-4">
        <h3 className="text-lg text-gray-700">Confirm your account</h3>
        <Input
          labelName='Email:'
          value={user.username}
          handleInputChange={(e) => handleInputChange(e, 'username')}
        />
        <Input
          labelName='Code:'
          value={user.authenticationCode}
          handleInputChange={(e) => handleInputChange(e, 'authenticationCode')}
        />
        <button
          onClick={() => confirmSignUp()}
          className="mt-4 mb-4 w-full sm:w-auto border border-transparent px-6 py-3 text-base font-semibold leading-snug bg-gray-900 text-white rounded-md shadow-md hover:bg-gray-800 focus:outline-none focus:bg-gray-800 transition ease-in-out duration-150 hover:bg-gray-600"
        >
          Confirm
        </button>
        <div>
          <Link
            to={{
              pathname: '/register'
            }}
            className="pt-2 text-sm text-blue-500 hover:text-blue-600"
          >
            Back
          </Link>
        </div>
      </div>
    </div>
  )
}

export default ConfirmRegister;

``` 
Our app is ready to start registering new accounts, you don't necessarily need to use your personal email, this brilliant [10 min email](https://10minutemail.com/) site can provide you a temporary one.

Now that we have registered user(s), let's create our `components/Login.js` page by adding this code:

```js
import { Auth } from 'aws-amplify';
import React, { useState } from 'react';
import { useHistory, Link } from "react-router-dom";
import Input from './common/Input';

const LogIn = () => {
  let history = useHistory();
  const [user, setUser] = useState({ username: '', password: '' });

  const handleInputChange = (event, keyName) => {
    event.persist();
    setUser((user) => {
      return { ...user, [keyName]: event.target.value }
    })
  }

  const logIn = async () => {
    try {
      await Auth.signIn({
        username: user.username,
        password: user.password,
      });
      history.push('./home')
    } catch (error) {
      console.error('error', error);
    }
  }
  return (
    <div className="container w-4/12 w-medium">
      <div className="bg-white shadow-xl rounded px-12 pt-6 pb-8 mb-4">
        <h3 className="text-lg text-gray-800 mb-2">Log In</h3>
        <Input
          labelName='Email:'
          value={user.username}
          handleInputChange={(e) => handleInputChange(e, 'username')}
        />
        <Input
          labelName='Password:'
          type="password"
          value={user.password}
          handleInputChange={(e) => handleInputChange(e, 'password')}
        />
        <div className="flex items-center justify-between">
          <button
            onClick={() => logIn()}
            className="mt-4 mb-4 w-full sm:w-auto border border-transparent px-6 py-3 text-base font-semibold leading-snug bg-gray-900 text-white rounded-md shadow-md hover:bg-gray-800 focus:outline-none focus:bg-gray-800 transition ease-in-out duration-150 hover:bg-gray-600"
          >
            Log in
          </button>
        </div>
        <div className="w-full">
          <hr />
          <p className="text-gray-700 pb-2 pt-2 text-sm">Don't have an account?</p>
          <Link
            to={{
              pathname: '/register'
            }}
            className="pt-2 text-sm text-blue-500 hover:text-blue-600"
          >
            Register
          </Link>
        </div>
      </div>
    </div>
  )
}

export default LogIn;
```
After the user is login successful, we can finally grant them access to the home page.

Let's create a simple `components/Home` page component: 

```js
import React from 'react'
import Auth from '@aws-amplify/auth';
import { Link } from "react-router-dom";

const Home = () => {
  let signOut = async() => {
    await Auth.signOut();
    console.log("Sign out succesfully")
  }
  return (
    <div>
      <h2 className="px-3 mb-3 lg:mb-3 uppercase tracking-wide font-semibold text-sm lg:text-lg text-gray-900">
        Home page
      </h2>
      <div className="ml-3 text-base">
        <Link
          to={{
            pathname: '/log-in',
          }}
          onClick={signOut}
          className="pt-2 text-sm text-gray-500 hover:text-gray-600"
        >
          Log out
        </Link>
      </div>
    </div>
  )
}

export default Home

```
We just need to put all these routes together to make the connection between pages with the help of [react router](https://reactrouter.com/), let's change our `App.js` file to look like:

```js
import React from 'react';
import {
  BrowserRouter as Router,
  Switch,
  Route,
} from "react-router-dom";
import Login from './components/Login';
import Register from './components/Register';
import Home from './components/Home';
import ConfirmRegister from './components/ConfirmRegister';

import './App.css';
import './tailwind.generated.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Router>
          <Switch>
            <Route component={Home} path="/home" />
            <Route component={ConfirmRegister} path="/confirm-register" />
            <Route component={Login} path="/log-in" />
            <Route component={Register} path="/" />
          </Switch>
        </Router>
      </header>
    </div>
  )
}

export default App

```
Finally, we can start testing our app, create accounts, logging in, etc! let's run `npm start` or `yarn start`, our registration page at `http://localhost:3000` should be the first one to come up, looking like this:

![register](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sz5xgld5y7ysnjrcux8v.png)
 
But wait, this app is not fully done! someone can actually navigate to the home page (http://localhost:3000/home) without having an account or being authenticated, that's pretty bad! 

Let's write a private route to solve this issue and secure our app, create a new `components/PrivateRoute` file.

```js

import React, { useState, useEffect }  from 'react';
import { Redirect, Route } from "react-router-dom";
import { Auth } from 'aws-amplify';
import Homepage from './Home'

const PrivateRoute = ({ children, ...rest }) => {
  const [signInUser, setSignInUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    let getUser = async() => {
      try {
        let user = await Auth.currentAuthenticatedUser();
        await setSignInUser(user);
        setIsLoading(false);
      } catch (error) {
        setIsLoading(false);
        console.log(error)        
      }
    }
    getUser();
  },[]);

  if(isLoading) {
    return <p>...Loading</p>
  }
  return (
    <Route {...rest} render={({ location }) => {
      return signInUser? <Homepage/>
        : <Redirect to={{
            pathname: '/log-in',
            state: { from: location }
          }} />
    }} />
  )
}

export default PrivateRoute;
```
In the `App.js` file, let's "wrap" the home page with our private route component
```js
.....
import PrivateRoute from './components/PrivateRoute';

import './App.css';
import './tailwind.generated.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Router>
          <Switch>
            <PrivateRoute path="/home">
            </PrivateRoute>
            <Route component={ConfirmRegister} path="/confirm-register" />
            <Route component={Login} path="/log-in" />
            <Route component={Register} path="/" />
          </Switch>
        </Router>
      </header>
    </div>
  )
}

export default App
```
## Conclusion

We have our custom auth flow thanks mainly to AWS amplify and react, it even has a navigation security layer (the private route), all done in just a few steps.

## Todo
You can do some homework by adding a forget password feature to the flow, let me know in the comments if you have questions.

If you think other people should read this post. Tweet, share and Follow me on [twitter](https://twitter.com/rolandobarbella) for the next articles