OAuth2: Authorization Grant Flow using oauth2orize, express 4 and mongoJS

In this blog post I will describe how to implement the authorization grant flow with Node.JS using oauth2orize. If you don’t know about this flow at all check out this blog, which gives a nice introduction and an example using groovy. So, let’s get started.
The first thing we need to do is to register a client. This can be done using a simple registration form, which requires the client to give a name. The following is an example using Jade:

extends layout
block content
    div
      form(action='/client/registration',method='post')
        p
            label(for='name') Name:
               input(id='name',type='text',value='', name='name')
        p
            input(type='submit',value='Sign up')

As you can see there’s only a form taking the name for the client as input.
Next we need to have a function which handles the registration:

req.checkBody('name', 'No valid name is given').notEmpty().len(3, 40)

var errors = req.validationErrors()
if (errors) {
    res.send(errors, 400)
} else {
    var name = req.body['name']
    var clientId = utils.uid(8)
    var clientSecret = utils.uid(20)

    db.collection('clients').findOne({name: name}, function (err, client) {
        if(client) {
            res.send("Name is already taken", 422)
        } else {
            db.collection('clients').save({name: name, clientId: clientId, clientSecret: clientSecret}, function (err) {
                res.send({name: name, clientId: clientId, clientSecret: clientSecret}, 201)
            })
        }
    })
}

In this function the clientId and clientSecret are randomly created alphanumeric strings. These get stored in the DB along with the name.
We also need user data for the oauth service, such that user can later successfully login. In this case we provide a registration form.
Again this is a pretty simple form, which takes the username and password:

extends layout
block content
    div
      form(action='/registration',method='post')
        p
            label(for='username') Username:
               input(id='username', type='text',value='', name='username')
        p
            label(for='password') Password:
               input(id='password', type='password',value='', name='password')
        p
            input(type='submit',value='Sign up')

And this is the function that does the actual registration:

req.checkBody('username', 'No valid username is given').notEmpty().len(3, 40)
req.checkBody('password', 'No valid password is given').notEmpty().len(6, 50)

var errors = req.validationErrors()
if (errors) {
    res.send(errors, 400)
} else {
    var username = req.body['username']
    var password = req.body['password']

    db.collection('users').findOne({username: username}, function (err, user) {
        if(user) {
            res.send("Username is already taken", 422)
        } else {
            bcrypt.hash(password, 11, function (err, hash) {
                db.collection('users').save({username: username, password: hash}, function (err) {
                    res.send({username: username}, 201)
                })
            })
        }
    })
}

For the hashing of the password, bcrypt, which is a very sophisticated hashing algorithm/library, is used.
Now we are able to do the actual authorization process. The first thing we need is a user login:

extends layout
block content
    div
      form(action='/oauth/authorization',method='post')
        p
            label(for='username') Username:
               input(id='username', type='text',value='', name='username')
        p
            label(for='password') Password:
               input(id='password', type='password',value='', name='password')
        p
            input(id='clientId', type='hidden',value='#{clientId}', name='clientId')
            input(id='redirectUri', type='hidden',value='#{redirectUri}', name='redirectUri')
            input(id='responseType', type='hidden',value='#{responseType}', name='responseType')
            input(type='submit',value='Login')

The form is almost the same as with the user registration, except that we have some additional hidden fields in the form. The data for these fields comes from the query parameter passed with the URL. These query parameter must include the clientId, the response type and a redirect URI. The redirect URI is the callback URL, which is used by oauth to redirect back to the client. This could be, for example, http://www.example.org/oauth_callback. Usually the user won’t directly call this login page with the query params, but will send a login request to the client, which will redirect the user to the actual login page of the authorization server with the appropriate params.
To check for a valid login passport is used. The function that reads the user from the database and checks if the password is valid is as follows:

passport.use(new LocalStrategy(
    function(username, password, done) {
        db.collection('users').findOne({username: username}, function(err, user) {
            if (err) { return done(err); }
            if (!user) { return done(null, false); }
            bcrypt.compare(password, user.password, function (err, res) {
                if (!res) return done(null, false)
                return done(null, user)
            })
        })
    }
))

