How to add Amazon Cognito Auth to a Web App (part 3)

How to add Amazon Cognito Auth to a Web App (part 3)

Understanding the Next.JS code samples that creates the User Interface for an Amazon Cognito Authenticated Web App.

Intro

In my last post, I explained how Terraform was being used to create the cloud infrastructure that enables Amazon Cognito to be implemented in the below demo 👇

In this post, I will explain how to integrate Amazon Cognito into the Front End of a Web App, so you can see it as visualised above.

Implementing Cognito Auth on the Front End

I have a Wrapper.js template that you can use to spin up an implementation of Cognito, here are some highlight files from that template:

In this example, I’ll show you a Next.js React App, that uses the Amazon Amplify library to authenticate the Front End with Amazon Cognito.

I recommend reading the first post in this series before continuing, so you understand the concept of what is being implemented!

components/Cognito/utils.js

Let’s start by showing how the variables that were exported from Terraform integrate into the Front End.

import Amplify, { Auth } from 'aws-amplify';

export function setupAmplify() {
    Amplify.configure({
        Auth: {
            // REQUIRED only for Federated Authentication - Amazon Cognito Identity Pool ID
            identityPoolId: process.env.cognito_identity_pool_id,
            // REQUIRED - Amazon Cognito Region
            region: process.env.region,
            // OPTIONAL - Amazon Cognito User Pool ID
            userPoolId: process.env.cognito_user_pool_id,
            userPoolWebClientId: process.env.cognito_user_pool_client_id
        }
    });
}

You can see the following variables being set up in the AWS Amplify library:

  • identityPoolId: the ID for the identity pool that is used to authorise access to other AWS services

  • region: the region for the Amazon Cognito resource

  • userPoolId: the ID for the user pool where that allows users to create credentials that can be authenticated

  • userPoolWebClientId: the ID that is assigned from Cognito to our app, to allow it to access the User Pool

pages/_app.js

At the core of the application is _app.js, which executes logic each time a page is loaded.

import { createGlobalStyle } from 'styled-components'
import Cognito from '../components/Cognito'
import {  Authenticator } from '@aws-amplify/ui-react';

const GlobalStyle = createGlobalStyle`
    html {
        overflow: hidden;
    }
    *, *:before, *:after {
        box-sizing: inherit;
    }
    body{
        margin: 0;
    }
    :root {
        --amplify-primary-color:lightblue;
        --amplify-primary-tint: #0A3369;
        --amplify-primary-shade:#0A3369;
        --amplify-secondary-color:#0A3369;
        --amplify-secondary-tint:#D00C1B;
        --amplify-secondary-shade:#1F2A37;
        --amplify-tertiary-color:#5d8aff;
        --amplify-tertiary-tint:#7da1ff;
        --amplify-tertiary-shade:#537BE5;
        --amplify-grey:#828282;
        --amplify-light-grey:#c4c4c4;
        --amplify-white:#ffffff;
        --amplify-red:#dd3f5b;
        --amplify-primary-contrast: var(--amplify-white);
        --amplify-secondary-contrast:var(--amplify-white);
        --amplify-tertiary-contrast:var(--amplify-red);
        --amplify-font-family:'Helvetica Neue Light', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif';
        --amplify-text-xxs:0.75rem;
        --amplify-text-xs:0.81rem;
        --amplify-text-sm:0.875rem;
        --amplify-text-md:1rem;
        --amplify-text-lg:1.5rem;
        --amplify-text-xl:2rem;
        --amplify-text-xxl:2.5rem;
      }

    amplify-authenticator {
        justify-content:center;
        align-items: center;
        display: inline-block;
        height: auto;
        --width:400px;
        border: 0px solid;
        color: #0A3369;
        font-size:var(--amplify-text-md);
        --box-shadow:none;
        --container-height:400px;
        --padding:20px;
      }
`

export default function App({ Component, pageProps }) {
    return (
        <Authenticator.Provider>
            <Cognito>
                <GlobalStyle />
                <Component {...pageProps} />
            </Cognito>
        </Authenticator.Provider>
    )
  }

In this case, our app checks if a user is authenticated with Cognito (the App function) and renders content based on that users level of authorisation.

components/Cognito/index.js

This component contains the core Amazon Cognito logic.

import { Auth } from 'aws-amplify';
import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import React, { useEffect } from 'react';
import cognitoStore from './../../stores/cognito';
import userStore from './../../stores/user';
import styled from 'styled-components';
import { setupAmplify } from './utils.js';
import { httpApiURL } from '../../utils';
import axios from 'axios';

