DEV Community

FusionAuth
FusionAuth

Posted on • Originally published at fusionauth.io on

Using OAuth and PKCE to Add Authentication to Your Gatsby Site

Gatsby is one of the most popular JavaScript static site generators available. While static sites offer excellent performance, they only store state locally in the user’s browser, so they can’t provide features like user authentication natively. If you want to add authentication to your Gatsby site, FusionAuth is an excellent solution.

In this blog post, you’ll learn how to create a Gatsby site that uses FusionAuth to allow users to log in and access their profile securely. This application will use an OAuth Authorization Code workflow and the PKCE extension to log users in and a Node application to store your access token securely. PKCE stands for Proof Key for Code Exchange, and is often pronounced “pixie”.

At a high level, the authorization process looks like this:

Diagram of the OAuth Authorization Code flow with PKCE extension using FusionAuth and Gatsby.
Diagram of the OAuth Authorization Code flow with PKCE extension using FusionAuth and Gatsby.

In this tutorial, you’ll walk through the process step-by-step, but if you want to download the code, it is available on Github.

What we’ll cover

  1. Setting up FusionAuth
  2. Creating a new user
  3. Creating a Node proxy application
  4. Creating a Gatsby site
  5. Conclusion and next steps

What you’ll need

Setting up FusionAuth

Before you start writing any code, download FusionAuth and get it running on your local machine. FusionAuth is available for all major operating systems or it can be run in Docker.

Once you have FusionAuth running, log into the admin panel and create a new Application. This process is outlined here, but you’ll need to add your application’s URLs to the OAuth configuration:

  • Add http://localhost:9000/oauth-callback to the “Authorized redirect URLs”.
  • Add http://localhost:9000 to the “Authorized request origin URLs”.
  • Enter http://localhost:8000 in the “Logout URL” field.

You’ll also want to save the “Client Id” and “Client secret” values as you’ll need them later.

FusionAuth configuration options for a Gatsby static site.

You’ll also need to create an API key. Go to “Settings”, then to “API Keys”. You may create one with adminstrative privileges for the purposes of this tutorial. For a production application, please follow the principle of least privilege and limit the endpoints available to the key. Save the API key off as you’ll need it later.

Creating a new user

To test your Gatsby-based login, you’ll need to add a new user and register them for your application in FusionAuth. From the Users page in FusionAuth, click “+” to add a user. Enter an email address and password for your new user and click the save button.

Creating a new user in FusionAuth.

Next, click “Add registration” to link this user to the application you created in Step 1. Click the save button when you’re finished.

Registering a user in FusionAuth.

Now that a user is registered for your application, you can start building the Node app.

Creating a Node proxy application

This project will use two separate applications: a Node app to securely store your access token and make calls to the FusionAuth API, and a Gatsby site to present information to the user. The Node app will have four endpoints:

  • /login - Generates the FusionAuth login URL with a PKCE challenge
  • /oauth-callback - Trades the one-time authorization code and PKCE verifier for an access token which is added to session storage
  • /user - Uses the access token and the FusionAuth introspect endpoint to get information about the current user
  • /logout - Logs the user out and destroys the session

You’ll create all the endpoints first, and then you’ll see how to call them from Gatsby.

Setting up the Node app

Before you get started, you need to create a new subdirectory and initialize an Express app. Use a similar structure to the one outlined here:

fusionauth-gatsby
├─gatsby
├─server
└─config.js

Enter fullscreen mode Exit fullscreen mode

Your config.js file should contain all your FusionAuth information. Add the following to the file with your FusionAuth application’s ID, client ID, and ports:

module.exports = {
  // FusionAuth info (copied from the FusionAuth admin panel)
  clientID: '5eb76e67-c65e-474d-ba23-4cb61b0c8414',
  clientSecret: 'BVS1NIgID3HWE5U38HYSb4DOie3UbIySOsJKLT41WWg',
  redirectURI: 'http://localhost:9000/oauth-callback',
  applicationID: '5eb76e67-c65e-474d-ba23-4cb61b0c8414',

  // Your FusionAuth api key
  apiKey: 'skAHV4mOEhz2zYQcG_5l4BkhsCzmtYTU8VGOi8Y40zo',

  // Ports
  clientPort: 8000,
  serverPort: 9000,
  fusionAuthPort: 9011
};

Enter fullscreen mode Exit fullscreen mode

