# Docusaurus Authentication using AWS Cognito

# Background

[Docusaurus](https://docusaurus.io/) is an excellent documentation generator tool that doesn’t support authentication out of the box. If you want to read about the roadmap of this issue and get to know more about what we want to achieve, please refer to my main article [here](https://iammassoud.net/blog/docusaurus-authentication-roadmap).

# Goal

The main goal of this article is to provide a step-by-step guide on how to add a simple authentication layer to Docusaurus using AWS Cognito.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683992557162/57bccf77-fbd8-4d2c-bf41-d908a231c734.png align="center")

Please find the more advanced authentication flows using Docusaurus and AWS Cognito [here](https://iammassoud.net/blog/docusaurus-authentication-roadmap).

# Getting Started

Before starting the tutorial, I assume you have a basic understanding of React and how Docusaurus works.

## Prerequisite

* An AWS account
    
* A running Docusaurus website using the steps provided in [the main website](https://docusaurus.io/docs/installation)
    

## Installing packages

After initializing your Docusaurus project, it is time to install the necessary packages:

```bash
npm i @aws-amplify/ui-react@^4.6.0 add@^2.0.6 aws-amplify@^5.1.4 aws-crt@^1.15.15 docusaurus-plugin-dotenv@^1.0.1
```

The command above will install a few packages respectively:

* `@aws-amplify/ui-react`: The Amplify UI library used in the login page
    
* `add`: A helper package is needed for most of the next packages
    
* `AWS-amplify`: The main AWS Amplify library for JavaScript
    
* `aws-crt`: NodeJS/Browser bindings for the AWS Common Runtime
    
* `docusaurus-plugin-dotenv`: A Docusaurus plugin that allows you to access your environment variables
    

## Update `docusaurus.config.js`

Go to your `docusaurus.config.js` file and update the plugins list:

```javascript
// docusaurus.config.js

plugins: [
    [
      "docusaurus-plugin-dotenv",
      {
        path: "./.env.local",
        systemvars: true,
      },
    ],
  ],
```

> ❗️Be careful about the below points regarding the above configuration:
> 
> * `path` will look for the `.env.local` file which is needed for test purposes on your local machine.
>     
> * `systemvars` should be set to `true` if you would rather load all system variables as well (useful for CI purposes). If it is not the case for you, please set it as `false`.
>     

## AWS Cognito Setup

In this section, we will see how to create an AWS Cognito User Pool to satisfy the requirements.

1. Go to the [Amazon Cognito console](https://console.aws.amazon.com/cognito/home). If prompted, enter your AWS credentials.
    
2. Choose User Pools.
    
3. Choose Create a user pool to start the user pool creation wizard.
    
4. In **Step 1 (Configure sign-in experience)**, in the section **Cognito user pool sign-in options**, choose **Email** and press **Next**.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683982484563/0904e785-75f7-4dba-842d-56fc18a4ebb6.png align="center")
    
5. In **Step 2 (Configure security requirements)**, in the section **Multi-factor authentication**, choose **No MFA** (you can consider other options based on your flow) and leave the rest as they are, and press **Next**.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683982782676/aba3c425-86d3-422f-9e69-73302e318e4e.png align="center")
    
6. In **Step 3 (Configure sign-up experience)**, you may check **Enable Self-registration** if your flow allows the users to self-register. Leave the rest as they are, and press **Next**.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683982843712/f9506072-244a-4d10-ad67-61e794fb20ec.png align="center")
    
7. In **Step 4 (Configure message delivery)**, choose to **Send email with Cognito** to let Cognito takes care of the emails. Though, you may choose to **Send email with Amazon SES** based on the requirements and press **Next**.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683983101384/3aeec5b3-c4f0-4b1b-8765-f591c38e8923.png align="center")
    
8. In **Step 5 (Integrate your app)**, in the section **User pool name**, write a name for the user pool.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683983252091/37080405-cdd6-4efb-863c-1282038a60c9.png align="center")
    
