Writing an OAuth flow from scratch in NodeJS

When you search online OAuth seems intimidating; you’ll find lots of useless flow diagrams, and packages like PassportJS make it seem complicated because configuration is challenging as it is.

In this express example we’ll authenticate with GitHub, but the same process applies to all OAuth providers.

Step 1: Redirect the user to the providers auth access page

We could do this directly, but going to our API first allows us to set cookies so that we can share state between the auth request and success responses.

This is important for two reasons:

  1. We can pass the recommended but optional “state” query param to the OAuth provider which prevents Cross-Site-Request-Forgery
  2. (Optional) We can pass custom state from our app via query params, save it in cookies, and do something with it in the success callback. Great for when you want to customize the path to return the user to for example.
app.get('auth/github', (req, res) => {
const csrfState = Math.random().toString(36).substring(7);
res.cookie('csrfState', csrfState, { maxAge: 60000 });
  const query = {
scope: 'read:user',
client_id: 'YOUR_APP_CLIENT_ID',
state: csrfState,

First we are creating a random state string and saving it to a cookie to compare in the success callback. Then we are redirecting the user to the provider’s auth page, with the scope of the access we want, and the client ID we were given when creating an OAuth app on the provider’s website.

Step 2: Handle the redirect back from the OAuth provider

Once the user has authorized your app, the provider will redirect back to the Callback URL which you set on their site when creating your OAuth app (in this example we assume you set it to auth/github/calllback).

app.get('auth/github/callback', async (req, res) => {
const { code, state } = req.query;
const { csrfState } = req.cookies;
  // Make sure our saved csrfState string matches the one sent back.
if (state && csrfState && state !== csrfState) {
res.status(422).send(`Invalid state: ${csrfState} != ${state}`);
  // Make POST back to OAuth provider with the `code` and
// receive an access token back.
const res = await got.post('https://github.com/login/oauth/access_token', {
json: true,
body: {
client_id: 'YOUR_APP_CLIENT_ID',
client_secret: 'YOUR_APP_CLIENT_SECRET',
  // We have successfully authenticated the user!
const accessToken = resp.body.access_token;
  // ... Do stuff with authToken or save it for later use.

That’s it — a full OAuth flow. I have purposefully left out the last part as it’s implementation specific — perhaps you want to create a user by fetching the user’s email and other details, or perhaps you just wanted the accessToken to perform API requests with the user’s credentials.

Don’t fear the last part, it’s really not complicated! For example in an auth login or sign up flow you would do this:

// Fetch user email with accessToken from provider.
const email = await fetchEmailFromGitHub(accessToken);
// Get or create user on your database, return the user ID:
const user = await updateOrCreateUser(email);
// Create a JSON web token.
const token = jwt.sign({ id: user.id }, 'YOUR_SECRET_JWT_KEY', { expiresIn: '30 days' });
// Redirect to your app, your app should save the token to send
// with requests, and redirect the user to e.g. /dashboard.

The flexibility you get from implementing this yourself is why I like it; not every auth flow is a typical login or sign up one! This way you can share custom state via cookies, redirect to whatever path you want even dynamic ones etc.

I hope this helped to demystify OAuth a little.