DEV Community

Christian Nwamba
Christian Nwamba

Posted on

How to Setup Authentication Fast with AWS Amplify

In this article, we will build a blog application using AWS Amplify which allows users to sign in and start blogging right after signup has been completed.

AWS Amplify enables front-end and mobile developers to easily build and deploy full-stack web and mobile applications powered by AWS. Authentication is one of the most vital features in an application as it determines the identity of a user. Recently, the Amplify team added a new feature to the Amplify authentication flow. The feature allows users to automatically sign in to the app immediately after signing up.

Pre-requisites

To follow along, you need to have:

  • AWS account
  • Knowledge of JavaScript and React
  • A code editor (VS Code preferably)
  • Nodejs >=v14 installed
  • AWS Amplify CLI installed. Run this command if you don’t have it installed npm install -g @aws-amplify/cli
  • Authorized AWS Amplify CLI amplify configure

The complete code for this post is on this Github repository.

Setting up the React project

We’ll be building a simple React application for this post. Run this command to scaffold a new React project:

     npx create-react-app react-amplify-auth
Enter fullscreen mode Exit fullscreen mode

When it’s done installing, change directory to the react project, and open the folder with your code editor.

    cd react-amplify-auth
    code .
Enter fullscreen mode Exit fullscreen mode

Let’s go ahead to configure amplify for the project and create a user profile.

Setting up Amplify

We have setup a user profile for the amplify project, let’s now initialize amplify in the project. Run this command on your terminal.

 amplify init
Enter fullscreen mode Exit fullscreen mode

Select these options:

    ➜  react-amplify-auth git:(master) amplify init
    Note: It is recommended to run this command from the root of your app directory
    ? Enter a name for the project reactamplifyauth
    The following configuration will be applied:

    Project information
    | Name: reactamplifyauth
    | Environment: dev
    | Default editor: Visual Studio Code
    | App type: javascript
    | Javascript framework: react
    | Source Directory Path: src
    | Distribution Directory Path: build
    | Build Command: npm run-script build
    | Start Command: npm run-script start

    ? Initialize the project with the above configuration? Yes
    Using default provider  awscloudformation
    ? Select the authentication method you want to use: AWS profile

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

    ? Please choose the profile you want to use react-amplify-auth
Enter fullscreen mode Exit fullscreen mode

You will see some installation logs and when it’s done, you will see a message like this:

    CREATE_COMPLETE amplify-reactamplifyauth-dev-133741 AWS::CloudFormation::Stack Wed Aug 03 2022 13:38:19 GMT+0100 (West Africa Standard Time) 
    ✔ Successfully created initial AWS cloud resources for deployments.
    ✔ Help improve Amplify CLI by sharing non sensitive configurations on failures (y/N) · yes
    ✔ Initialized provider successfully.
    ✅ Initialized your environment successfully.

    Your project has been successfully initialized and connected to the cloud!
Enter fullscreen mode Exit fullscreen mode

If you’ve gotten to this point, you’re Awesome 🙌🏾 An amplify folder has been created and the directory structure looks like this:

    .
    ├── #current-cloud-backend
    │   ├── amplify-meta.json
    │   ├── awscloudformation
    │   │   └── build
    │   │       └── root-cloudformation-stack.json
    │   └── tags.json
    ├── README.md
    ├── backend
    │   ├── amplify-meta.json
    │   ├── awscloudformation
    │   │   └── build
    │   │       └── root-cloudformation-stack.json
    │   ├── backend-config.json
    │   ├── tags.json
    │   └── types
    │       └── amplify-dependent-resources-ref.d.ts
    ├── cli.json
    ├── hooks
    │   ├── README.md
    │   ├── post-push.sh.sample
    │   └── pre-push.js.sample
    └── team-provider-info.json
Enter fullscreen mode Exit fullscreen mode

Fantastic! Let’s now go ahead to add the authentication module to our React application.

Setting up Amplify Auth

We’ve successfully initialized amplify and connected our amplify project to the cloud. Let’s now create the authentication service. Run this command on your terminal:

      amplify add auth
Enter fullscreen mode Exit fullscreen mode

You will be prompted with these options:

    You will be prompted with these options:

    Using service: Cognito, provided by: awscloudformation

    The current configured provider is Amazon Cognito. 

    Do you want to use the deUPDATE_COMPLETE amplify-reactamplifyauth-dev-133741 AWS::CloudFormation::Stack Wed Aug 03 2022 15:03:45 GMT+0100 (West Africa Standard Time) 
    ✔ All resources are updated in the cloudfault 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 auth resource reactamplifyauth963ad60d locally
Enter fullscreen mode Exit fullscreen mode

This adds a auth/reactamplifyauth963ad60d to the amplify backend directory. Now let’s go ahead and push the auth service to amplify cloud. Run this command on your terminal:

     amplify push