9. In **Step 5 (Integrate your app)**, in the section **Hosted authentication pages**, opt for **Use the Cognito Hosted UI**.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683983544327/1b520750-5a8c-4e00-ab30-0f5fd42d773b.png align="center")
    
10. In **Step 5 (Integrate your app)**, fill up the **Cognito domain** based on the requirements. The only point is to note down the **Cognito domain** since we will need it later to connect the Docusaurus website to AWS Cognito.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683984661544/5a9c8db2-5e1c-4d67-9a61-f0bfd97da460.png align="center")
    
11. In **Step 5 (Integrate your app)**, in the section **Initial app client**, fill up the name in the **App client name** field.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683983599801/c25e1366-20ba-4206-a6d8-ee1bb04659dd.png align="center")
    
12. In **Step 5 (Integrate your app)**, in the section **Client secret**, choose **Don't generate a client secret**. Then fill up the **Allowed callback URLs** based on the requirements.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683984718662/5139453e-b22a-450a-85dc-721d7e838d75.png align="center")
    
13. In **Step 5 (Integrate your app)**, in the section **Advanced app client settings**, add the **Allowed sign-out URLs - optional** and press **Next**.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683984769658/c0727968-adcd-4cef-ad9a-81b3914344b6.png align="center")
    
14. In **Step 5 (Review and create),** review your choices and modify any selections you wish to. When you are satisfied with your user pool configuration, select **Create user pool** to proceed.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1683992584595/4e49c1dd-4c6e-4375-8dc9-f3d97ea8641b.png align="center")
    

> ❓In the above steps:
> 
> * `localhost:3000` is my local development setup.
>     
> * `https://docusaurus-auth-cognito.vercel.app` is the URL that I published my docusaurus example on. **YOU NEED TO WRITE YOURS**.
>     

## Docusaurus Setup

This section contains all the changes and new files needed to be added to the Docusaurus website. I suggest going through the previous steps to set up your AWS Cognito before starting this section.

### Final Directory Hierarchy

Here is the final directory tree which **mostly** shows the changes **added/modified** to the Docusaurus project. Use it as a reference.

```plaintext
├── docusaurus.config.js
└── src
   ├── components
   |  ├── Auth
   |  |  └── index.js
   |  └── Login
   |     ├── index.js
   |     └── styles.css
   ├── config
   |  ├── amplify-config.js
   |  └── aws-exports.js
   ├── pages
   |  └── login
   |     └── index.js
   ├── theme
   |  ├── Navbar
   |  |  ├── Content
   |  |  |  ├── index.js
   |  |  |  └── styles.module.css
   |  |  └── MobileSidebar
   |  |     └── PrimaryMenu
   |  |        └── index.js
   |  └── Root.js
   └── utils
      ├── constants.js
      └── utils.js
```

### Docusaurus Swizzling

