Docusaurus with Google Authentication
With a little bit of programming, your site made by Docusaurus can be modified to be accessible only to users signed in to Google.
The source material came from this article by Thomasdevshare. His article describes a similar authentication scheme for Docusaurus with Firebase as the identity provider whereas this article describes the same approach using Google API directly.
The main concepts described in this article will be largely the same as that written by Thomas. The main differences:
- instead of depending on the Firebase package, the
@react-oauth
package is used, and - a new section on designating allowed users to access the page.
(The example in this document is written in TypeScript but it should be easily applicable to JavaScript.)
Pre-requisites
These are the things that you need to set up:
1. Google credential
This step can be done at https://console.cloud.google.com/apis/credentials
Follow the Google instructions to configure your project and create a client ID for Web application.
For local development, add http://localhost
and http://localhost:<port_number>
to the Authorized JavaScript origins for @react-oauth
to work. When deploying to production, specify the actual URL of your site.
Note the Client ID
field for the OAuth 2.0 Client - this needs to be placed into the .env file (see below).
2. Add the @react-oauth/google
dependency
npm i @react-oauth/google
This dependency makes it easy to incorporate the Google SSO implicit-authorization flow.
3. Specify Google Client ID and allowed users
Create a file .env in the root of the directory (same location as docusaurus.config.js) and add the following contents, replacing the text in arrow brackets with actual values:
- .env
- Example
GOOGLE_CLIENTID=<google client ID>
ALLOWED_USERS=<email addresses separated by commas>
GOOGLE_CLIENTID=1111111111111-djfh38dhf467as1hdgg3f2df334msd.apps.googleusercontent.com
ALLOWED_USERS=user1@example.com, user2@example.com, user3@example.com
The Google credentials obtained in Step 1 should replace <google client ID>
.
<email addresses separated by commas>
should be replaced with actual email addresses e.g. email1@example.com, email2@example.com
(The space after the comma will be trimmed by the plugin so it doesn't matter if the commas are followed by spaces.)
4. Configure docusaurus.config.js to read the environment variables
Docusaurus is only able to read the environment variable during pre-processing. This means that the values in the .env file have to be "transferred" to the configuration file.
This is done by first adding the following line to docusaurus.config.js:
require('dotenv').config();
Then, somewhere in the file below, assign the process environment variables to properties under customFields
:
const config = {
title: 'Me3 Technical Documentation',
customFields: {
allowedUsers: process.env.ALLOWED_USERS,
googleClientId: process.env.GOOGLE_CLIENTID,
},
// ...
};
I had used the plugin docusaurus2-dotenv before but it didn't work with deployment to Cloudflare Pages.
Log-in Component
Next create the log-in component. This component displays the Google sign-in button for the user to authenticate himself/herself.
Create a file src/components/login-google/index.tsx
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import {
CredentialResponse,
GoogleLogin,
GoogleOAuthProvider,
} from '@react-oauth/google';
import React from 'react';
export function LoginGoogle(props: {
login: (string) => void,
denied?: boolean,
}) {
const {
siteConfig: { customFields },
} = useDocusaurusContext();
const clientId = customFields.googleClientId as string;
function handleError() {
console.error('Failed to sign in with Google.');
}
function handleSuccess(creds: CredentialResponse) {
const plaintext = decode(creds.credential);
if (!plaintext) {
return;
}
const payload = JSON.parse(plaintext);
if (isAllowed(payload.email)) {
props.login(payload.email);
} else {
props.login(null);
}
}
// Checks if the supplied email address is allowed to see the page.
function isAllowed(email: string): boolean {
let allowedUsers = [];
if (typeof customFields.allowedUsers === "string") {
allowedUsers = customFields.allowedUsers.split(",").map((e) => e.trim());
}
if (allowedUsers.includes(email)) {
return true;
}
return false;
}
return (
<GoogleOAuthProvider clientId={clientId}>
<div
style={{
display: 'flex',
flexFlow: 'column nowrap',
alignItems: 'center',
margin: '5rem auto',
textAlign: 'center',
}}
>
<h2>Please sign in to your Google account to get access.</h2>
{props.denied ? (
<em>Not authorised</em>
) : (
<GoogleLogin
onSuccess={handleSuccess}
onError={handleError}
></GoogleLogin>
)}
</div>
</GoogleOAuthProvider>
);
}
// Function to decode the JWT from Google to get the user's email address.
function decode(jwt: string): string {
const parts = jwt.split('.');
if (parts.length !== 3) {
return '';
}
return window.atob(parts[1]);
}
Swizzle the Root Component
Swizzling is a term describing the overridding of an existing component with a custom implementation of it.
The Root component is the component to swizzle as it covers all generated pages.
import React, { useState } from 'react';
import { LoginGoogle } from '@site/src/components/login-google';
export default function Root({ children }) {
// `email` is used to determine if user is allowed.
const [email, setEmail] = (useState < string) | (null > '');
if (!email) {
return <LoginGoogle login={setEmail} denied={email === null}></LoginGoogle>;
}
return <>{children}</>;
}
Conclusion
One thing to note: the authentication information is only persisted in memory. What this means is that the authentiction is temporary (the correct term is ephemeral). The implication is that as soon as the user refreshes the page, the authenticated information (i.e. email address) is lost and the user gets "logged out".