After the user is successful logged in, we directly forward to the authorization endpoint (it is not essential for the flow to redirect here, it would also be possible to call this directly). This one will check if a correct clientId was given and then render a decision dialog. The authorization function looks as follows:

exports.authorization = [
  function(req, res, next) {
    if (req.user) next()
    else res.redirect('/oauth/authorization')
  },
  server.authorization(function(clientId, redirectURI, done) {
    db.collection('clients').findOne({clientId: clientId}, function(err, client) {
      if (err) return done(err)
      // WARNING: For security purposes, it is highly advisable to check that
      // redirectURI provided by the client matches one registered with
      // the server. For simplicity, this example does not. You have
      // been warned.
      return done(null, client, redirectURI)
    })
  }),
  function(req, res) {
    res.render('decision', { transactionID: req.oauth2.transactionID, user: req.user, client: req.oauth2.client })
  }
]

And the decision Jade file like this:

extends layout
block content
    p Hi #{user.username}
      p
          b #{client.name}
          | is requesting access to your account.
        p Do you approve?
        form(action='/decision', method='post')
            p
                input(name='transaction_id', type='hidden', value='#{transactionID}')
            p
                input(type='submit',value='Allow', id='allow')
                input(type='submit',value='Deny', name='cancel', id='deny')

When the user sends the decision to the server, we call the decision function, which evaluates if the user approves the client. If so, an authorization code will be issued. The decision function looks like this:

exports.decision = [
  function(req, res, next) {
    if (req.user) next()
    else res.redirect('/oauth/authorization')
  },
  server.decision()
]

And the function for creating and storing an authorization code like this:

server.grant(oauth2orize.grant.code(function(client, redirectURI, user, ares, done) {
    var code = utils.uid(16)
    var codeHash = crypto.createHash('sha1').update(code).digest('hex')

    db.collection('authorizationCodes').save({code: codeHash, clientId: client._id, redirectURI: redirectURI, userId: user.username}, function(err) {
        if (err) return done(err)
        done(null, code)
    })
}))

In the last step the authorization code is exchanged for an access token. To do that a request with the authorization code and the grant_type set to “authorization_code” must be send. Furthermore, the request must include the clientId and clientSecret. The client information may be send using basic authentication or as part of the body.
The function that handles the request looks as follows:

exports.token = [
    passport.authenticate(['clientBasic', 'clientPassword'], { session: false }),
    server.token(),
    server.errorHandler()
]

The functions for the authentication of the client:
Basic authentication

passport.use("clientBasic", new BasicStrategy(
    function (clientId, clientSecret, done) {
        db.collection('clients').findOne({clientId: clientId}, function (err, client) {
            if (err) return done(err)
            if (!client) return done(null, false)

            if (client.clientSecret == clientSecret) return done(null, client)
            else return done(null, false)
        });
    }
));

Client information as part of the body

passport.use("clientPassword", new ClientPasswordStrategy(
    function (clientId, clientSecret, done) {
        db.collection('clients').findOne({clientId: clientId}, function (err, client) {
            if (err) return done(err)
            if (!client) return done(null, false)

            if (client.clientSecret == clientSecret) return done(null, client)
            else return done(null, false)
        })
    }
))

After the client is successfully authenticated the code is exchanged using the following function:

server.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, done) {
    db.collection('authorizationCodes').findOne({code: code}, function (err, authCode) {
        if (err) return done(err)
        if (!authCode) return done(null, false)
        if (client.clientId !== authCode.clientId) return done(null, false)
        if (redirectURI !== authCode.redirectURI) return done(null, false)

        db.collection('authorizationCodes').remove({code: code}, function(err) {
            if(err) return done(err)
            var token = utils.uid(256)
            var refreshToken = utils.uid(256)
            var tokenHash = crypto.createHash('sha1').update(token).digest('hex')
            var refreshTokenHash = crypto.createHash('sha1').update(refreshToken).digest('hex')

            var expirationDate = new Date(new Date().getTime() + (3600 * 1000))

            db.collection('accessTokens').save({token: tokenHash, expirationDate: expirationDate, userId: authCode.userId, clientId: authCode.clientId}, function(err) {
                if (err) return done(err)
                db.collection('refreshTokens').save({refreshToken: refreshTokenHash, clientId: authCode.clientId, userId: authCode.userId}, function (err) {
                    if (err) return done(err)
                    done(null, token, refreshToken, {expires_in: expirationDate})
                })
            })
        })
    })
}))

