Putting Docusaurus behind authentication through Azure Static Web Apps

Then you create a documentation system that you are hosting on Azure Static Web Apps, but suddenly the request comes in to put authentication behind it. How do you get started?

Normally this would be a lengthy task. We need a server to check credentials, implement OAuth, ... BUT Azure made this SUPER-easy by providing handy routes on our Static Web Apps BY DEFAULT:

  • /.auth/me check the current logged in user
  • /.auth/login/<provider> (e.g., /.auth/login/aad) login to the provider
  • /.auth/logout logout

When we login, it will provide something like this:

{
    "clientPrincipal": {
        "identityProvider": "aad",
        "userId": "your_user_id",
        "userDetails": "xavier@example.com",
        "userRoles": [
            "anonymous",
            "authenticated"
        ]
    }
}

which we can then check to see if the user details for example include our domain configured in the settings.

What will we be creating?

In our case, we simply want an authentication wall in front of our project.

Project Structure

First create the following files in your Docusaurus project:

├── docusaurus.config.js
└── src
   ├── components
   |  ├── Auth
   |  |  └── index.js
   |  ├── Loading
   |  |  ├── index.js
   |  |  └── styles.css
   ├── theme
   |  └── Root.js
   └── constants.js

in the above we are:

  • Creating components for requiring a user to be logged in (Root.js)
    • this works together with Auth and Loading as re-usable components
  • Defining constants for our project for easy management

Creating our Files

Let's create these files

src/components/Auth/index.js

import React, {useEffect, useState} from 'react';

import {Redirect, useLocation} from '@docusaurus/router';

import Loading from '../Loading';
import {CHECK_DOMAIN, SWA_PATH_LOGIN, SWA_PATH_ME} from '../../constants';

/**
 * In Azure Static Web Apps we can get the user from the /.auth/me endpoint.
 *
 * Example
 * {
 *     "clientPrincipal": {
 *         "identityProvider": "aad",
 *         "userId": "f906824d20b542919bcf31287b89a70e",
 *         "userDetails": "xavier@composabl.io",
 *         "userRoles": [
 *             "anonymous",
 *             "authenticated"
 *         ]
 *     }
 * }
 */
const getUser = async () => {
  const response = await fetch(SWA_PATH_ME);
  const payload = await response.json();
  const {clientPrincipal} = payload;
  return clientPrincipal;
};

export function AuthCheck({children}) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    async function loadUser() {
      try {
        const user = await getUser();
        setUser(user);
      } catch (error) {
        console.error(error);
      } finally {
        setIsLoading(false);
      }
    }

    loadUser();
  }, []);

  const location = useLocation();

  let from = location.pathname;

  if (isLoading) {
    return <Loading />;
  }

  // If we are not logged in, redirect to the login page
  if (!user?.userDetails) {
    window.location.href = `${SWA_PATH_LOGIN}?post_login_redirect_uri=${from}`;
    return <>Authenticating...</>;
  }

  // If we are logged in but not authorized, show a message
  if (
    user?.userDetails &&
    user?.userDetails?.indexOf(`@${CHECK_DOMAIN}`) === -1
  ) {
    return <div>No access</div>;
  }

  return children;
}

src/components/Loading/index.js

import React from 'react';

import './styles.css';

export default function Loading() {
  return (
    <div className="overlay">
      <div className="overlayDoor" />
      <div className="overlayContent">
        <div className="loader">
          <div className="inner" />
        </div>
      </div>
    </div>
  );
}

src/components/Loading/styles.css

/* src/components/Loading/styles.css */

.overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 100000000;
}
.overlay .overlayDoor:before,
.overlay .overlayDoor:after {
  content: '';
  position: absolute;
  width: 50%;
  height: 100%;
  background: #111;
  transition: 0.5s cubic-bezier(0.77, 0, 0.18, 1);
  transition-delay: 0.8s;
}
.overlay .overlayDoor:before {
  left: 0;
}
.overlay .overlayDoor:after {
  right: 0;
}
.overlay.loaded .overlayDoor:before {
  left: -50%;
}
.overlay.loaded .overlayDoor:after {
  right: -50%;
}
.overlay.loaded .overlayContent {
  opacity: 0;
  margin-top: -15px;
}
.overlay .overlayContent {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  transition: 0.5s cubic-bezier(0.77, 0, 0.18, 1);
  background: #fff;
}
.overlay .overlayContent .skip {
  display: block;
  width: 130px;
  text-align: center;
  margin: 50px auto 0;
  cursor: pointer;
  color: #fff;
  font-family: 'Nunito';
  font-weight: 700;
  padding: 12px 0;
  border: 2px solid #fff;
  border-radius: 3px;
  transition: 0.2s ease;
}
.overlay .overlayContent .skip:hover {
  background: #ddd;
  color: #444;
  border-color: #ddd;
}
.loader {
  width: 128px;
  height: 128px;
  border: 3px solid #222222;
  border-bottom: 3px solid transparent;
  border-radius: 50%;
  position: relative;
  animation: spin 1s linear infinite;
  display: flex;
  justify-content: center;
  align-items: center;
}
.loader .inner {
  width: 64px;
  height: 64px;
  border: 3px solid transparent;
  border-top: 3px solid #222222;
  border-radius: 50%;
  animation: spinInner 1s linear infinite;
}
@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
@keyframes spinInner {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(-720deg);
  }
}

src/components/theme/Root.js

import React from 'react';
import {AuthCheck} from '../components/Auth';

export default function Root({children}) {
  return <AuthCheck children={children} />;
}

src/components/theme/constants.js

export const SWA_PATH_LOGIN = '/.auth/login/aad';
export const SWA_PATH_LOGOUT = '/.auth/logout';
export const SWA_PATH_ME = '/.auth/me';
export const CHECK_DOMAIN = 'example.com';

Conclusion

And that's it! It couldn't be any simpler to put an auth wall in front of your page!