jaredhanson/oauth2orize

View on GitHub
lib/server.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * Module dependencies.
 */
var SessionStore = require('./txn/session')
  , UnorderedList = require('./unorderedlist')
  , authorization = require('./middleware/authorization')
  , resume = require('./middleware/resume')
  , decision = require('./middleware/decision')
  , transactionLoader = require('./middleware/transactionLoader')
  , token = require('./middleware/token')
  , authorizationErrorHandler = require('./middleware/authorizationErrorHandler')
  , errorHandler = require('./middleware/errorHandler')
  , utils = require('./utils')
  , debug = require('debug')('oauth2orize');


/**
 * `Server` constructor.
 *
 * @api public
 */
function Server(options) {
  options = options || {};
  this._reqParsers = [];
  this._resHandlers = [];
  this._errHandlers = [];
  this._exchanges = [];

  this._serializers = [];
  this._deserializers = [];
  this._txnStore = options.store || new SessionStore();
}

/**
 * Register authorization grant middleware.
 *
 * OAuth 2.0 defines an authorization framework, in which authorization grants
 * can be of a variety of types.  Initiating and responding to an OAuth 2.0
 * authorization transaction is implemented by grant middleware, and the server
 * registers the middleware it wishes to support.
 *
 * Examples:
 *
 *     server.grant(oauth2orize.grant.code());
 *
 *     server.grant('*', function(req) {
 *       return { host: req.headers['host'] }
 *     });
 *
 *     server.grant('foo', function(req) {
 *       return { foo: req.query['foo'] }
 *     });
 *
 * @param {String|Object} type
 * @param {String} phase
 * @param {Function} fn
 * @return {Server} for chaining
 * @api public
 */
Server.prototype.grant = function(type, phase, fn) {
  if (typeof type == 'object') {
    // sig: grant(mod)
    var mod = type;
    if (mod.request) { this.grant(mod.name, 'request', mod.request); }
    if (mod.response) { this.grant(mod.name, 'response', mod.response); }
    if (mod.error) { this.grant(mod.name, 'error', mod.error); }
    return this;
  }
  if (typeof phase == 'object') {
    // sig: grant(type, mod)
    var mod = phase;
    if (mod.request) { this.grant(type, 'request', mod.request); }
    if (mod.response) { this.grant(type, 'response', mod.response); }
    if (mod.error) { this.grant(type, 'error', mod.error); }
    return this;
  }
  
  if (typeof phase == 'function') {
    // sig: grant(type, fn)
    fn = phase;
    phase = 'request';
  }
  if (type === '*') { type = null; }
  if (type) { type = new UnorderedList(type); }
  
  if (phase == 'request') {
    debug('register request parser %s %s', type || '*', fn.name || 'anonymous');
    this._reqParsers.push({ type: type, handle: fn });
  } else if (phase == 'response') {
    debug('register response handler %s %s', type || '*', fn.name || 'anonymous');
    this._resHandlers.push({ type: type, handle: fn });
  } else if (phase == 'error') {
    debug('register error handler %s %s', type || '*', fn.name || 'anonymous');
    this._errHandlers.push({ type: type, handle: fn });
  }
  return this;
};

/**
 * Register token exchange middleware.
 *
 * OAuth 2.0 defines an authorization framework, in which authorization grants
 * can be of a variety of types.  Exchanging of these types for access tokens is
 * implemented by exchange middleware, and the server registers the middleware
 * it wishes to support.
 *
 * Examples:
 *
 *     server.exchange(oauth2orize.exchange.authorizationCode(function() {
 *       ...
 *     }));
 *
 * @param {String|Function} type
 * @param {Function} fn
 * @return {Server} for chaining
 * @api public
 */
Server.prototype.exchange = function(type, fn) {
  if (typeof type == 'function') {
    fn = type;
    type = fn.name;
  }
  if (type === '*') { type = null; }
  
  debug('register exchanger %s %s', type || '*', fn.name || 'anonymous');
  this._exchanges.push({ type: type, handle: fn });
  return this;
};

/**
 * Parses requests to obtain authorization.
 *
 * @api public
 */
Server.prototype.authorize =
Server.prototype.authorization = function(options, validate, immediate, complete) {
  return authorization(this, options, validate, immediate, complete);
};

Server.prototype.resume = function(options, immediate, complete) {
  var loader;
  if (typeof options == 'function' && typeof immediate == 'function' && typeof complete == 'function') {
    options = { loadTransaction: options };
  }
  
  if (options && options.loadTransaction === false) {
    return resume(this, options, immediate, complete);
  }
  if (options && typeof options.loadTransaction === 'function') {
    loader = options.loadTransaction;
  } else {
    loader = transactionLoader(this, options);
  }
  return [loader, resume(this, options, immediate, complete)];
};

/**
 * Handle a user's response to an authorization dialog.
 *
 * @api public
 */
Server.prototype.decision = function(options, parse, complete) {
  if (options && options.loadTransaction === false) {
    return decision(this, options, parse, complete);
  }
  return [transactionLoader(this, options), decision(this, options, parse, complete)];
};

Server.prototype.authorizeError =
Server.prototype.authorizationError =
Server.prototype.authorizationErrorHandler = function(options) {
  var loader = transactionLoader(this, options);
  
  return [
    function transactionLoaderErrorWrapper(err, req, res, next) {
      loader(req, res, function(ierr) {
        return next(err);
      });
    },
    authorizationErrorHandler(this, options)
  ];
};

/**
 * Handle requests to exchange an authorization grant for an access token.
 *
 * @api public
 */
Server.prototype.token = function(options) {
  return token(this, options);
};

/**
 * Respond to errors encountered in OAuth 2.0 endpoints.
 *
 * @api public
 */
Server.prototype.errorHandler = function(options) {
  return errorHandler(options);
};

