Writing an OAuth flow from scratch in NodeJS

Dominic Tobias
Jan 7, 2019 · 3 min read

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.

Image for post
Image for post

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

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,
};
res.redirect(`https://github.com/login/oauth/authorize?${querystring.stringify(query)}`);
});

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

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}`);
return;
}
// 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',
code,
state,
},
});
// We have successfully authenticated the user!
const accessToken = resp.body.access_token;
// ... Do stuff with accessToken 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.
res.redirect(`https://your-react-app.com/auth?token=${jwtToken}`);

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.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store