tutorial // Jan 14, 2022

How to Implement an OAuth2 Workflow in Node.js

How to implement an OAuth2 workflow in JavaScript and Node.js by setting up an OAuth connection to the Github API.

How to Implement an OAuth2 Workflow in Node.js

Getting Started

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start. Before you run joystick start, we need to add one dependency: node-fetch.

Terminal

cd app && npm i node-fetch

With that installed, go ahead and start up your app:

Terminal

joystick start

After this, your app should be running and we're ready to get started.

Fair warning

While OAuth2 itself is a standard for implementing authentication patterns, the implementation of that standard is not always consistent. We've chosen Github as our example API as their OAuth implementation is well-done and well-documented. This is not always the case for your API of choice.

The point being: look at the steps we cover here as an approximation of what an OAuth2 implementation should look like for an API. Sometimes you get lucky, sometimes you end up with a noise complaint from the police. A few common inconsistencies to watch out for:

  1. Undocumented or poorly documented parameters that need to be passed in the HTTP headers, query params, or body.
  2. Undocumented or poorly documented response types that need to be passed in the HTTP headers. For example, some APIs may require the Accept header being set to application/json in order to get back a response in a JSON format.
  3. Bad example code in the documentation.
  4. Bad error codes when incorrect parameters (see the previous items above) are passed.

While this isn't everything you'll encounter, these are usually the ones that will waste your time and energy. If you're certain you're following your APIs documentation perfectly and still having issues: review the list above and play around with what you're passing (even if it's not documented by the API in question, as frustrating as that may be).

Note: there is always help available. Book an on-demand pair programming session with CheatCode here to get hands-on help.

Getting credentials from the Github API

To start, we'll need to register our application with Github and obtain security credentials. This is a common pattern with all OAuth2 implementations. In particular, you will need two things: a client_id and a client_secret.

The client_id tells the API who or what app is trying to get permission to authenticate on behalf of a user while the client_secret authorizes the connection by proving ownership of the app specified by the client_id (this is public so technically anybody can pass it to an API, while the client_secret is, like the name implies, secret).

If you don't already have a Github account, head to this link and create an account.

Once you're logged in, in the top-right hand corner of the site, click the circle icon with your avatar and a down arrow next to it. From the menu that pops up, select "Settings."

Next, near the bottom of the left-hand menu on that page, locate and click the "Developer Settings" option. On the next page, in the left-hand menu, locate and click the "OAuth Apps" option.

If this is your first time registering an OAuth app with Github, you should see a green button that prompts you to "Register a new application." Click that to start the process of obtaining your client_id and client_secret.

lk0Bz2Ex6wPGpPsu/FI9fF7rMxhuy1x1h.0
Registering a new OAuth application on Github.

On this page, you will need to provide three things:

  1. A name for your OAuth application. This is what Github will display to users when they confirm your access to their account.
  2. A homepage URL for your app (this can just be a dummy URL for testing).
  3. An "Authorization callback URL" which is where Github will send a special code in response to a user's approval to grant our app permission to access their account.

For #3, in this tutorial, we want to enter http://localhost:2600/oauth/github (this is different from what you'll see in the screenshot above but is equivalent in terms of intent). http://localhost:2600 is where the app we created using CheatCode's Joystick framework will run by default. The /oauth/github part is the path/route that we'll wire up next where we'll expect Github to send us an authorization code that we can exchange for an access_token for the user's account.

After this is filled out, click "Register application" to create your OAuth app. On the next screen, you will want to locate the "Client ID" and click the "Generate a new client secret" button near the middle of the page.

lk0Bz2Ex6wPGpPsu/BQoX6ACfF1gTCYSo.0
Viewing an OAuth app on Github.

Note: when you generate your client_secret Github will intentionally only show it to you on screen one time. It's recommended that you back this and your client_id up in a password manager or other secrets manager. If you lose it, you will need to generate a new secret and delete the old one to avoid a potential security issue.

Keep this page up or copy the client_id and client_secret for use in the next step.

Adding our credentials to our settings file

Before we dig into the code, next, we need to copy our client_id and client_secret into our application's settings file. In a Joystick app, this is automatically created for us when we run joystick create.

Open up the settings-development.json file at the root of your app:

/settings-development.json

{
  "config": {
    "databases": [ ... ],
    "i18n": {
      "defaultLanguage": "en-US"
    },
    "middleware": {},
    "email": { ... }
  },
  "global": {},
  "public": {
    "github": {
      "client_id": "dc47b6a0a67b904c58c7"
    }
  },
  "private": {
    "github": {
      "client_id": "dc47b6a0a67b904c58c7",
      "client_secret": "<Client Secret Here>",
      "redirect_uri": "http://localhost:2600/oauth/github"
    }
  }
}

