Implementing OAuth authentication

As we did for Basic Auth, we are going to build a server-side implementation of the OAuth2 protocol. As the Backbone App and Server App are both built by us, the best grant type to choose is Resource Owner Password Credentials Grant.

A difference from Basic Auth is that OAuth2 needs to add an endpoint that is used to issue access and refresh tokens. As described in RFC-6749, the requests made to this endpoint should include the following:

The client makes a request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded":

grant_type: REQUIRED. Value MUST be set to "password".

username: REQUIRED. The resource owner username.

Password: REQUIRED. The resource owner password.

A valid request will look as shown in the following:

POST /api/oauth/token HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=john&password=doe

Then, the server will respond with a valid access token, an optional refresh token, and a token type; it could contain additional values, as follows:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
    "access_token":"2YotnFZFEjr1zCsicMWpAA",
    "token_type":"example",
    "expires_in":3600,
    "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
    "example_parameter":"example_value"
}

The token_type value tells the client about the kind of token that was issued, in our case, it is Bearer. We can start the implementation by creating the necessary functions in order to issue authorization tokens:

function authorize(data, callback) {
  var grantType = data.grant_type;
  var username = data.username;
  var password = data.password;

  if (grantType !== 'password') {
    return callback({error: 'invalid_grant'});
  }

  if (!username || !password) {
    return callback({error: 'invalid_request'});
  }

  if (username === 'john' && password === 'doe') {
    issueAuthorization(username, callback);
  } else {
    callback({error: 'invalid_grant'});
  }
}

As specified in the RFC document, if the grant type is not supported, then we should respond with an invalid_grant error; and if a parameter is missing in the request, then we should respond with an invalid_request error.

If the username and password coincide, then we can issue an authorization token:

const DEFAULT_EXPIRATION_TIME = 3600; // seconds (1 hour)

// ...

function issueAuthorization(username, callback) {
  var accessToken = generateToken();
  var refreshToken = generateToken();
  var token = {
    access_token: accessToken,
    token_type: 'Bearer',
    expires_in: DEFAULT_EXPIRATION_TIME,
    refresh_token: refreshToken
  };

  saveValidToken(token, username);
  callback(token);
}

The generated tokens are just a random string generated with the generateToken() function, as follows:

const TOKEN_LENGTH = 20;

// ...

function generateToken() {
return crispy.base32String(TOKEN_LENGTH);
}

These tokens should be stored somewhere in order to be validated for future requests. For simplicity, in this book, we will store the tokens in memory objects; however, you can use a databases such as Redis for real projects:

var validTokens = {};
var refreshTokens = {};

// ...

function saveValidToken(token, username) {
  var tokenCopy = _.clone(token);
  tokenCopy.username = username;

  validTokens[token.access_token] = tokenCopy;
  refreshTokens[token.refresh_token] = tokenCopy;

  setTimeout(function() {
    expireToken(tokenCopy.access_token);
  }, DEFAULT_EXPIRATION_TIME * 1000);
}

function expireToken(token) {
  delete validTokens[token];
}

The validTokens and refreshTokensare hash tables store the tokens. The tokens in validTokens should be removed after the TTL (Time to live) expires, the setTimeout() call will ensure that these items are automatically removed.

To validate whether a user is authenticated, we just need to check whether the token is active in the validTokenshash table, as follows:

function authenticate(token, callback) {
  if (_.has(validTokens, token)) {
    callback({valid: true, token: validTokens[token]});
  } else {
    callback({valid: false, token: null});
  }
}

With the function that is described in this section, it is possible to implement OAuth2 in our Contacts App project. Let's add a route in order to generate the access tokens and add a middleware to protect the resources, as follows:

var controller = require('./controller');
var auth = require('./oauth2Middleware);

module.exports = routes = function(server) {
  server.post('/api/oauth/token', auth.authenticate);
  server.post('/api/contacts', auth.requireAuthorization,
    controller.createContact);
  server.get('/api/contacts', auth.requireAuthorization,
    controller.showContacts);
  server.get('/api/contacts/:contactId',
    auth.requireAuthorization, controller.findContactById);
  server.put('/api/contacts/:contactId',
    auth.requireAuthorization, controller.updateContact);
  server.delete('/api/contacts/:contactId',
    auth.requireAuthorization, controller.deleteContact);
  server.post('/api/contacts/:contactId/avatar',
    auth.requireAuthorization, controller.uploadAvatar);
};

The oauth2Middleware module provides the requireAuthorization() middleware and the authenticate()authentication handler as described in the following:

module.exports = {
  authenticate(req, res) {
    authorize(req.body || {}, _.bind(res.json, res));
  }
}

To issue a new token, you need to call the authorize() function, which returns a valid OAuth2 response as specified in the RFC document:

requireAuthorization(req, res, next) {
  var authorization = req.headers.authorization || '';

  if (!authorization) {
    return res.sendStatus(401);
  }

  var splitValues = authorization.split(' ');
  var tokenType = splitValues[0];
  var token = splitValues[1];

  if (!tokenType || tokenType !== 'Bearer' || !token) {
    return res.sendStatus(401);
  }

  authenticate(token, function(response) {
    if (response.valid) {
      next();
    } else {
      return res.sendStatus(401);
    }
  });
}

The requireAuthorization() middleware is used to protect the resources with our OAuth2 protocol implementation. The middleware splits the token in two parts: the token type and the token itself; it verifies whether the token type and its existence in the active access tokens list is valid.

In the Backbone App, we can reuse the objects that we made for the Basic Auth protocol; however, we have to make small changes. In the LoginView object, you should change the url request to /api/oauth/token and change the method to POST, as follows:

class LoginView extends Common.ModelView {
  // ...

  makeLogin(event) {
    event.preventDefault();

    var username = this.$el.find('#username').val();
    var password = this.$el.find('#password').val();

    Backbone.$.ajax({
      method: 'POST',
      url: '/api/oauth/token',
      data: {
        grant_type: 'password',
        username: username,
        password: password
      },
      success: response => {
        var App = require('../../../app');
        var accessToken = response.access_token;
        var tokenType = response.token_type;

        App.saveAuth(tokenType, accessToken);
        App.router.navigate('contacts', true);
      },
      error: jqxhr => {
        if (jqxhr.status === 401) {
          this.showError('User/Password are not valid');
        } else {
          this.showError('Oops... Unknown error happens');
        }
      }
    });
  }

  buildAuthenticationString(token) {
    return 'Bearer ' + token;
  }

  showError(message) {
    this.$('#message').html(message);
  }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.114.19