Enter fullscreen mode Exit fullscreen mode

You will see deploy logs and this message at the end

UPDATE_COMPLETE amplify-reactamplifyauth-dev-133741 AWS::CloudFormation::Stack Wed Aug 03 2022 15:03:45 GMT+0100 (West Africa Standard Time) 
✔ All resources are updated in the cloud
Enter fullscreen mode Exit fullscreen mode

Head over to your amplify studio with amplify console and you will see something like this:

Amplify studio

You can see Categories added: Authentication. Awesome. Let’s now integrate authentication to the frontend.

Setting up GraphQL API

We will be building a simple blog Posts API. Run this command to create an amplify api

amplify add api
Enter fullscreen mode Exit fullscreen mode

Select these options:

➜  react-amplify-auth git:(master) ✗ amplify add api
? Select from one of the below mentioned services: GraphQL
? Here is the GraphQL API that we will create. Select a setting to edit or continue Continue
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)
Enter fullscreen mode Exit fullscreen mode

Update the schema.graphql with these lines of code:

    @auth(rules: [{allow: owner}])
    type Posts @model {
      id: ID!
      title: String!
      description: String
      author: String
    }
Enter fullscreen mode Exit fullscreen mode

Next, run

     amplify push --y
Enter fullscreen mode Exit fullscreen mode

This will create

  • a AppSync GraphQL API,
  • a graphql folder in the app src folder that consists of local GraphQL operations
  • a DynamoDB table for Posts

At this point, we have successfully created the backend for the project. Let’s go ahead to create the user interface with React.

Setting up Auth in React

Firstly, delete all the files except App.js, index.js, and aws-exports.js . Install these dependencies:

     npm i aws-amplify @aws-amplify/ui-react tailwindcss postcss autoprefixer
Enter fullscreen mode Exit fullscreen mode

Update your index.js to look like this:

    //src/index.js
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    import './index.css';
    import Amplify from 'aws-amplify';
    import config from './aws-exports';
    Amplify.configure(config);

    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>
    );
Enter fullscreen mode Exit fullscreen mode

Create an index.css and add these lines of code add tailwind styles to the project.

    /* src/index.css */
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Finally, update the content of tailwind.config.js to this.

    //tailwind.config.js
    module.exports = {
      content: [
        "./src/**/*.{js,jsx,ts,tsx}",
      ],
      theme: {
        extend: {},
      },
      plugins: [],
    } 
Enter fullscreen mode Exit fullscreen mode