Within the ./server directory, create a new Express app and install the cors, session and request packages.

npm init
# Complete all the questions as appropriate
npm install express cors express-session request --save

Enter fullscreen mode Exit fullscreen mode

Next, open up your package.json file and add a "start" script:

// ...
"scripts": {
  "start": "node index.js"
},
// ...

Enter fullscreen mode Exit fullscreen mode

Finally, create a new file in the ./server directory called index.js that will initialize your Express app:

const express = require('express');
const session = require('express-session');
const cors = require('cors');
const config = require('../config');

// configure Express app and install the JSON middleware for parsing JSON bodies
const app = express();
app.use(express.json());

// configure sessions
app.use(session(
  {
    secret: '1234567890',
    resave: false,
    saveUninitialized: false,
    cookie: {
      secure: 'auto',
      httpOnly: true,
      maxAge: 3600000
    }
  })
);

// configure CORS
app.use(cors(
  {
    origin: true,
    credentials: true
  })
);

// use routes
app.use('/user', require('./routes/user'));
app.use('/login', require('./routes/login'));
app.use('/oauth-callback', require('./routes/oauth-callback'));
app.use('/logout', require('./routes/logout'));

// start server
app.listen(config.serverPort, () => console.log(`FusionAuth example app listening on port ${config.serverPort}.`));

Enter fullscreen mode Exit fullscreen mode

In the following sections, you’ll create the route files listed in the code above.

Creating the login route

To generate a login URL, your application will need to create a PKCE verifier and challenge. It will send the challenge to FusionAuth via query string parameters along with your client ID and a redirect URL.

Using PKCE adds an additional layer of security, as it is a one time use and guarantees that the Node application that generated the challenge is the same one that sent the verifier. Normally, PKCE is used where the client cannot keep a secret, such as a single page application.

To generate a PKCE challenge and verifier, you’ll need to use some of the Node crypto functions. Create a new folder in the ./server directory called helpers. Add a new file called pkce.js to the folder. You will generate a verifier and challenge in this file:

const crypto = require('crypto');

function base64URLEncode(str) {
  return str
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=/g, "")
}

function sha256(buffer) {
  return crypto.createHash("sha256").update(buffer).digest()
}

module.exports.generateVerifier = () => {
  return base64URLEncode(crypto.randomBytes(32))
}

module.exports.generateChallenge = (verifier) => {
  return base64URLEncode(sha256(verifier))
}

Enter fullscreen mode Exit fullscreen mode

The two exported functions, generateVerifier and generateChallenge, will be used in your login route to create a PKCE verifier. Create a new directory called routes in your ./server directory and add a new file called login.js to it:

const express = require('express');
const router = express.Router();
const config = require('../../config');
const pkce = require('../helpers/pkce');

router.get('/', (req, res) => {
  // Generate and store the PKCE verifier
  req.session.verifier = pkce.generateVerifier();

  // Generate the PKCE challenge
  const challenge = pkce.generateChallenge(req.session.verifier);

  // Redirect the user to log in via FusionAuth
  res.redirect(`http://localhost:${config.fusionAuthPort}/oauth2/authorize?`+
    `client_id=${config.clientID}&redirect_uri=${config.redirectURI}&response_type=code`+
    `&code_challenge=${challenge}&code_challenge_method=S256`);
});

module.exports = router;

Enter fullscreen mode Exit fullscreen mode

Now when users visit localhost:9000/login the Node app will generate a PKCE verifier and challenge, save the verifier to session storage, and redirect the user to FusionAuth with the challenge in the URL. The FusionAuth app will store this challenge and make sure that the verifier sent in the OAuth callback is valid.

Creating the OAuth callback

Once the user has entered their username and password, the FusionAuth server will check their credentials and redirect them to your Node app’s OAuth callback endpoint with an authorization code. Your app will use that code and the PKCE verifier generated in the previous step to request a long-lived access token.

Again, adding PKCE adds another layer of security by proving that the entity which sent the challenge is now requesting an access token. Your Node app will store the access token returned by FusionAuth in session storage and redirect the user to the Gatsby profile page we’ll create in the next step.

Create a new route called oauth-callback.js and add the following:

const express = require('express');
const router = express.Router();
const request = require('request');
const config = require('../../config');

