Docusaurus Authentication using AWS Cognito
A guide on how to add authentication to your Docusaurus website utilizing AWS Cognito
Background
Docusaurus 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.
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.
Please find the more advanced authentication flows using Docusaurus and AWS Cognito here.
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
Installing packages
After initializing your Docusaurus project, it is time to install the necessary packages:
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 pageadd
: A helper package is needed for most of the next packagesAWS-amplify
: The main AWS Amplify library for JavaScriptaws-crt
: NodeJS/Browser bindings for the AWS Common Runtimedocusaurus-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:
// 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 totrue
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 asfalse
.
AWS Cognito Setup
In this section, we will see how to create an AWS Cognito User Pool to satisfy the requirements.
Go to the Amazon Cognito console. If prompted, enter your AWS credentials.
Choose User Pools.
Choose Create a user pool to start the user pool creation wizard.
In Step 1 (Configure sign-in experience), in the section Cognito user pool sign-in options, choose Email and press Next.
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.
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.
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.
In Step 5 (Integrate your app), in the section User pool name, write a name for the user pool.
In Step 5 (Integrate your app), in the section Hosted authentication pages, opt for Use the Cognito Hosted UI.
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.
In Step 5 (Integrate your app), in the section Initial app client, fill up the name in the App client name field.
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.
In Step 5 (Integrate your app), in the section Advanced app client settings, add the Allowed sign-out URLs - optional and press Next.
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.
❓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.
├── 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 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:
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...).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 or follow my guide to add the required files.
Check here 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.
# .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.
// 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
or any other URL.// 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>
.// 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.
If the route is protected and the user is
authenticated
, it will redirect to the requested page.If the route is protected and the user is not
authenticated
, it will redirect to the login page.If the route is
login
orlogout
, it will redirect to the base URL.Otherwise, means the path is not protected and will redirect to the requested page.
// 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).
// 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} />; }
// 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> ); }
/* 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 betweenLogin
andLogout
based on the fact whether the user isauthenticated
or not.So, we just override
useNavbarItems()
function and use the modified function placedsrc/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.// 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> )} </> } /> ); }
/* 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 betweenLogin
andLogout
based on the fact whether the user isauthenticated
or not.So, we just override
useNavbarItems()
function and use the modified function placedsrc/utils/utils.js
.The same change happens in
Navbar Desktop
as well.// 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.
// 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.// 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.
🔔 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.
Authentication for Specific Route
Please refer to it here.
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.
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 :)