DEV Community

Rolando for AWS Community Builders

Posted on • Edited on

Auth flow with Aws Amplify, ReactJs, and Tailwind CSS

Source code for this post can be found here

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 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

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 has a video that show's how here.

$ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now, let's open the src/index.js file and add the following lines:

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

Enter fullscreen mode Exit fullscreen mode

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. 

Enter fullscreen mode Exit fullscreen mode

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 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:

  "scripts": {
    "build:tailwind": "tailwindcss build src/tailwind.css -o src/tailwind.generated.css",
    "prestart": "npm run build:tailwind",
    "prebuild": "npm run build:tailwind",
.....
Enter fullscreen mode Exit fullscreen mode

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:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

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.

import './App.css';
import './tailwind.generated.css';
Enter fullscreen mode Exit fullscreen mode

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

.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);
}
Enter fullscreen mode Exit fullscreen mode

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:

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

Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

Create a common/Input.js file for our small but practical input component.


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;

Enter fullscreen mode Exit fullscreen mode

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.

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;

Enter fullscreen mode Exit fullscreen mode

Our app is ready to start registering new accounts, you don't necessarily need to use your personal email, this brilliant 10 min email 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:

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;
Enter fullscreen mode Exit fullscreen mode

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:

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

Enter fullscreen mode Exit fullscreen mode

We just need to put all these routes together to make the connection between pages with the help of react router, let's change our App.js file to look like:

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

Enter fullscreen mode Exit fullscreen mode

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

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.


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;
Enter fullscreen mode Exit fullscreen mode

In the App.js file, let's "wrap" the home page with our private route component

.....
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
Enter fullscreen mode Exit fullscreen mode

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 for the next articles

Top comments (4)

Collapse
 
bhison profile image
Tim Bhison

Thank you so much for this. I have been wrangling with cognito for weeks and this finally was a simple and effective, up to date guide.

Collapse
 
adeleke5140 profile image
Kehinde Adeleke • Edited

Wow... I'm pretty new to react and I need to implement an auth flow soon so this article is not only timely but super helpful. Thank you.
But for me to use AWS amplify, should the account be for my company or for me personally?

Collapse
 
doncoder profile image
Rolando

Thanks for the comment Kehinde, the AWS account can be set for any of the two, hope that helps :)

Collapse
 
tbrangman profile image
Ty

Hey Rolando, how would you set up the PrivateRoutes if you wanted multiple "containers"/components to have them?