07 Oct 2019
cat: Express, Node
14 Comments

Express JS OAuth 2 Server using oauth2-server package

Introduction

OAuth2-Server is quite a popular package for implementing OAuth authentication for Node JS but the examples provided on express-oauth-server on GitHub is outdated and does not work. In this tutorial we will create an OAuth 2 server using Express JS and test using postman. For database we will use Mongo DB with the help of Mongoose JS. The code for this tutorial can be downloaded from the GitHub repository.

Installation

To create a project template, we can use express-generator.

express -v hbs -c sass –git
npm install
npm i express-oauth-server mongoose –save

express-oauth-server is just a wrapper for the methods that oauth2-server provides. This also installs the oauth2-server package as a dependency.

Connecting to Database

For this tutorial we will use Mongo DB as a database but you can change this of course. First I will store my Mongo DB connection in a separate JavaScript file which I will call env.js.

module.exports = {
    isProduction: false || process.env.isProduction,
    mongoDbUrl: 'mongodb://localhost/oauth_server' || process.env.mongoDbUrl,
    salt: 'a5828e9d6052dc3b14a93e07a5932dd9' || process.env.salt
};

This is not required but it is helpful to have a central place to put environment or application variables. I also added isProduction and salt which I will use later.

To connect to Mongo DB, we can add this on app.js on the upper part of the code. Just make sure it is above where you actually need the connection.

// other codes are remove for brevity
app.use(express.static(path.join(__dirname, 'public')));

// Database
if (env.isProduction) {
    mongoose.connect(process.env.MONGODB_URI);
} else {
    mongoose.connect('mongodb://localhost/oauth', { useNewUrlParser: true });
    mongoose.set('debug', true);
}
mongoose.set('useCreateIndex', true);

let db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB error: '));
db.once('open', console.log.bind(console, 'MongoDB connection successful'));

Models

OAuth2-server requires an object or model with specific methods that it use internally. This model will also need the application’s user model so we need to create that too. Refer to https://oauth2-server.readthedocs.io/en/latest for the model specification.

User Model

This is a typical user model look like with helper methods for setting and validating password.

const mongoose = require('mongoose');
const crypto = require('crypto');
const env = require('../env');

let UserSchema = new mongoose.Schema({
    firstName: { type: String },
    lastName: { type: String },
    username: { type: String, unique: true },
    password: { type: String },
    email: { type: String, unique: true },
    verificationCode: { type: String },
    verifiedAt: { type: Date },
}, {
    timestamps: true,
});

UserSchema.methods.validatePassword = function (password) {
    let _password = crypto.pbkdf2Sync(password, env.salt, 10000,
        32, 'sha512').toString('hex');
    return this.password === _password;
};

UserSchema.methods.setPassword = function (password) {
    this.password = crypto.pbkdf2Sync(password, env.salt, 10000,
        32, 'sha512').toString('hex');
};

mongoose.model('User', UserSchema, 'users');

Access Token Model

For access token, authorization code, and client models we can just write it on the same JavaScript file. In my case I will put all of them on models/oauth.js.

Access tokens are related to the client and can be owned by a user for authorization and password grants.

let OAuthAccessTokenModel = mongoose.model('OAuthAccessToken', new mongoose.Schema({
    user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
    client: { type: mongoose.Schema.Types.ObjectId, ref: 'OAuthClient' },
    accessToken: { type: String },
    accessTokenExpiresAt: { type: Date },
    refreshToken: { type: String },
    refreshTokenExpiresAt: { type: Date },
    scope: { type: String }
}, {
    timestamps: true
}), 'oauth_access_tokens');

Authorization Code Model

We need to store the authorization code for authorization code flow. OAuth2-server also requires the user and client model.

let OAuthCodeModel = mongoose.model('OAuthCode', new mongoose.Schema({
    user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
    client: { type: mongoose.Schema.Types.ObjectId, ref: 'OAuthClient' },
    authorizationCode: { type: String },
    expiresAt: { type: Date },
    scope: { type: String }
}, {
    timestamps: true
}), 'oauth_auth_codes');

Client Model

Last but not the least is the client model. User model is also needed in this case since clients has to have a user.

let OAuthClientModel = mongoose.model('OAuthClient', new mongoose.Schema({
    user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
    clientId: { type: String },
    clientSecret: { type: String },
    redirectUris: { type: Array },
    grants: { type: Array },
}, {
    timestamps: true
}), 'oauth_clients');

Now we can implement the methods required by OAuth2-server. These will be simple query and insert using Mongoose JS.