Docusaurus has an advanced feature called [**Swizzling**](https://docusaurus.io/docs/swizzling) which allows the developer to go deeper into the Docusaurus lifecycle and provides more flexibility to the developer.

To make our website be able to utilize sign-up/sign-in flow, we need to swizzle three components:

1. **ROOT**: The `<Root>` component is rendered at the very top of the React tree, above the theme `<Layout>`, and never unmounts. It is the perfect place to add stateful logic that should not be re-initialized across navigation (user authentication status, shopping cart state...).
    
2. **Navbar**: To show the **Login/Logout** button in the Navbar on top of the website.
    
    * **Desktop**: To show the button in the **Desktop** view.
        
    * **Mobile**: To show the button in the **Mobile** view.
        

To swizzle the above components, you may use [the instruction from Docusaurus](https://docusaurus.io/docs/swizzling) or follow my guide to add the required files.

Check [**here**](https://massoud.hashnode.dev/docusaurus-authentication-using-aws-cognito#heading-final-directory-hierarchy) for the final directory hierarchy.

### Adding Required Files

This section will provide the changes or additional files required for this project.

> 🔔 On **the first line** of each code block, you can find the **relative path** of the file.

* **Environment variables:** Contains all the variables used in the application. I've provided some explanations for each variable.
    
    ```makefile
    # .env.local
    
    # i.e: localhost, dev, prod
    ENV="localhost"
    
    # AWS Cognito Region
    REGION="ap-southeast-1"
    
    # AWS Cognito User Pool ID
    USER_POOL_ID="ap-southeast-1_f5XdaserR"
    
    # AWS Cognito User Pool App Client ID
    USER_POOL_WEB_CLIENT_ID="7858pxvalkeqenbv3sac"
    
    # AWS Cognito Domain
    OAUTH_DOMAIN="docusaurus-auth-cog.auth.ap-southeast-1.amazoncognito.com"
    
    # Amplify redirect url after a successful sign-in
    OAUTH_REDIRECT_SIGN_IN="http://localhost:3000/login,https://docusaurus-auth-cognito.vercel.app/login"
    
    # Amplify redirect url after a successful sign-out
    OAUTH_REDIRECT_SIGN_OUT="http://localhost:3000,https://docusaurus-auth-cognito.vercel.app"
    
    # Amplify setup, no need to change it!
    OAUTH_REDIRECT_SIGN_RESPONSE_TYPE="code"
    ```
    
* **aws-exports.js**: Contains the definitions needed to set up AWS Amplify.
    
    ```javascript
    // src/config/aws-exports.js
    
    const awsConfig = {
        region: process.env.REGION,
        userPoolId: process.env.USER_POOL_ID,
        userPoolWebClientId: process.env.USER_POOL_WEB_CLIENT_ID,
        oauth: {
            domain: process.env.OAUTH_DOMAIN,
            redirectSignIn: process.env.OAUTH_REDIRECT_SIGN_IN,
            redirectSignOut: process.env.OAUTH_REDIRECT_SIGN_OUT,
            responseType: process.env.OAUTH_REDIRECT_SIGN_RESPONSE_TYPE
        }
    };
    
    export default awsConfig;
    ```
    
* **amplify-config.js**: Sets up the AWS Amplify based on the configurations passed to it and it will decide to choose some settings based on the environment whether it is [`localhost`](http://localhost) or any other URL.
    
    ```javascript
    // src/config/amplify-config.js
    
    import awsConfig from "./aws-exports";
    
    export function configure() {
        // Assuming you have two redirect URIs, and the first is for localhost and second is for production
        const [
            localRedirectSignIn,
            productionRedirectSignIn,
        ] = awsConfig.oauth.redirectSignIn.split(",");
    
        const [
            localRedirectSignOut,
            productionRedirectSignOut,
        ] = awsConfig.oauth.redirectSignOut.split(",");
    
        const updatedAwsConfig = {
            ...awsConfig,
            oauth: {
                ...awsConfig.oauth,
                redirectSignIn: process.env.ENV === 'localhost' ? localRedirectSignIn : productionRedirectSignIn,
                redirectSignOut: process.env.ENV === 'localhost' ? localRedirectSignOut : productionRedirectSignOut,
            }
        };
    
        return updatedAwsConfig;
    }
    ```
    
* **Root.js**: Here we wrap the main `<Root>` component using `<Authenticator.Provider>`.
    
    ```javascript
    // src/theme/Root.js
    
    import React from 'react';
    
    import { Amplify } from 'aws-amplify';
    import { Authenticator } from '@aws-amplify/ui-react';
    
    import { configure } from "../config/amplify-config";
    import { AuthCheck } from "../components/Auth";
    
    const awsConfig = configure();
    Amplify.configure(awsConfig);
    
    export default function Root({ children }) {
      return (
        <Authenticator.Provider>
          <AuthCheck children={children} />
        </Authenticator.Provider>
      );
    }
    ```
    
* **Auth**: In this file, we check if the route requested is private or not.
    
    1. If the route is protected and the user is `authenticated`, it will redirect to the requested page.
        
    2. If the route is protected and the user is not `authenticated`, it will redirect to the login page.
        
    3. If the route is `login` or `logout`, it will redirect to the base URL.
        
    4. Otherwise, means the path is not protected and will redirect to the requested page.
        
        ```javascript
        // src/components/Auth/index.js
        
        import React from "react";
        
        import { useAuthenticator } from "@aws-amplify/ui-react";
        import { Redirect, useLocation } from "@docusaurus/router";
        
        import { AUTHENTICATED, BASE, LOGIN_PATH, LOGOUT_PATH, PROTECTED_PATHS } from "../../utils/constants";
        import { Login } from "../Login";
        
        export function AuthCheck({ children }) {
        
            const location = useLocation();
            let from = location.pathname;
        
            const { route, signOut } = useAuthenticator((context) => [context.route, context.signOut]);
        
            // If it is not authenticated and tries to access the protected paths. Also, a
            // custom error appears if anything happens.
            if (route === AUTHENTICATED) {
                if (from === LOGOUT_PATH) {
                    signOut();
                    return <Redirect to={BASE} from={LOGOUT_PATH} />;
                } else if (from === LOGIN_PATH) return <Redirect to={BASE} from={from} />;
        
                return children;
            } else {
                if (from === LOGOUT_PATH) return <Redirect to={BASE} from={from} />;
                else if (PROTECTED_PATHS.filter(x => from.includes(x)).length) return <Login />;
                else if (from === LOGIN_PATH) return <Login />;
                return children;
            }
        }
        ```
        
* **Login**: This page will be shown when the user needs to log in or be redirected to `/login` page.
    
    Additionally, I wrote a CSS file tied to this page which mostly fixes the bugs for the AWS Amplify library used above (hopefully it will be fixed in future releases).
    
    ```javascript
    // src/pages/login/index.js
    
    import React from "react";
    
    import { useHistory, useLocation } from "@docusaurus/router";
    
    import { Login } from "../../components/Login";
    
    import { BASE, LOGIN_PATH } from "../../utils/constants";
    
    export default function loginPage() {
      const location = useLocation();
      const history = useHistory();
    
      let from = location.pathname;
      let to =
        history.location.pathname === LOGIN_PATH
          ? BASE
          : history.location.pathname;
    
      return <Login from={from} to={to} />;
    }
    ```
    
    ```javascript
    // src/components/Login/index.js
    
    import React from "react";
    
    import { Authenticator, View } from "@aws-amplify/ui-react";
    import "@aws-amplify/ui-react/styles.css";
    
    import { Redirect } from "@docusaurus/router";
    
    import "./styles.css";
    
    export function Login({ from, to }) {
      return (
        <View className="auth-wrapper">
          <Authenticator>{<Redirect to={to} from={from} />}</Authenticator>
        </View>
      );
    }
    ```
    
    ```css
    /* src/components/Login/styles.css */
    
    .auth-wrapper {
      margin: auto 10px;
    }
    
    /* it is necessary because of the bug in the main css library of the AWS Amplify */
    .amplify-field-group__outer-end .amplify-field-group__control,
    .amplify-field-group__outer-start .amplify-field-group__control {
      height: 100%;
      height: -webkit-fill-available;
    }
    
    /* it is necessary because of the bug in the main css library of the AWS Amplify */
    .amplify-button--fullwidth {
      width: inherit;
    }
    ```
    
* **Navbar Desktop**: Nothing changes on this component except we want to add `Login/Logout` to the Navbar.
    
    The reason we don't add the button using `docusaurus.config.js` is that the text should switch between `Login` and `Logout` based on the fact whether the user is `authenticated` or not.
    
    So, we just override `useNavbarItems()` function and use the modified function placed `src/utils/utils.js`.  
    Additionally, there is a CSS file tied to this page which is the original one captured from the swizzling component.
    
    The same change happens in `Navbar Mobile` as well.
    
    ```javascript
    // src/theme/Navbar/Content/index.js
    
    import React from 'react';
    import { useThemeConfig, ErrorCauseBoundary } from '@docusaurus/theme-common';
    import {
      splitNavbarItems,
      useNavbarMobileSidebar,
    } from '@docusaurus/theme-common/internal';
    import NavbarItem from '@theme/NavbarItem';
    import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle';
    import SearchBar from '@theme/SearchBar';
    import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle';
    import NavbarLogo from '@theme/Navbar/Logo';
    import NavbarSearch from '@theme/Navbar/Search';
    import styles from './styles.module.css';
    import { useNavbarItems } from '../../../utils/utils';
    // function useNavbarItems() {
    //   // TODO temporary casting until ThemeConfig type is improved
    // return useThemeConfig().navbar.items;
    // }
    function NavbarItems({ items }) {
      return (
        <>
          {items.map((item, i) => (
            <ErrorCauseBoundary
              key={i}
              onError={(error) =>
                new Error(
                  `A theme navbar item failed to render.
    Please double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config:
    ${JSON.stringify(item, null, 2)}`,
                  { cause: error },
                )
              }>
              <NavbarItem {...item} />
            </ErrorCauseBoundary>
          ))}
        </>
      );
    }
    function NavbarContentLayout({ left, right }) {
      return (
        <div className="navbar__inner">
          <div className="navbar__items">{left}</div>
          <div className="navbar__items navbar__items--right">{right}</div>
        </div>
      );
    }
    export default function NavbarContent() {
      const mobileSidebar = useNavbarMobileSidebar();
      const items = useNavbarItems();
      const [leftItems, rightItems] = splitNavbarItems(items);
      const searchBarItem = items.find((item) => item.type === 'search');
      return (
        <NavbarContentLayout
          left={
            // TODO stop hardcoding items?
            <>
              {!mobileSidebar.disabled && <NavbarMobileSidebarToggle />}
              <NavbarLogo />
              <NavbarItems items={leftItems} />
            </>
          }
          right={
            // TODO stop hardcoding items?
            // Ask the user to add the respective navbar items => more flexible
            <>
              <NavbarItems items={rightItems} />
              <NavbarColorModeToggle className={styles.colorModeToggle} />
              {!searchBarItem && (
                <NavbarSearch>
                  <SearchBar />
                </NavbarSearch>
              )}
            </>
          }
        />
      );
    }
    ```
    
    ```css
    /* src/theme/Navbar/Content/styles.module.css */
    
    /*
    Hide color mode toggle in small viewports
     */
    @media (max-width: 996px) {
      .colorModeToggle {
        display: none;
      }
    }
    ```
    
* **Navbar Mobile**: Nothing changes on this component except we want to add `Login/Logout` to the Navbar.
    
    The reason we don't add the button using `docusaurus.config.js` is that the text should switch between `Login` and `Logout` based on the fact whether the user is `authenticated` or not.
    
    So, we just override `useNavbarItems()` function and use the modified function placed `src/utils/utils.js`.
    
    The same change happens in `Navbar Desktop` as well.
    
    ```javascript
    // src/theme/Navbar/MobileSidebar/PrimaryMenu/index.js
    
    import React from 'react';
    import { useThemeConfig } from '@docusaurus/theme-common';
    import { useNavbarMobileSidebar } from '@docusaurus/theme-common/internal';
    import NavbarItem from '@theme/NavbarItem';
    import { useNavbarItems } from '../../../../utils/utils';
    // function useNavbarItems() {
    //   // TODO temporary casting until ThemeConfig type is improved
    //   return useThemeConfig().navbar.items;
    // }
    // The primary menu displays the navbar items
    export default function NavbarMobilePrimaryMenu() {
      const mobileSidebar = useNavbarMobileSidebar();
      // TODO how can the order be defined for mobile?
      // Should we allow providing a different list of items?
      const items = useNavbarItems();
      return (
        <ul className="menu__list">
          {items.map((item, i) => (
            <NavbarItem
              mobile
              {...item}
              onClick={() => mobileSidebar.toggle()}
              key={i}
            />
          ))}
        </ul>
      );
    }
    ```
    
* **constants.js**: This file contains the most important constant values giving easier management of the project.
    
    ```javascript
    // src/utils/constants.js
    
    export const LOGIN_PATH = '/login';
    export const LOGOUT_PATH = '/logout';
    export const AUTHENTICATED = 'authenticated';
    export const BASE = '/';
    
    export const LOGOUT_BUTTON = "Logout";
    export const LOGIN_BUTTON = "Login";
    
    // Add the protected paths here
    export const PROTECTED_PATHS = [BASE];
    ```
    
* **utils.js**: Contains a function to add `Login/Logout` buttons to the Navbar.
    
    ```javascript
    // src/utils/utils.js
    
    import { useThemeConfig } from '@docusaurus/theme-common';
    
    import { useAuthenticator } from '@aws-amplify/ui-react';
    import { AUTHENTICATED, LOGIN_BUTTON, LOGIN_PATH, LOGOUT_BUTTON, LOGOUT_PATH } from "./constants";
    
    export function useNavbarItems() {
        const { route } = useAuthenticator((context) => [context.route]);
    
        let label, to;
        if (route === AUTHENTICATED) {
            label = LOGOUT_BUTTON;
            to = LOGOUT_PATH;
        } else {
            label = LOGIN_BUTTON;
            to = LOGIN_PATH;
        }
    
        // TODO temporary casting until ThemeConfig type is improved
        // return useThemeConfig().navbar.items;
        let items = useThemeConfig().navbar.items;
        items.push({
            label: label,
            position: "right",
            to: to
        });
    
        // remove irrelevant items
        if (route === AUTHENTICATED) items = items.filter(x => x.label !== LOGIN_BUTTON);
        else items = items.filter(x => x.label !== LOGOUT_BUTTON);
    
        const uniqueItems = [...new Map(items.map(x => [x.label, x])).values()];
    
        return uniqueItems;
    }
    ```
    

# Source Code

You may find the full source code of this project [here](https://github.com/massoudmaboudi/docusaurus-auth-cognito).

> 🔔 The code repository shared above consists of a few branches that all are related to the Docusaurus Authentication using AWS Cognito. Though, you can find the changes for this article in the branch called `main`.

---

> ❗️DO NOT upload any `.env` to your public repository.
> 
> To run the project on your machine, you only need to fill up the `.env.local` file as described WITH YOUR VALUES.
> 
> To use your `.env` values in production, you need to add them to your hosting.
> 
> Below I listed a few hosting options and how you can upload your `.env` variables as well.
> 
> * [Netlify](https://docs.netlify.com/environment-variables/get-started/#work-with-env-files)
>     
> * [Vercel](https://vercel.com/docs/concepts/projects/environment-variables)
>     
> * [Render](https://render.com/docs/configure-environment-variables#configuring-secrets-and-other-environment-information-on-render)
>     

# Authentication for Specific Route

Please refer to it [here](https://iammassoud.net/blog/protect-custom-routes-in-docusaurus-using-aws-cognito).

# Authentication using Social Providers

Please refer to it here *(coming soon).*

# Adding limitation to SignUp/SignIn flow

Please refer to it here *(coming soon).*

# Complete Docusaurus Authentication Series

Please find the complete roadmap [here](https://iammassoud.net/blog/docusaurus-authentication-roadmap).

In this series, I will provide a step-by-step guide on how to add an authentication layer to your Docusaurus website using well-known auth providers like AWS Cognito, Google Firebase, Auth0, etc.

Also, you can find more advanced auth flows for complicated scenarios as well.

# Support

In the end, if you find the articles useful, don’t forget to support me at least with a message :)

%%[buymeacoffee]