/**
 * Registers a function used to serialize client objects into the session.
 *
 * Examples:
 *
 *     server.serializeClient(function(client, done) {
 *       done(null, client.id);
 *     });
 *
 * @api public
 */
Server.prototype.serializeClient = function(fn, done) {
  if (typeof fn === 'function') {
    return this._serializers.push(fn);
  }
  
  // private implementation that traverses the chain of serializers, attempting
  // to serialize a client
  var client = fn;
  
  var stack = this._serializers;
  (function pass(i, err, obj) {
    // serializers use 'pass' as an error to skip processing
    if ('pass' === err) { err = undefined; }
    // an error or serialized object was obtained, done
    if (err || obj) { return done(err, obj); }
    
    var layer = stack[i];
    if (!layer) {
      return done(new Error('Failed to serialize client. Register serialization function using serializeClient().'));
    }
    
    try {
      layer(client, function(e, o) { pass(i + 1, e, o); } );
    } catch (ex) {
      return done(ex);
    }
  })(0);
};

/**
 * Registers a function used to deserialize client objects out of the session.
 *
 * Examples:
 *
 *     server.deserializeClient(function(id, done) {
 *       Client.findById(id, function (err, client) {
 *         done(err, client);
 *       });
 *     });
 *
 * @api public
 */
Server.prototype.deserializeClient = function(fn, done) {
  if (typeof fn === 'function') {
    return this._deserializers.push(fn);
  }
  
  // private implementation that traverses the chain of deserializers,
  // attempting to deserialize a client
  var obj = fn;
  
  var stack = this._deserializers;
  (function pass(i, err, client) {
    // deserializers use 'pass' as an error to skip processing
    if ('pass' === err) { err = undefined; }
    // an error or deserialized client was obtained, done
    if (err || client) { return done(err, client); }
    // a valid client existed when establishing the session, but that client has
    // since been deauthorized
    if (client === null || client === false) { return done(null, false); }
    
    var layer = stack[i];
    if (!layer) {
      return done(new Error('Failed to deserialize client. Register deserialization function using deserializeClient().'));
    }
    
    try {
      layer(obj, function(e, c) { pass(i + 1, e, c); } );
    } catch (ex) {
      return done(ex);
    }
  })(0);
};


/**
 * Parse authorization request into transaction using registered grant middleware. 
 *
 * @param {String} type
 * @param {http.ServerRequest} req
 * @param {Function} cb
 * @api private
 */
Server.prototype._parse = function(type, req, cb) {
  var ultype = new UnorderedList(type)
    , stack = this._reqParsers
    , areq = {};
  
  if (type) { areq.type = type; }
  
  (function pass(i) {
    var layer = stack[i];
    if (!layer) { return cb(null, areq); }
    
    try {
      debug('parse:%s', layer.handle.name || 'anonymous');
      if (layer.type === null || layer.type.equalTo(ultype)) {
        var arity = layer.handle.length;
        if (arity == 1) { // sync
          var o = layer.handle(req);
          utils.merge(areq, o);
          pass(i + 1);
        } else { // async
          layer.handle(req, function(err, o) {
            if (err) { return cb(err); }
            utils.merge(areq, o);
            pass(i + 1);
          });
        }
      } else {
        pass(i + 1);
      }
    } catch (ex) {
      return cb(ex);
    }
  })(0);
};

/**
 * Respond to authorization transaction using registered grant middleware. 
 *
 * @param {Object} txn
 * @param {http.ServerResponse} res
 * @param {Function} cb
 * @api private
 */
Server.prototype._respond = function(txn, res, complete, cb) {
  if (cb === undefined) {
    cb = complete;
    complete = undefined;
  }
  complete = complete || function(cb) { cb(); }
  
  var ultype = new UnorderedList(txn.req.type)
    , stack = this._resHandlers
    , idx = 0;
  
  function next(err) {
    if (err) { return cb(err); }
    
    var layer = stack[idx++];
    if (!layer) { return cb(); }
    
    try {
      debug('respond:%s', layer.handle.name || 'anonymous');
      if (layer.type === null || layer.type.equalTo(ultype)) {
        var arity = layer.handle.length;
        if (arity == 4) {
          layer.handle(txn, res, complete, next);
        } else {
          layer.handle(txn, res, next);
        }
      } else {
        next();
      }
    } catch (ex) {
      return cb(ex);
    }
  }
  next();
};

Server.prototype._respondError = function(err, txn, res, cb) {
  var ultype = new UnorderedList(txn.req.type)
    , stack = this._errHandlers
    , idx = 0;
    
  function next(err) {
    var layer = stack[idx++];
    if (!layer) { return cb(err); }
  
    try {
      debug('error:%s', layer.handle.name || 'anonymous');
      if (layer.type === null || layer.type.equalTo(ultype)) {
        layer.handle(err, txn, res, next);
      } else {
        next(err);
      }
    } catch (ex) {
      return cb(ex);
    }
  }
  next(err);
}

/**
 * Process token request using registered exchange middleware. 
 *
 * @param {String} type
 * @param {http.ServerRequest} req
 * @param {http.ServerResponse} res
 * @param {Function} cb
 * @api private
 */
Server.prototype._exchange = function(type, req, res, cb) {
  var stack = this._exchanges
    , idx = 0;
  
  function next(err) {
    if (err) { return cb(err); }
    
    var layer = stack[idx++];
    if (!layer) { return cb(); }
    
    try {
      debug('exchange:%s', layer.handle.name || 'anonymous');
      if (layer.type === null || layer.type === type) {
        layer.handle(req, res, next);
      } else {
        next();
      }
    } catch (ex) {
      return cb(ex);
    }
  }
  next();
};


/**
 * Expose `Server`.
 */
exports = module.exports = Server;