The creation of an refresh token, which can be used to create a new access token if the old one is expired, is optional.
Now an access token is retrieved, which can be used to authenticate the user. For that a special passport authentication strategy is used:

passport.use("accessToken", new BearerStrategy(
    function (accessToken, done) {
        var accessTokenHash = crypto.createHash('sha1').update(accessToken).digest('hex')
        db.collection('accessTokens').findOne({token: accessTokenHash}, function (err, token) {
            if (err) return done(err)
            if (!token) return done(null, false)
            if (new Date() > token.expirationDate) {
                db.collection('accessTokens').remove({token: accessTokenHash}, function (err) { done(err) })
            } else {
                db.collection('users').findOne({username: token.userId}, function (err, user) {
                    if (err) return done(err)
                    if (!user) return done(null, false)
                    // no use of scopes for no
                    var info = { scope: '*' }
                    done(null, user, info);
                })
            }
        })
    }
))

Lastly, if an authentication token is expired, it is possible to create a new one if there is a refresh token given.

server.exchange(oauth2orize.exchange.refreshToken(function (client, refreshToken, scope, done) {
    var refreshTokenHash = crypto.createHash('sha1').update(refreshToken).digest('hex')

    db.collection('refreshTokens').findOne({refreshToken: refreshTokenHash}, function (err, token) {
        if (err) return done(err)
        if (!token) return done(null, false)
        if (client.clientId !== token.clientId) return done(null, false)

        var newAccessToken = utils.uid(256)
        var accessTokenHash = crypto.createHash('sha1').update(newAccessToken).digest('hex')

        var expirationDate = new Date(new Date().getTime() + (3600 * 1000))

        db.collection('accessTokens').update({userId: token.userId}, {$set: {token: accessTokenHash, scope: scope, expirationDate: expirationDate}}, function (err) {
            if (err) return done(err)
            done(null, newAccessToken, refreshToken, {expires_in: expirationDate})
        })
    })
}))

The full example project can be found at: https://github.com/reneweb/oauth2orize_authorization_grant_example

Advertisements

3 thoughts on “OAuth2: Authorization Grant Flow using oauth2orize, express 4 and mongoJS

  1. Hi – Thanks for the demo. I’m going through the code to learn the flows. I seem to have a problem in the app.post(‘/login’… handler when a authenticated user is redirected using res.redirect(‘/oauth/authorization?response_type=code&client_id=’ + req.body.clientId + ‘&redirect_uri=’ + req.body.redirectUri).
    Passport is support to save the user object on req.user, which is available in the app.post(‘/login’ function. However, after the redirect (a 302 redirect in the browser), the req.user is not available in app.get(‘/oauth/authorization’, oauth.authorization). In oauth.js line 84, exports.authorization = [
    function(req, res, next) {
    if (req.user) next()
    else res.redirect(‘/login’) << this always happens because req.user is always missing

    I'm wondering if I have a session issue? How is req.user supposed to survive the redirect to be used in app.get('/oauth/authorization'?
    Thanks in advance! Kevin

    • Hi,

      sorry for the late reply – not sure if this is still relevant for you, but I will try to answer your question anyway. So there are a few things that can be a little confusing here, that I will try to clarify. First of all, to answer your question, you do have a session issue. I don’t know what exactly, but you should check that your auth.js file contains the passport.serializeUser and passport.deserializeUser functions (like here: https://github.com/reneweb/oauth2orize_authorization_grant_example/blob/master/auth.js) and that you enabled the session in express as well as in passport in your app.js.

      Now, it is actually not essential for this oauth flow to redirect after a successful login. The authorization started when calling the login endpoint and ends when returning the authorization code. This all happens in the domain of the authorization server, which is why the redirect is not essential. However, in practice the login page and the oauth logic usually (at least in larger environments) dont live in the same place, which is why often redirects are used.

      Furthermore, since the oauth authorization starts when calling the /login endpoint it is usually the one called /oauth/authorize and not the one I called /oauth/authorize in the example (this could be anything since it would be an optional internal redirect within the authorization server).

      Hope this clear up a few things!

      Rene

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s