module.exports.getAccessToken = async (accessToken) => {

    let _accessToken = await OAuthAccessTokenModel.findOne({ accessToken: accessToken })
        .populate('user')
        .populate('client');

    if (!_accessToken) {
        return false;
    }

    _accessToken = _accessToken.toObject();

    if (!_accessToken.user) {
        _accessToken.user = {};
    }
    return _accessToken;
};

module.exports.refreshTokenModel = (refreshToken) => {
    return OAuthAccessTokenModel.findOne({ refreshToken: refreshToken })
        .populate('user')
        .populate('client');
};

module.exports.getAuthorizationCode = (code) => {
    return OAuthCodeModel.findOne({ authorizationCode: code })
        .populate('user')
        .populate('client');
};

module.exports.getClient = (clientId, clientSecret) => {
    let params = { clientId: clientId };
    if (clientSecret) {
        params.clientSecret = clientSecret;
    }
    return OAuthClientModel.findOne(params);
};

module.exports.getUser = async (username, password) => {
    let UserModel = mongoose.model('User');
    let user = await UserModel.findOne({ username: username });
    if (user.validatePassword(password)) {
        return user;
    }
    return false;
};

module.exports.getUserFromClient = (client) => {
    // let UserModel = mongoose.model('User');
    // return UserModel.findById(client.user);
    return {};
};

module.exports.saveToken = async (token, client, user) => {
    let accessToken = (await OAuthAccessTokenModel.create({
        user: user.id || null,
        client: client.id,
        accessToken: token.accessToken,
        accessTokenExpiresAt: token.accessTokenExpiresAt,
        refreshToken: token.refreshToken,
        refreshTokenExpiresAt: token.refreshTokenExpiresAt,
        scope: token.scope,
    })).toObject();

    if (!accessToken.user) {
        accessToken.user = {};
    }

    return accessToken;
};

module.exports.saveAuthorizationCode = (code, client, user) => {
    let authCode = new OAuthCodeModel({
        user: user.id,
        client: client.id,
        authorizationCode: code.authorizationCode,
        expiresAt: code.expiresAt,
        scope: code.scope
    });
    return authCode.save();
};

module.exports.revokeToken = async (accessToken) => {
    let result = await OAuthAccessTokenModel.deleteOne({
        accessToken: accessToken
    });
    return result.deletedCount > 0;
};

module.exports.revokeAuthorizationCode = async (code) => {
    let result = await OAuthCodeModel.deleteOne({
        authorizationCode: code.authorizationCode
    });
    return result.deletedCount > 0;
};

Here are some key points.

  • getUserFromClient – Only used on client credentials grant and we return empty object. This is so when securing a route like user registration, forgot-password, and etc. we don’t get any user from the request and think someone has actually logged in. For resources like account profile where we need a user to login, we user authorization grant or password grant.
  • saveToken – We made the the user optional for client credentials grant. Then we convert the document to object to set accessToken.user to an empty object since Mongoose only lets you set related columns to actual document. We also can’t just leave it to just null since OAuth2-server want’s the user property a truthy value.
  • getAccessToken – On this method we just consider the client credentials that can have empty user.

OAuth Routes

We will need to create three routes that will be used for authorization, client credentials, and password credentials grants. One of the route will also render a login page that when logged-in successfully will redirect the user back to their redirect URL for authorization code grant.

We can write the routes on routes/oauth.js.

const router = require('express').Router();
const OAuthServer = require('express-oauth-server');
const OAuthModel = require('../models/oauth');
const mongoose = require('mongoose');

let oauth = new OAuthServer({
    model: OAuthModel,
    debug: true
});

router.post('/oauth/access_token', oauth.token({
    requireClientAuthentication: { authorization_code: false }
}));

router.get('/oauth/authenticate', async (req, res, next) => {
    return res.render('authenticate')
});

router.post('/oauth/authenticate', async (req, res, next) => {

    let UserModel = mongoose.model('User');
    req.body.user = await UserModel.findOne({ username: req.body.username });

    return next();
}, oauth.authorize({
    authenticateHandler: {
        handle: req => {
            return req.body.user;
        }
    }
}));

module.exports = router;
  • POST  /oauth/access_token – We set requireClientAuthentication.authorization_code to false so we don’t have to pass the client secret when getting the access token for authorization code grant.
  • GET /oauth/authenticate – We just render a login page that posts to our next route.
  • POST /oauth/authenticate – We login the user and pass it to OAuth2-server's authorize.

Here is the code for the login page that is named authenticate.hbs.