router.get('/', (req, res) => {
  request(
    // POST request to /token endpoint
    {
      method: 'POST',
      uri: `http://localhost:${config.fusionAuthPort}/oauth2/token`,
      form: {
        'client_id': config.clientID,
        'client_secret': config.clientSecret,
        'code': req.query.code,
        'code_verifier': req.session.verifier,
        'grant_type': 'authorization_code',
        'redirect_uri': config.redirectURI
      }
    },

    // callback
    (error, response, body) => {
      // save token to session
      req.session.token = JSON.parse(body).access_token;

      // redirect to Gatsby
      res.redirect(`http://localhost:${config.clientPort}/profile`);
    }
  );
});

module.exports = router;

Enter fullscreen mode Exit fullscreen mode

Your app now authenticates users and stores their access tokens in session storage. When you build the Gatsby application, you’ll pass the session ID stored in a cookie to the Node app to access the current user endpoint.

Why not omit the Node application? Storing the access token in the browser is, in general, insecure. It’s vulnerable to cross site scripting attacks. If you must store the access token in the browser, make sure it is stored as a Secure HttpOnly cookie.

Creating the current user route

FusionAuth includes an introspect endpoint that decodes the access token (represented by a JWT) and returns details about the current user. You will call this endpoint through your Node application by attaching the authorization token stored in the session.

Create a new route file called user.js:

const express = require('express');
const router = express.Router();
const request = require('request');
const config = require('../../config');

router.get('/', (req, res) => {
  // token in session -> get user data and send it back to the react app
  if (req.session.token) {
    request(
      // POST request to /introspect endpoint
      {
        method: 'POST',
        uri: `http://localhost:${config.fusionAuthPort}/oauth2/introspect`,
        form: {
          'client_id': config.clientID,
          'token': req.session.token
        }
      },

      // callback
      (error, response, body) => {
        let introspectResponse = JSON.parse(body);

        // valid token -> get more user data and send it back to Gatsby
        if (introspectResponse.active) {
          request(
            // GET request to /registration endpoint
            {
              method: 'GET',
              uri: `http://localhost:${config.fusionAuthPort}/api/user/registration/`+
                `${introspectResponse.sub}/${config.applicationID}`,
              json: true,
              headers: {
                'Authorization': config.apiKey
              }
            },

            // callback
            (error, response, body) => {
              res.send(
                {
                  token: {
                    ...introspectResponse,
                  },
                  ...body
                }
              );
            }
          );
        }

        // expired token -> send nothing
        else {
          req.session.destroy();
          res.send({});
        }
      }
    );
  }

  // no token -> send nothing
  else {
    res.send({});
  }
});

module.exports = router;

Enter fullscreen mode Exit fullscreen mode

Assuming the call is successful, this endpoint will return a decoded token object, which includes the current user’s ID, email address, and authentication details. You will use this endpoint for the profile page in Gatsby.

Creating the logout route

The last endpoint in your Node app allows users to log out. To fully log out of the application, you need to both destroy the Node application’s session and redirect users to the FusionAuth logout endpoint.

Create a new route called logout.js and add the following:

const express = require('express');
const router = express.Router();
const config = require('../../config');

router.get('/', (req, res) => {
  // delete the session
  req.session.destroy();

  // end FusionAuth session
  res.redirect(`http://localhost:${config.fusionAuthPort}/oauth2/logout?client_id=${config.clientID}`);
});

module.exports = router;

Enter fullscreen mode Exit fullscreen mode

Now that your server-side application is complete, you can test the login and logout flows.

Start the Node app using npm start and make sure FusionAuth is running locally. Visit localhost:9000/login, and you should be redirected to the FusionAuth login page. Log in, and you should be taken to the OAuth callback and then to localhost:8000/profile. That URL won’t work (we’ll create it in the Gatsby app next), but you should be able to go to localhost:9000/logout to end your session.

Creating a Gatsby site

Now that you’ve set up FusionAuth and your server-side Node application, you are ready to create your new Gatsby site. The easiest way to get started is to install the Gatsby CLI:

npm install -g gatsby-cli

Enter fullscreen mode Exit fullscreen mode

Once installed, you can create a new Gatsby site by running the following in your terminal in the root directory of your project:

gatsby new gatsby

Enter fullscreen mode Exit fullscreen mode

There are other ways to set up and configure your Gatsby site, so be sure to read over their documentation to learn more.

Creating the home page

The Gatsby site we create will include a home page and profile page. The home page will have a login link that takes the user to the Node app endpoint we created in the previous section.