We want to focus on two places: the public and private objects already present in the file. Inside of both, we want to nest a github object that will contain our credentials.

Pay attention here: we only want to store the client_id under the public.github object while we want to store both the client_id and client_secret under the private.github object. We also want to add the redirect_uri we typed in on Github (the http://localhost:2600/oauth/github one).

Once you've got these set, we're ready to dig into the code.

Wiring up the client request for authorization

To begin, we're going to add a simple page in our UI where we can access a "Connect to Github" button our users can click to initialize an OAuth request. To build it, we're going to reuse the / route that's automatically defined for us when we generate an app with joystick create. Real quick, if we open up /index.server.js at the root of the project, we can see how this is being rendered by Joystick:

/index.server.js

import node from "@joystick.js/node";
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

In a Joystick app, routes are defined via an Express.js instance that's automatically set up via the node.app() function imported from the @joystick.js/node package. To that function, an object is passed with a routes option set to an object where all of the routes for our app are defined.

Here, the / index route (or "root" route) uses the res.render() function defined by Joystick on the HTTP response object we get from Express.js. That function is designed to render a Joystick component created using Joystick's UI library @joystick.js/ui.

Here, we can see the ui/pages/index/index.js path being passed. Let's open up that file now and modify it to display our "Connect to Github" button.

/ui/pages/index/index.js

import ui from "@joystick.js/ui";

const Index = ui.component({
  events: {
    'click .login-with-github': (event) => {
      location.href = `https://github.com/login/oauth/authorize?client_id=${joystick.settings.public.github.client_id}&scope=repo user`;
    },
  },
  css: `
    div {
      padding: 40px;
    }

    .login-with-github {
      background: #333;
      padding: 15px 20px;
      border-radius: 3px;
      border: none;
      font-size: 15px;
      color: #fff;
    }

    .login-with-github {
      cursor: pointer;
    }

    .login-with-github:active {
      position: relative;
      top: 1px;
    }
  `,
  render: () => {
    return `
      <div>
        <button class="login-with-github">Connect to Github</button>
      </div>
    `;
  },
});

export default Index;

Here, we've overwritten the existing contents of our /ui/pages/index/index.js file with the component that will render our button. In Joystick, components are defined by calling the ui.component() function imported from the @joystick.js/ui package and passed an object of options to describe the behavior and appearance of the component.

Here, down in the render function, we return a string of HTML that we want Joystick to render in the browser for us. In that string, we have a simple <button></button> element with a class name .login-with-github. If we look at the option above render, css, we can see some styles being applied to our component, adding a bit of padding to the page and styling our button up.

The important part here is up in the events object. Here, we define an event listener for a click event on an element with the class .login-with-github. When that event is detected in the browser, the function we've assigned to 'click .login-with-github here will be called.

Inside, our goal is to redirect the user to Github's URL for kicking off an OAuth authorization request. To do it, we set the global location.href value in the browser to a string containing the URL along with some query parameters:

  1. client_id here is assigned to the value of joystick.settings.public.github.client_id that we set in our settings-development.json file earlier.
  2. scope set equal to two "scopes" that grant specific permissions to the access_token we get from Github for this user. Here, we're using the repo and user (space-separated as per the Github documentation) scopes to give us access to a users repositories on Github and their full user profile. A full list of scopes to request is available here.

If we save these changes with our app running, Joystick will auto-refresh in the browser. Assuming our credentials are correct, we should be redirected to Github and see something like this:

lk0Bz2Ex6wPGpPsu/PRsIMws6cdHQEm2E.0
Approving an OAuth request as a user on Github.

Next, before we click the "Authorize" button, we need to wire up the endpoint that Github will redirect the user to (the "Authorization callback URL" that we set to http://localhost:2600/oauth/github earlier).

Handling the token exchange

The final step to get everything working is to perform a token exchange with Github. In order to approve our request and finalize our connection, Github needs to verify the request to connect with our server. To do it, when the user clicks "Authorize" in the UI we just saw on Github, they will send a request to the "Authorization callback URL" we specified when setting up our app, passing a temporary code value in the query params of the request URL that we can "exchange" for a permanent access_token for our user.

To start, the first thing we need to do is wire up that URL/route back in our index.server.js file:

/index.server.js

import node from "@joystick.js/node";
import api from "./api";
import github from "./api/oauth/github";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/oauth/github": async (req, res) => {
      await github({ req });
      res.status(200).redirect('/');
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

Some minor changes to what we saw earlier. Here, we're adding our route /oauth/github in the exact same way we learned about / earlier. Inside, we add the async keyword to the function that will be called when our route is loaded, anticipating a call to a function github() which will return a JavaScript Promise that we can await before responding to the request to the route.

Once that function completes, we want to respond to the request from Github with a status of 200 and call .redirect() to redirect the user back to the page in our app where they originated the request (our / index route).

Next, let's wire up that function we anticipated being available at /api/oauth/github.js in our project:

/api/oauth/github.js

/* eslint-disable consistent-return */

import fetch from 'node-fetch';
import { URL, URLSearchParams } from 'url';

const getReposFromGithub = (username = '', access_token = '') => {
  return fetch(`https://api.github.com/user/repos`, {
    headers: {
      Accept: 'application/json',
      Authorization: `token ${access_token}`,
    },
  }).then(async (response) => {
    const data = await response.json();
    return data;
  }).catch((error) => {
    console.warn(error);
    throw new Error(error);
  });
};

const getUserFromGithub = (access_token = '') => {
  return fetch('https://api.github.com/user', {
    headers: {
      Accept: 'application/json',
      Authorization: `token ${access_token}`,
    },
  }).then(async (response) => {
    const data = await response.json();
    return data;
  }).catch((error) => {
    console.warn(error);
    throw new Error(error);
  });
};

const getAccessTokenFromGithub = (code = '') => {
  try {
    const url = new URL('https://github.com/login/oauth/access_token');
    const searchParams = new URLSearchParams({
      client_id: joystick.settings.private.github.client_id,
      client_secret: joystick.settings.private.github.client_secret,
      code,
      redirect_uri: joystick.settings.private.github.redirect_uri,
    });

    url.search = searchParams.toString();

    return fetch(url, {
      method: 'POST',
      headers: {
        Accept: 'application/json'
      },
    }).then(async (response) => {
      const data = await response.json();
      return data;
    }).catch((error) => {
      console.warn(error);
      throw new Error(error);
    });
  } catch (exception) {
    throw new Error(`[github.getAccessTokenFromGithub] ${exception.message}`);
  }
};

const validateOptions = (options) => {
  try {
    if (!options) throw new Error('options object is required.');
    if (!options.req) throw new Error('options.req is required.');
  } catch (exception) {
    throw new Error(`[github.validateOptions] ${exception.message}`);
  }
};

const github = async (options, { resolve, reject }) => {
  try {
    validateOptions(options);
    const { access_token } = await getAccessTokenFromGithub(options?.req?.query?.code);
    const user = await getUserFromGithub(access_token);
    const repos = await getReposFromGithub(user?.login, access_token);

    // NOTE: Set this information on a user in your database or store elsewhere for reuse.
    console.log({
      access_token,
      user,
      repos,
    });

    resolve();
  } catch (exception) {
    reject(`[github] ${exception.message}`);
  }
};

export default (options) =>
  new Promise((resolve, reject) => {
    github(options, { resolve, reject });
  });

To make everything easier to understand, here, we're doing a full code dump and then stepping through it. In this file, we're using a pattern known as the action pattern (something I came up with a few years back for organizing algorithmic or multi-step code in an app).

The basic construction of an action pattern is that we have a single main function (here, defined as github) that calls other functions in sequence. Each function in that sequence performs a single task and if necessary, returns a value to hand off to the other functions in the sequence.

Each function is defined as an arrow function with a JavaScript try/catch block immediately inside of its body. In the try block, we run the code for the function and in the catch we call to throw passing a standardized string with our error.

The idea at play here is to give our code some structure and keep things organized while making errors easier to track down (if an error occurs within a function, the [github.<functionName>] part tells us where exactly the error occurred).

Here, because this is a "Promise" action, we wrap the main github() function with a JavaScript Promise at the bottom of our file and export that function. Back in our /index.server.js file, this is why we can use the async/await pattern.

For our "action," we have three steps:

  1. Exchange the code that we get from Github for a permanent access_token.
  2. Get the user associated with that access_token from the Github API.
  3. Get the repos for the user associated with that access_token from the Github API.

The idea here is to showcase the process of getting a token and then performing API requests with that token. So it's clear, this is kept generic so that you can apply this pattern/login to any OAuth API.

/api/oauth/github.js

const getAccessTokenFromGithub = (code = '') => {
  try {
    const url = new URL('https://github.com/login/oauth/access_token');
    const searchParams = new URLSearchParams({
      client_id: joystick.settings.private.github.client_id,
      client_secret: joystick.settings.private.github.client_secret,
      code,
      redirect_uri: joystick.settings.private.github.redirect_uri,
    });

    url.search = searchParams.toString();

    return fetch(url, {
      method: 'POST',
      headers: {
        Accept: 'application/json'
      },
    }).then(async (response) => {
      const data = await response.json();
      return data;
    }).catch((error) => {
      console.warn(error);
      throw new Error(error);
    });
  } catch (exception) {
    throw new Error(`[github.getAccessTokenFromGithub] ${exception.message}`);
  }
};

Focusing on the first step in the sequence getAccessTokenFromGithub(), here, we need to perform a request back to the https://github.com/login/oauth/access_token endpoint in the Github API to get a permanent access_token.

To do it, we want to perform an HTTP POST request (as per the Github docs and the standard for OAuth implementations), passing the required parameters for the request (again, per Github but similar for all OAuth2 requests).

To do that, we import the URL and URLSearchParams classes from the Node.js url package (we don't have to install this package—it's automatically available in a Node.js app).

First, we need to create a new URL object for the /login/oauth endpoint on Github with new URL() passing in that URL. Next, we need to generate the search params for our request ?like=this and so we use the new URLSearchParams() class, passing in an object with all of the query parameters we want to add to our URL.

Here, we need four: client_id, client_secret, code, and redirect_uri. Using these four parameters, Github will be able to authenticate our request for an access_token and return one we can use.

For our client_id, client_secret, and redirect_uri, we pull these in from the joystick.settings.private.github object we defined earlier in the tutorial. The code is the code that we retrieved from the req?.query?.code value passed to us by Github (in an Express.js app, any query params passed to our server are set to the object query on the inbound request object).

With that, before we perform our request we add our search params to our URL by setting the url.search value equal to the result of calling .toString() on our searchParams variable. This will generate a string that looks like ?client_id=xxx&client_secret=xxx&code=xxx&redirect_uri=http://localhost:2600/oauth/github.

Finally, with this, up top we import fetch from the node-fetch package we installed earlier. We call it, passing our url object we just generated, followed by an options object with a method value set to POST (signifying we want the request performed as an HTTP POST request) and a headers object. In that headers object, we pass the standard Accept header to tell the Github API the MIME type we will accept for their response to our request (in this case application/json). If we omit this, Github will return the response using the default url-form-encoded MIME type.

Once this is called, we expect fetch() to return us a JavaScript Promise with the response. To get the response as a JSON object, we take in the response passed to the callback of our .then() method and then call to response.json() to tell fetch to format the response body it received as JSON data (we use async/await here to tell JavaScript to wait on the response from the response.json() function).

With that data on hand, we return it from our function. If all went according to plan, we should get back an object that looks something like this from Github:

{
  access_token: 'gho_abc123456',
  token_type: 'bearer',
  scope: 'repo,user'
}

Next, if we review our main github function for our action, we can see that the next step is to take the resulting object we get from the getAccessTokenFromGithub() function and destructure it, plucking off the access_token property we see in the example response above.

With this, now we have permanent access to this user's repos and user account on Github (completing the OAuth part of the workflow) until they revoke access.

While we're technically done with our OAuth implementation, it's helpful to see the why behind what we're doing. Now, with our access_token we're able to perform requests to the Github API on behalf of our users. Meaning, as far as Github is concerned (and within the limitations of the scopes we requested), we are that user until the user says we aren't and revokes our access.

/api/oauth/github.js

const getUserFromGithub = (access_token = '') => {
  return fetch('https://api.github.com/user', {
    headers: {
      Accept: 'application/json',
      Authorization: `token ${access_token}`,
    },
  }).then(async (response) => {
    const data = await response.json();
    return data;
  }).catch((error) => {
    console.warn(error);
    throw new Error(error);
  });
};

Focusing on our call to getUserFromGithub() the process to make our API request is nearly identical to our access_token request with the minor addition of a new header Authorization. This is another standard HTTP header which allows us to pass an authorization string to the server we're making our request to (in this case, Github's API server).

In that string, following the conventions of the Github API (this part will be different for each API—some require the bearer <token> pattern while others require the <user>:<pass> pattern while still others require a base64 encoded version of one of those two or another pattern), we pass the keyword token followed by a space and then the access_token value we received from the getAccessTokenFromGithub() function we wrote earlier.

To handle the response, we perform the exact same steps we saw above using response.json() to format the response as JSON data.

With that, we should expect to get back a big object describing our user!

We're going to wrap up here. Though we do have another a function call to getReposFromGithub(), we've already learned what we need to understand to perform this request.

Back down in our main github() function, we take the result of all three calls and combine them together on an object we log to our console.

That's it! We now have OAuth2 access to our Github user's account.

Wrapping up

In this tutorial, we learned how to implement an OAuth2 authorization workflow using the Github API. We learned about the difference between different OAuth implementations and looked at an example of initializing a request on the client and then handling a token exchange on the server. Finally, we learned how to take an access_token we get back from an OAuth token exchange and use that to perform API requests on behalf of the user.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode