Building an OAuth2 provider in Node.js
Last week I needed a Node.js web application to provide OAuth 2 access tokens under the
authorization_code
grant. I choose to use
the node-oauth2-server
, mainly because it is
reasonably popular, does not require passport
and it looks well-written.
Getting the integration to fully work was a bit of a struggle, since the library is somewhere between version 2.x and 3.x. Or otherwise stated, the library is version 3.x and the documentation is version 2.x. So I decided to write down what I needed to know, it might just help someone else.
In case you’re not familiar with OAuth 2.0, it’s a framework that enabled third-parties to obtain (limited) access to an HTTP service. It’s well described in RFC 6749.
Setup
The HTTP service is built with the Express web framework. There’s
the express-oauth-server
adapter to
integrate node-oauth-server
in an Express app.
The examples in this article are based on Express, though you should be able to follow along when using a different web framework.
First we must initialize the OAuth server. Don’t worry about the Model
for now, I’ll get back
to it in a bit.
const OAuthServer = require("express-oauth-server");
const oauth = new OAuthServer({ model: Model });
Then we mount the authorize
and token
endpoints, both on POST
. A GET
request on
authorize
should present your user (resource owner) with a form to authorize or deny the
third-party. Note that you must ensure that all OAuth query parameters on the GET
request
(client_id
, redirect_uri
, et al) are also available on the POST
request.
app.get("/oauth/authorize", function(req, res) {
// render an authorization form
});
app.post("/oauth/authorize", oauth.authorize());
app.post("/oauth/token", oauth.token());
Then we can require a valid OAuth token on secure areas.
app.use("/secure", oauth.authenticate());
Model
The toughest part was providing the correct model
to the OAuthServer
. The documentation nor
the examples of this model
were apt, so mostly I reverse engineered this from the source.
For the authorization_code
and refresh_token
grants the model
must implement the
functions listed below.
Function | Signature |
getAccessToken |
(accessToken): Token |
getAuthorizationCode |
(authorizationCode): AuthorizationCode |
getClient |
(clientId, ?clientSecret): Client |
getRefreshToken |
(refreshToken): Token |
revokeAuthorizationCode |
(AuthorizationCode): Boolean |
revokeToken |
(Token): Boolean |
saveAuthorizationCode |
(AuthorizationCode, Client, User): AuthorizationCode |
saveToken |
(Token, Client, User): Token |
validateScope |
(scope): Boolean |
getClient
gets the clientSecret
only when the client authenticates (i.e. when requesting or
refreshing a token). When the clientSecret
is provided getClient
must validate it.
Note that any function may return its result wrapped in a Promise
.
The data structures used by these functions are:
Token: {
accessToken: String,
accessTokenExpiresAt: Date,
refreshToken: String,
refreshTokenExpiresAt: Date,
user: User
}
AuthorizationCode: {
code: String,
scope: String,
expiresAt: Date,
redirectUri: String,
client: Client,
user: User
}
Client: {
id: String, // client_id
clientSecret: String,
grants: [ String ],
redirectUris: [ String ]
}
User: {
id: String
}
By default the generated tokens are the SHA1 hash of 256 random bytes. In case you want
different tokens, you can implement generateAccessToken
, generateAuthorizationCode
, and
generateRefreshToken
in the model (with signature (): String
).
Mapping custom sessions to users
In case the regular authentication flow or your web application does not use bearer tokens
(e.g. when using session cookies), you need to implement your own handler to get the current
user when POST
ing to authorize
. This is a function handle
that maps a Request
to a
User
(see above for its definition).
For example, load the User
from your database by the sessionid
:
function loadCurrentUser(req) {
return db.getUserBySessionId(req.session.sessionid);
}
Then pass it in the authenticateHandler
:
app.post("/oauth/authorize", oauth.authorize({
authenticateHandler: {
handle: loadCurrentUser
}
}));
Final thoughts
node-oauth2-server
is quite stable and while working my way through I didn’t run into any bugs I couldn’t fix by changing my model
. Its assertions help a long way with debugging.
The lack of accurate documentation is made up for by its well-structured source code. You just have to read the source, Luke :)