setupAmplify();

const getUserData = (cognito) => axios({
    method: 'post',
    url: `${httpApiURL}/users/data`,
    data: {
      cognito: cognito
    },
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${cognito.jwt}`
    }
}).then((res) => {
    const data = JSON.parse(res.data.body);
    return data;
}, (error) => {
    throw new Error(error);
})

const Cognito = (props) => {
    const { children } = props;
    const { cognito, setCognito, signInState, setSignInState } = cognitoStore();
    const { setUser } = userStore();
    const { user } = useAuthenticator((context) => [context.user]);

    useEffect(() => {
        const fetchData = async() => {
            // Update the document title using the browser API
            if(cognito != '' && cognito != undefined) {
                try{
                    setUser(await getUserData(cognito));
                } catch (e) {
                    console.log(e);
                }
            }

            if(user != undefined && cognito == '' && user.signInUserSession) {
                const {accessToken, idToken} = user.signInUserSession;
                const role = accessToken.payload['cognito:groups'];
                const token = {
                    jwt: idToken.jwtToken,
                    role: (role) ? role[0] : '',
                    username: accessToken.payload.username
                }
                setCognito(token);
            }
        }

        try {
            fetchData();
        }
        catch (e) {
            console.error(e);
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [user])

    useEffect(() => {
        const checkState = async() => {
            if(signInState == 'signOut') {
                await Auth.signOut();
                setCognito('');
                setSignInState('signedOut');
            }
        }
        checkState();
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [signInState]);
    return (
        <>
            {cognito == '' ?
                <AmplifyWrapper>
                    <Authenticator
                        headerText="Enter your details below"
                        submitButtonText="Sign in"
                        usernameAlias="email"
                        hideSignUp
                    >
                    </Authenticator>
                </AmplifyWrapper>

            :
                <ContentWrapper>
                    {children}
                </ContentWrapper>
            }
        </>
    )
}

const AmplifyWrapper = styled('div')`
    position: absolute;
    top: 50%;
    left: 50%;
    -ms-transform: translate(-50%, -50%);
    transform: translate(-50%, -50%);
`
const ContentWrapper = styled('div')`
    position: relative;
    z-index: 1;
    width: 100vw;
    height: 100vh;
`

export default Cognito

Diving into the logic of how this Cognito component works:

  • The first useEffect life cycle method demonstrates the logic that attempts to make a request to authenticate with Cognito and get the JWT as a response.

  • The returned elements demonstrates the logic that renders a log in page if the user is not authenticated and renders the children content if the user is authenticated

The idea being, if a user is not authenticated, they enter their details into the log in form, this will trigger the an attempt to authenticate with Cognito and then to show content upon success.

pages/index.js

Finally, we have a component that renders the web page (assuming that the user has passed Cognito Auth).

import Head from 'next/head'
import React, { useEffect } from 'react'
import styled from 'styled-components'
import axios from 'axios'

import { httpApiURL } from './../utils'
import Header from '../components/Header'
import cognitoStore from '../stores/cognito'

export default function Home() {
  const { cognito } = cognitoStore();

  useEffect(() => {
    const getData = async(cognito) => await axios({
      method: 'get',
      url: `${httpApiURL}/users/data`,
      data: {
        cognito: cognito
      },
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${cognito.jwt}`
      }
    }).then((res) => {
      console.log(res.data.body)
    }, (error) => {
      console.log(error);
    })

    getData(cognito);

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <Head>
        <title>Wrapper.js Authentication Example</title>
      </Head>
      <Header />

      <Img src='/wrapperjs.png' />
      <P>Authentication template</P>
    </>
  )
}

const Img = styled('img')`
  display: block;
  height: 60vh;
  width: auto;
  margin-left:auto;
  margin-right:auto;
  position: relative;
  // min-width: 500px;
`

const P = styled('p')`
  text-align: center;
  font-weight: bold;
`

Talking through this example of how the content that could be shown on the Front End works:

  • The useEffect method shows data being loaded from the Back End based on an API request that uses JWT authentication.

  • The returned elements show what the Front End should render if authentication has been successful.

Conclusion

I hope the above is helpful in showing you how to implement Amazon Cognito on the Front End using the Amplify library, to only show content to users that have gone through the Auth process!

In the next (and last) post in this series, I’ll talk through how to implement Amazon Cognito on the Back End to secure API data.

In the meantime, have fun 😀

Did you find this article valuable?

Support James Miller by becoming a sponsor. Any amount is appreciated!