Let’s go ahead and create the authentication flow. Create a Signup.js and add these lines of code.

    //src/Signup.js
    import { Auth } from 'aws-amplify';
    import { useState } from 'react';
    import { useNavigate } from 'react-router-dom';

    const initialFormState = { username: "", password: "", email: "" }
    const Signup = () => {
        const [formData, setFormData] = useState(initialFormState);
        const navigate = useNavigate();
        async function signUp() {
            try {
                const { user } = await Auth.signUp({
                    username: formData.username,
                    password: formData.password,
                    attributes: {
                        email: formData.email,
                    },
                    autoSignIn: { // optional - enables auto sign in after user is confirmed
                        enabled: true,
                    }
                });
                console.log(user);
                localStorage.setItem('username', user.username);
                navigate('/confirm', { replace: true })

            } catch (error) {
                console.log('error signing up:', error);
            }
        }
        return ()
Enter fullscreen mode Exit fullscreen mode

Here, we import Auth from aws-amplify. It has all the methods we will need to implement the auth flow. Methods such as signUp, confirmSignUp, signOut, and a host of others. We went ahead to create an initialFormState object with username, password, and email as keys, they all have an empty string as initial values this will represent the initial state values for the form data we will be creating next.
Next, in the signUp function, we destructure the data retrieved from the Auth.signup() method into a user object. The Auth.signUp() method accepts a username, password, and attributes and any other optional parameters. Here, we included autoSignIn and enabled it. The autoSignIn method is a new feature that allows the user to sign in automatically without requiring Auth.signIn to login.
Finally, we save the username from the user object to localStorage. Add the following lines of code:

<div className="bg-white">
    <div className="grid max-w-screen-xl h-screen text-black m-auto place-content-center">
        <div className="w-[30rem] space-y-6">
            <div className="flex flex-col">
                <label> Username </label>
                <input
                    onChange={e => setFormData({ ...formData, 'username': e.target.value })}
                    placeholder="username"
                    value={formData.username}
                    type="text"
                    className="border border-sky-500 p-2 rounded w-full"
                />
            </div>

            <div className="flex flex-col">
                <label>Password  </label>
                <input
                    onChange={e => setFormData({ ...formData, 'password': e.target.value })}
                    placeholder="pasword"
                    value={formData.password}
                    type="password"
                    className="border p-2 rounded border-sky-500"
                />
            </div>

            <div className="flex flex-col">
                <label>Email </label>
                <input
                    onChange={e => setFormData({ ...formData, 'email': e.target.value })}
                    placeholder="email"
                    value={formData.email}
                    type="email"
                    className="border p-2 rounded border-sky-500"
                />
            </div>

            <div>
                <button className="border-none bg-sky-700 text-white p-2 mt-4 rounded m-auto" onClick={signUp}> Sign up </button>
            </div>
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Here, we have inputs. On each field, we use an onChange event and assign the value of the field to the formData object.
Let’s go ahead and create the confirm sign up screen. Create a confirmSignup.js and add these lines of code:


    //src/confirmSignup.js
    import { useState } from 'react';
    import { Auth } from 'aws-amplify';
    import { useNavigate } from 'react-router-dom';

    const initialFormState = { code: "" }
    const ConfirmSignup = () => {
        const [formData, setFormData] = useState(initialFormState);
        const navigate = useNavigate();
        const username = localStorage.getItem('username')
        const code = formData.code
        async function confirmSignUp() {
            try {
                await Auth.confirmSignUp(username, code);
                localStorage.clear();
                navigate('/posts', { replace: true })
            } catch (error) {
                console.log('error confirming sign up', error);
            }
        }
        return (
            <div className="grid max-w-screen-xl h-screen text-black m-auto place-content-center">
                <div className="w-[30rem] space-y-6">
                    <label htmlFor='Confirmation Code'> Enter the confirmation code sent to your email </label>
                    <input
                        onChange={e => setFormData({ ...formData, 'code': e.target.value })}
                        placeholder="code"
                        value={formData.code}
                        type="text"
                        className="border border-sky-500 p-2 rounded w-full shadow"
                    />
                </div>
                <button className="border-2 bg-sky-700 border-none text-white p-2 mt-4 rounded m-auto" onClick={confirmSignUp}> Confirm</button>
            </div>
        )
    }

    export default ConfirmSignup
Enter fullscreen mode Exit fullscreen mode

Here, the Auth.confirmSigUp() accepts a username and code. We retrieve the username from localStorage and the code from the value of the input.

Go ahead and create a Home.js and add these lines of code:


    //src/Home.js
    import { useNavigate } from "react-router-dom"

    const Home = () => {
        const navigate = useNavigate()
        const redirectToSignup = () => {
            navigate('/signup')
        }
        return (
            <div className="bg-black">

                <div className="grid max-w-screen-xl h-screen text-white m-auto place-content-center">
                    <h2 className="mb-8 text-4xl"> Amplify Posts app</h2>
                    <button className="border-2 p-2" onClick={redirectToSignup}> Create Account</button>
                </div>
            </div>
        )
    }

    export default Home
Enter fullscreen mode Exit fullscreen mode

In this page, we just have a button that’ll redirect us to the signup screen.
Update the App.js with these lines of code:


    //src/App.js
    import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
    import Signup from './Signup'
    import Posts from './Post'
    import Confirm from './confirmSignup'
    import Home from "./Home"

    const App = () => {
      return (
        <div>
          <Router>
            <Routes>
              <Route path="/" element={<Home />}> Home</Route>
              <Route path="/signup" element={<Signup />}> Signup </Route>
              <Route path="/confirm" element={<Confirm />}></Route>
              <Route path="/posts" element={<Posts />}> </Route>
            </Routes>
          </Router>
        </div>
      )
    }

    export default App
Enter fullscreen mode Exit fullscreen mode

Here, we define the routes for our application. So far, we’ve created Home, Signup, and Confirm routes, we should go ahead to create the Posts screen.
Create a src/Posts.js and add these lines of code:

    //src/Posts.js
    import "@aws-amplify/ui-react/styles.css";
    import { Auth, API } from "aws-amplify";
    import { useState, useEffect } from 'react'
    import { listPosts } from "./graphql/queries";
    import { createPosts as createPostsMutation } from "./graphql/mutations";
    import { useNavigate } from "react-router-dom";

    const initialFormState = { title: "", author: "", description: "" }
    function Posts() {
        const [user, setUser] = useState()
        const [posts, setPosts] = useState([]);
        const [formData, setFormData] = useState(initialFormState);
        const navigate = useNavigate()

        async function fetchUser() {
            Auth.currentAuthenticatedUser({
                bypassCache: true  // Optional, By default is false. If set to true, this call will send a request to Cognito to get the latest user data
            })
                .then(user => {
                    console.log(user)
                    setUser(user)
                })
                .catch(err => console.log(err));
        }

        async function fetchPosts() {
            const apiData = await API.graphql({ query: listPosts });
            setPosts(apiData.data.listPosts.items);
        }

        async function createPost() {
            if (!formData.title || !formData.description || !formData.author) return;
            await API.graphql({ query: createPostsMutation, variables: { input: formData } });
            setPosts([...posts, formData]);
            setFormData(initialFormState);
        }

        async function signOut() {
            try {
                await Auth.signOut({ global: true });
                navigate('/signup', { replace: true })
            } catch (error) {
                console.log('error signing out: ', error);
            }
        }
        useEffect(() => {
            fetchUser()
            fetchPosts()
        }, [])
        return ()
Enter fullscreen mode Exit fullscreen mode

Let’s go through this code snippet. The posts array will hold the posts we are fetching from the graphql API. While formData will hold the values of the inputs. We also have these functions:

  • fetchPosts - This function uses the API class to send a query to the GraphQL API and retrieve a list of posts.
  • createPost - This function also uses the API class to send a mutation to the GraphQL API. Here, we pass in the variables needed for a GraphQL mutation to create a new post with the form data. Let’s go ahead and build the UI.
  • fetchUser() - This uses the Auth.currentAuthenticatedUser() method to check if the user has been authenticated. If yes, it returns the user object.
  • signOut- This function uses the Auth.signOut method to sign out an authenticated user.

Let’s go ahead to implement the ui. Add these lines of jsx


    <div className=" m-auto p-12">
        <div className=" space-y-6  w-[30rem] m-auto">
            <div className="flex flex-col">
                <input
                    onChange={e => setFormData({ ...formData, 'title': e.target.value })}
                    placeholder="Post title"
                    value={formData.title}
                    type="text"
                    className="border border-sky-500 p-2 rounded w-full"
                />
            </div>

            <div className="flex flex-col">
                <input
                    onChange={e => setFormData({ ...formData, 'author': e.target.value })}
                    placeholder="Post author"
                    value={formData.author}
                    type="text"
                    className="border border-sky-500 p-2 rounded w-full"
                />
            </div>

            <div className="flex flex-col">
                <textarea
                    onChange={e => setFormData({ ...formData, 'description': e.target.value })}
                    placeholder="Post description"
                    value={formData.description}
                    className="border border-sky-500 p-2 rounded w-full"
                />
            </div>

            <button className="border-none bg-sky-700 text-white p-2 mt-4 rounded m-auto" onClick={createPost}>Create Post</button>
        </div>
    </div>
Enter fullscreen mode Exit fullscreen mode

Finally, in this code snippet, we have a button that invokes the createPost() function. Let’s move on and create the view for the posts.

    <div className="mt-20">

    <h1 className="text-3xl font-semibold "> Welcome {user && (<span>{user.username}</span>)} </h1>
            <h2>Here are your posts:</h2>
            {posts === 0 ?
                (

                    <div>
                        <h3> They are no posts</h3>
                    </div>
                ) :

                posts?.map(post => (
                    <div key={post.id || post.title} className="my-10">
                        <h2 className="text-xl font-semibold">Title: {post.title}</h2>
                        <p className="font-bold text-lg"> Author: {post.author}</p>
                        <p>Message: {post.description}</p>
                    </div>
                ))
            }
        </div>

        <button className="border-2 bg-red-500 border-none text-white p-2 mt-8 rounded m-auto" onClick={signOut}> Signout</button>
    </div>
Enter fullscreen mode Exit fullscreen mode

Earlier, we created a posts array that contains the posts fetched from the GraphQL API. Here, we map through the posts and render the title, description, and author information. We also have a signOut button, we use the signOut function from Authenticator here.
If you’ve noticed when you Create an account and pass the verification process, you’re automatically logged in to the app. This feature was added recently, and we can get to use it now! Behind the scenes, the confirm sign-up listener initiates auto sign-in once the account is confirmed. It happens if autoSignIn is set to true in params for SignUp function. They are three different ways you can choose to auto sign in which are code, link, or autoConfirm. code is the default.
Code implementation uses a hub listening event. Once the user confirms the account with code, it dispatches a listening event. The SignUp function is listening for this event to initiate SignIn process.
The Link implementation uses polling to sign users in since there is no explicit way to know if the user clicked the link. Polling is done within 3 minutes (time verification link is valid) every 5 seconds.
So that’s it, you should have something like this:

amplify post app home view

sign up view

confirm code view

posts view

Conclusion

Amplify makes authentication easy while using Amazon Cognito as an Authentication provider. In this post, we learned how to configure and implement authentication with the Amplify framework.

Top comments (2)

Collapse
 
nicolasdegouveia profile image
NicolasDeGouveia

Hi thank you for this post. I have a question about the link implementation. There is something else to change in your tutorial to use link verification or we just need to check link verification box on cognito ?
Thank you again for this post

Collapse
 
mrroyce profile image
Royce Harding

Hey there, the code on GitHub doesnt match the blog. e.g. there is not Signup.js or index.css files in GH. Will you update GH?