<div class="row">
    <div class="col-md-6 offset-md-3">
        <form method="POST">
            <div class="form-group">
                <label for="username">
                    Username
                    <span class="text-danger">*</span>
                </label>
                <input name="username"
                       id="username"
                       type="text"
                       class="form-control"
                       value="johndoe">
            </div>
            <div class="form-group">
                <label for="password">
                    Password
                    <span class="text-danger">*</span>
                </label>
                <input name="password"
                       id="password"
                       type="text"
                       class="form-control"
                       value="@Test123">
            </div>
            <button class="btn btn-primary">
                Login
            </button>
        </form>
    </div>
</div>

There is no “action” attribute since the goal is to post it to the same URL which is /oauth/authenticate.

We can now finally import our models and routes to app.js. This will be all the changes to app.js.

// Database
if (env.isProduction) {
    mongoose.connect(process.env.MONGODB_URI);
} else {
    mongoose.connect('mongodb://localhost/oauth', { useNewUrlParser: true });
    mongoose.set('debug', true);
}
mongoose.set('useCreateIndex', true);

let db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB error: '));
db.once('open', console.log.bind(console, 'MongoDB connection successful'));

require('./models/user');
require('./models/oauth');

// Routes
app.use(require('./routes/oauth'));

Trying it out

Here are some screenshots when testing using Postman. I inserted a sample user and an oauth client for that user. The sample code in references section at the bottom includes a registration page.

Authorization Code Grant

Express JS OAuth 2 Server using oauth2-server package Express JS OAuth 2 Server using oauth2-server package Express JS OAuth 2 Server using oauth2-server package

Password Credentials Grant

Express JS OAuth 2 Server using oauth2-server package Express JS OAuth 2 Server using oauth2-server package

Client Credentials Grant

Express JS OAuth 2 Server using oauth2-server package Express JS OAuth 2 Server using oauth2-server package

Securing a Route

Lastly, we will try securing a page. In our routes file we need to get an instance of the OAuth2-server so we can access its authenticate method which is a middleware.

const router = require('express').Router();
const OAuthServer = require('express-oauth-server');
const OAuthModel = require('../models/oauth');

let oauth = new OAuthServer({
    model: OAuthModel,
    useErrorHandler: true,
    debug: true
});

router.use('/secured/profile', oauth.authenticate(), (req, res) => {
    return res.render('secured', { token: JSON.stringify(res.locals) });
});

secured.hbs

<h1>This page is secured.</h1>
<code>{{{token}}}</code>

Screenshots

Authorization Code Grant

User object is available.

Express JS OAuth 2 Server using oauth2-server package

Password Credentials Grant

User object is available.

Express JS OAuth 2 Server using oauth2-server package

Client Credentials Grant

User object is not available.

Express JS OAuth 2 Server using oauth2-server package

That is all for this tutorial. Thanks for reading!

References


  1. Thank you very much! You give me a valuable piece of the puzzle

  2. Javier Constanzo


    Thank you, you’re amazing


  3. When trying to login, I get “missing parameter: client id”


    • It could be that somehow your client_id is not included or missing from the request.

      {
          client_id:'api'
          redirect_uri:'http://localhost:3000/oauth/callback'
          response_type:'code'
          scope:'post.read post.write'
          state:'1234'
      }

      Another thing you can do to debug it is put breakpoints on the handlers in the following lines:
      – node_modules/express-oauth-server/node_modules/oauth2-server/lib/handlers/authorize-handler.js line 162
      – node_modules/express-oauth-server/node_modules/oauth2-server/lib/handlers/token-handler.js line 120

      These are the lines that are throwing these errors. throw new InvalidRequestError('Missing parameter: `client_id`');
      Then you can check the request variable.

      I hope that helps 🙂


  4. {“error”:”invalid_request”,”error_description”:”Missing parameter: `state`”}

    How to fix?


  5. Another question: By any chance, would you have an example of models with Sequelize (MySQL)?


  6. Thanks your tutorial. It works when I’m using the POSTMAN, but failed when using in my ‘passport-oauth2’. It will show the error: “{“statusCode”:401,”message”:”Unauthorized”}” in the last step. Here is my config in ‘passport-oauth2’:

    authorizationURL : ‘http://localhost:3000/oauth/authenticate?client_id=api&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fdiscord&response_type=code&scope=read’,
    tokenURL : ‘http://localhost:3000/oauth/access_token’,
    clientID : ‘api’,
    clientSecret : ‘secret’,
    callbackURL : ‘http://localhost:4000/discord’,
    scope : ‘read’,
    state : true

    I’m using the same config OAuth the discord app, and it works correctly. It makes me so frustrated…


    • Mmm. Sorry I have not checked you particular issue but what you can do is check the logs in postman and see how the request differs from your web app’s request.

Post A Comment