Before you create the home page, create a new folder in the ./gatsby directory called helpers. Add a new file within it called auth.js:

import config from "../../../config"

export const generateLoginUrl = () => {
  return `http://localhost:${config.serverPort}/login`
}

Enter fullscreen mode Exit fullscreen mode

This function returns the login URL for your Node application. Next, update the ./gatbsy/src/pages/index.js file to display a page title and login link:

import React from "react"
import Layout from "../components/layout"
import {
  generateLoginUrl,
} from '../helpers/auth';

const IndexPage = () => (
  <Layout>
    <h1>Home</h1>
    <p>
      <a href={generateLoginUrl()}>Login to get started</a>
    </p>
  </Layout>
)

export default IndexPage

Enter fullscreen mode Exit fullscreen mode

To start your Gatsby app, navigate to the ./gatsby directory in your terminal and run gatsby develop. Head over to localhost:8000 in your browser where you should see a link to login.

A login link in Gatsby.

Creating the profile page

The profile page will call the /user endpoint in your Node app and show users a logout link. Because this page is only available to authenticated users, you’ll make it a client-only route. This prevents the page from being indexed in search engines or generated during static site generation.

First, open up the gatsby-node.js configuration file to create the client-only route. Add the following to the file:

// Client-only profile route
exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions
  if (page.path.match(/^\/profile/)) {
    page.matchPath = "/profile/*"
    createPage(page)
  }
}

Enter fullscreen mode Exit fullscreen mode

Next, update the helpers/auth.js file created above. Add a new function called generateLogoutUrl that returns the Node app’s logout endpoint:

// ...
export const generateLogoutUrl = () => {
  return `http://localhost:${config.serverPort}/logout`
}

Enter fullscreen mode Exit fullscreen mode

And add another exported function called getCurrentUser which calls the Node app to get the current user from FusionAuth:

// ...
export const getCurrentUser = callback => {
  fetch(`http://localhost:${config.serverPort}/user`, {credentials: 'include'})
    .then(res => res.json())
    .then(data => {
      if (data && data.token) {
        callback(null, data.token)
      } else {
        throw new Error('Something went wrong and the user could not be found.')
      }
    })
    .catch(error => callback(error))
}

Enter fullscreen mode Exit fullscreen mode

Finally, create a new page at ./gatsby/src/pages/profile.js. Gatsby will automatically create a new route for any files in the pages directory. Open the new file and add the following:

import React from "react"
import Layout from "../components/layout"
import { generateLoginUrl, generateLogoutUrl, getCurrentUser } from '../helpers/auth';

class ProfilePage extends React.Component {
  state = {
    user: null,
  }

  componentDidMount() {
    getCurrentUser((error, user) => {
      if (!error && user) {
        this.setState({ user })
      } else {
        window.location.href = generateLoginUrl()
      }
    })
  }

  render() {
    return (
      <Layout>
        <h1>Profile</h1>
        {this.state.user ? (
          <p>You are currently logged in as {this.state.user.email}</p>
        ) : (
          ""
        )}
        <p>
          <a href={generateLogoutUrl()}>Logout</a>
        </p>
      </Layout>
    )
  }
}

export default ProfilePage

Enter fullscreen mode Exit fullscreen mode

Your Gatsby application is now complete. In the final step, you’ll test the entire login and logout flow.

Testing the whole thing out

Start your FusionAuth server (if you haven’t already) and your Node server using npm start. Navigate to your Gatsby app in the terminal and start it with gatsby develop. Visit localhost:8000. You should be able to:

  • Get to the FusionAuth login page using the link on the Gatsby site’s home page
  • Be redirected to the profile page where your email address is shown
  • Successfully log out by clicking the “Logout” button on your profile page

The profile page in Gatsby.

You have now successfully implemented an OAuth authorization code workflow using the PKCE extension in Gatsby with FusionAuth. This method allows you to authenticate users securely without exposing your client secret or access token by proxying your calls to FusionAuth through your Node application.

Conclusion and next steps

While I hope this tutorial helps you get started with user authentication using FusionAuth and Gatsby, there are several next steps you could take to make the application more robust:

  • You could add user registration to allow users to self-register.
  • You could use higher order components to protect certain content or pages from unauthenticated users.
  • You could use the access token saved in session storage to access another API.

If you have questions or need help integrating your FusionAuth application with Gatsby, feel free to leave a comment below.

Top comments (0)