OAuth is a protocol for allowing one application controlled access to a user's account on another application. It is commonly used for two purposes:
- Single sign on, like via Facebook login
- Apps built to make it easier to deal with other apps, like Buffer for scheduling tweets on Twitter
In this article, I'll describe how to build a minimal OAuth server
with Node.js and Express, no OAuth modules allowed.
The only exception is Matt Mueller's excellent oauth-open
package for displaying an OAuth popup on the client side to verify that we actually have a working OAuth setup.
The OAuth Flow
Your standard web OAuth 2.0 flow has 3 steps:
- Your app client opens a dialog that displays a dialog that asks the user to authorize your app. The dialog is usually on a different domain, like Facebook's OAuth login dialog.
- The dialog redirects back to your app client's domain with an auth code in the query string. An auth code is a short-lived code that you can exchange for a long-lived access token.
- Your app pulls the
code
parameter from the query string, and makes a POST request to the authorizing app's server with the access code. The authorizing app's server verifies the access code and sends back an access token your app can use for authorization going forward.
For the purposes of this example, there's 2 components involved in the OAuth flow:
- The client app. You can think of this as your app that's trying to get access to data from the authorizing app.
- The authorizing app. You can think of this as Facebook, Google, Twitter, or some other app that your client app is trying to access on the user's behalf.
Client App Implementation
Note that this code is meant as a minimal didactic example. The below code is most definitely not a production-grade OAuth authorization server. Don't copy/paste it into your prod app.
First, let's take a look at the client app to see what endpoints the authorization server needs to implement. The client app server's entry point is a simple static server listening on port 3000:
'use strict';
const express = require('express');
run().catch(err => console.log(err));
async function run() {
const app = express();
app.use(express.static('./'));
await app.listen(3000);
console.log('Listening on port 3000');
}
The client app has one file, index.html
. This file is responsible for opening an OAuth dialog, exchanging the auth code for an access token, and making an HTTP request to a secure endpoint using the access token as authorization. The
auth server will run on http://localhost:3001
.
<html>
<body>
<div id="content"></div>
<script type="text/javascript" src="https://codebarbarian-images.s3.amazonaws.com/open.dist.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="text/javascript">
// Step 1: open an OAuth dialog
oauthOpen('http://localhost:3001/oauth-dialog.html', async (err, code) => {
// Step 2: exchange the code for an access token
const resToken = await axios.post('http://localhost:3001/token', { code: code.code });
// Step 3: use the access token to make a request to a secure
// endpoint and display some data
const res = await axios.get('http://localhost:3001/secure', {
headers: { authorization: resToken.data['access_token'] }
});
document.querySelector('#content').innerHTML =
`The secret answer is ${res.data.answer}`;
});
</script>
</body>
</html>
The https://codebarbarian-images.s3.amazonaws.com/open.dist.js
file
in the above example is a Webpack bundle of the below script. I pre-compiled that script for convenience so the code in this
article doesn't require a compiler.
window.oauthOpen = require('oauth-open');
The client also needs an oauth-callback.html
file. The oauth-callback.html
file doesn't need to do anything, the oauthOpen()
function takes care of pulling out the auth code. Here's a minimal oauth-callback.html
:
<html>
<body>
<div>Authorized</div>
</body>
</html>
Auth Server Implementation
The auth server needs 4 endpoints:
- An OAuth dialog that asks the user to authorize the client app.
- A route that generates an auth code and redirects to the client app.
- A route to exchange an auth code for an access token.
- A "secure" endpoint that only responds if it is given a valid access token via the
Authorization
HTTP header.
Since the auth server will run on a different domain than the client app, it also needs CORS for the access token and secure endpoints in order to avoid the browser throwing errors about cross-origin requests. However, it is important to protect the endpoint for getting an auth code secure from cross-origin requests because of CSRF attacks.
The flow starts when the user opens the OAuth dialog. In this example,
the authorization server has a static oauth-dialog.html
file that
shows a single button the user can click to authorize the client app.
Clicking the button redirects to a /code
route that is responsible
for generating an auth code and redirecting to the client app.
<html>
<body>
<div>Authorize OAuth Test App?</div>
<form action="/code" method="POST">
<button type="submit">OK</button>
</form>
</body>
</html>
Below is the full auth-server.js
file:
'use strict';
const cors = require('cors');
const express = require('express');
run().catch(err => console.log(err));
async function run() {
const app = express();
// Store the auth codes and access tokens in memory. In a real
// auth server, you would store these in a database.
const authCodes = new Set();
const accessTokens = new Set();
app.use(express.json());
// Generate an auth code and redirect to your app client's
// domain with the auth code
app.post('/code', (req, res) => {
// Generate a string of 10 random digits
const authCode = new Array(10).fill(null).map(() => Math.floor(Math.random() * 10)).join('');
authCodes.add(authCode);
// Normally this would be a `redirect_uri` parameter, but for
// this example it is hard coded.
res.redirect(`http://localhost:3000/oauth-callback.html?code=${authCode}`);
});
app.options('/token', cors(), (req, res) => res.end());
app.options('/secure', cors(), (req, res) => res.end());
// Verify an auth code and exchange it for an access token
app.post('/token', cors(), (req, res) => {
if (authCodes.has(req.body.code)) {
// Generate a string of 50 random digits
const token = new Array(50).fill(null).map(() => Math.floor(Math.random() * 10)).join('');
authCodes.delete(req.body.code);
accessTokens.add(token);
res.json({ 'access_token': token, 'expires_in': 60 * 60 * 24 });
} else {
res.status(400).json({ message: 'Invalid auth token' });
}
});
// Endpoint secured by auth token
app.get('/secure', cors(), (req, res) => {
const authorization = req.get('authorization');
if (!accessTokens.has(authorization)) {
return res.status(403).json({ message: 'Unauthorized' });
}
return res.json({ answer: 42 });
});
// Serve up `oauth-dialog.html`
app.use(express.static('./'));
await app.listen(3001);
console.log('Listening on port 3001');
}
Note that the /code
endpoint, as written, is vulnerable to cross-site request forgery attacks. A malicious
website could POST a form to the /code
endpoint and that would trigger the
OAuth flow without the user's knowledge. You can use a module like
csurf to generate CSRF tokens.
Moving On
OAuth may seem baffling to beginners, but the process of implementing an OAuth server is simple once you understand the OAuth flow. All you need is a dialog, an endpoint to get an auth code, and an endpoint to exchange an auth code for an access token. Once you give a user an access token, they are effectively "logged in" to the authorizing app.
If you're looking to implement a real OAuth server, the next step is to store the auth codes and access tokens in a database. For more sophisticated apps, you may want to add support for OAuth scopes, which inform the user what permissions the client app has, like whether the client app has permission to tweet on your behalf.