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.
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:
- Undocumented or poorly documented parameters that need to be passed in the HTTP
headers
, queryparams
, orbody
. - Undocumented or poorly documented response types that need to be passed in the HTTP
headers
. For example, some APIs may require theAccept
header being set toapplication/json
in order to get back a response in a JSON format. - Bad example code in the documentation.
- 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
.
On this page, you will need to provide three things:
- A name for your OAuth application. This is what Github will display to users when they confirm your access to their account.
- A homepage URL for your app (this can just be a dummy URL for testing).
- 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.
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:
client_id
here is assigned to the value ofjoystick.settings.public.github.client_id
that we set in oursettings-development.json
file earlier.scope
set equal to two "scopes" that grant specific permissions to theaccess_token
we get from Github for this user. Here, we're using therepo
anduser
(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:
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:
- Exchange the
code
that we get from Github for a permanentaccess_token
. - Get the user associated with that
access_token
from the Github API. - 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 req
uest 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.