jaredhanson/oauth2orize

View on GitHub
lib/middleware/authorization.js

Summary

Maintainability
F
3 days
Test Coverage
/**
 * Module dependencies.
 */
var utils = require('../utils')
  , AuthorizationError = require('../errors/authorizationerror');


/**
 * Handle authorization requests from OAuth 2.0 clients.
 *
 * Obtaining authorization via OAuth 2.0 consists of a sequence of discrete
 * steps.  First, the client requests authorization from the user (in this case
 * using an authorization server as an intermediary).  The authorization server
 * conducts an approval dialog with the user to obtain permission.  After access
 * has been allowed, a grant is issued to the client which can be exchanged for
 * an access token.
 *
 * This middleware is used to initiate authorization transactions.  If a request
 * is parsed and validated, the following properties will be available on the
 * request:
 *
 *     req.oauth2.transactionID  an ID assigned to this transaction
 *     req.oauth2.client         client requesting the user's authorization
 *     req.oauth2.redirectURI    URL to redirect the user to after authorization
 *     req.oauth2.req            parameters from request made by the client
 *
 * The contents of `req.oauth2.req` depends on the grant type requested by the
 * the client.  The `server`'s request parsing functions are used to construct
 * this object, and the application can implement support for these types as
 * necessary, taking advantage of bundled grant middleware.
 *
 * Because the approval dialog may be conducted over a series of requests and
 * responses, a transaction is also stored in the session until a decision is
 * reached.  The application is responsible for verifying the user's identity
 * and prompting him or her to allow or deny the request (typically via an HTML
 * form).  At that point, `decision` middleware can be utilized to process the
 * user's decision and issue the grant to the client.
 *
 * Callbacks:
 *
 * This middleware requires a `validate` callback, for which the function
 * signature is as follows:
 *
 *     function(clientID, redirectURI, done) { ... }
 *
 * `clientID` is the client identifier and `redirectURI` is the redirect URI as
 * indicated by the client.  If the request is valid, `done` must be invoked
 * with the following signature:
 *
 *     done(err, client, redirectURI);
 *
 * `client` is the client instance which is making the request.  `redirectURI`
 * is the URL to which the user will be redirected after authorization is
 * obtained (which may be different, if the server is enforcing registration
 * requirements).  If an error occurs, `done` should be invoked with `err` set
 * in idomatic Node.js fashion.
 *
 * Alternate function signatures of the `validate` callback are available if
 * needed.  Consult the source code for a definitive reference.
 *
 *
 * Note that authorization may be obtained by the client directly from the user
 * without using an authorization server as an intermediary (for example, when
 * obtaining a grant in the form of the user's password credentials).  In these
 * cases, the client interacts only with the token endpoint without any need to
 * interact with the authorization endpoint.
 *
 * Options:
 *
 *     idLength    length of generated transaction IDs (default: 8)
 *     sessionKey  key under which transactions are stored in the session (default: 'authorize')
 *
 * Examples:
 *
 *     app.get('/dialog/authorize',
 *       login.ensureLoggedIn(),
 *       server.authorization(function(clientID, redirectURI, done) {
 *         Clients.findOne(clientID, function(err, client) {
 *           if (err) { return done(err); }
 *           if (!client) { return done(null, false); }
 *           return done(null, client, client.redirectURI);
 *         });
 *       }),
 *       function(req, res) {
 *         res.render('dialog', { transactionID: req.oauth2.transactionID,
 *                                user: req.user, client: req.oauth2.client });
 *       });
 *
 * References:
 *  - [Authorization Endpoint](http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.1)
 *
 * @param {Server} server
 * @param {Object} options
 * @param {Function} validate
 * @return {Function}
 * @api protected
 */
module.exports = function(server, options, validate, immediate, complete) {
  if (typeof options == 'function') {
    complete = immediate;
    immediate = validate;
    validate = options;
    options = undefined;
  }
  options = options || {};
  immediate = immediate || function (client, user, done) { return done(null, false); };
  
  if (!server) { throw new TypeError('oauth2orize.authorization middleware requires a server argument'); }
  if (!validate) { throw new TypeError('oauth2orize.authorization middleware requires a validate function'); }
  
  var userProperty = options.userProperty || 'user';
  
  return function authorization(req, res, next) {
    
    var body = req.body || {}
      , type = req.query.response_type || body.response_type;

    server._parse(type, req, function(err, areq) {
      if (err) { return next(err); }
      if (!areq || !areq.type) { return next(new AuthorizationError('Missing required parameter: response_type', 'invalid_request')); }
      if (areq.type && !areq.clientID) { return next(new AuthorizationError('Unsupported response type: ' + type, 'unsupported_response_type')); }
      
      function validated(err, client, redirectURI, webOrigin) {
        // Set properties *before* next()'ing due to error.  The presence of a
        // redirectURI being provided, even under error conditions, indicates
        // that the client should be informed of the error via a redirect.
        req.oauth2 = {};
        if (client) { req.oauth2.client = client; }
        if (redirectURI) { req.oauth2.redirectURI = redirectURI; }
        if (webOrigin) { req.oauth2.webOrigin = webOrigin; }
        req.oauth2.req = areq;
        req.oauth2.user = req[userProperty];
        if (req.locals) { req.oauth2.locals = req.locals; }

        if (err) { return next(err); }
        if (!client) { return next(new AuthorizationError('Unauthorized client', 'unauthorized_client')); }

        function immediated(err, allow, info, locals) {
          if (err) { return next(err); }
          if (allow) {
            req.oauth2.res = info || {};
            req.oauth2.res.allow = true;
            if (locals) {
              req.oauth2.locals = req.oauth2.locals || {};
              utils.merge(req.oauth2.locals, locals);
            }
            
            function completing(cb) {
              if (!complete) { return cb(); }
              complete(req, req.oauth2, cb);
            }

            server._respond(req.oauth2, res, completing, function(err) {
              if (err) { return next(err); }
              return next(new AuthorizationError('Unsupported response type: ' + req.oauth2.req.type, 'unsupported_response_type'));
            });
          } else {
            // Add info and locals to `req.oauth2`, where they will be
            // available to the next middleware.  Since this is a
            // non-immediate response, the next middleware's responsibility is
            // to prompt the user to allow or deny access.  `info` and
            // `locals` are passed along as they may be of assistance when
            // rendering the prompt.
            //
            // Note that, when using the legacy transaction store, `info` is
            // also serialized into the transaction, where it can further be
            // utilized in the `decision` middleware after the user submits the
            // prompt's form.  As such, `info` should be a normal JSON object,
            // so that it can be correctly serialized into the session.
            // `locals` is only carried through to the middleware chain for the
            // current request, so it may contain instantiated classes that
            // don't serialize cleanly.
            //
            // The transaction store is pluggable when initializing the `Server`
            // instance.  If an application implements a custom transaction
            // store, the specific details of what properties are serialized
            // into the transaction and loaded on subsequent requests are
            // determined by the implementation.
            req.oauth2.info = info;
            if (locals) {
              req.oauth2.locals = req.oauth2.locals || {};
              utils.merge(req.oauth2.locals, locals);
            }
            
            // A dialog needs to be conducted to obtain the user's approval.
            // Serialize a transaction to the session.  The transaction will be
            // restored (and removed) from the session when the user allows or
            // denies the request.
            function stored(err, tid) {
              if (err) { return next(err); }
              req.oauth2.transactionID = tid;
              next();
            }
            
            if (server._txnStore.legacy == true) {
              var txn = {};
              txn.protocol = 'oauth2';
              txn.client = client;
              txn.redirectURI = redirectURI;
              txn.webOrigin = webOrigin;
              txn.req = areq;
              txn.info = info;
              
              server._txnStore.store(server, options, req, txn, stored);
            } else {
              server._txnStore.store(req, req.oauth2, stored);
            }
          }
        }

        var arity = immediate.length;
        if (arity == 7) {
          immediate(req.oauth2.client, req.oauth2.user, req.oauth2.req.scope, req.oauth2.req.type, req.oauth2.req, req.oauth2.locals, immediated);
        } else if (arity == 6) {
          immediate(req.oauth2.client, req.oauth2.user, req.oauth2.req.scope, req.oauth2.req.type, req.oauth2.req, immediated);
        } else if (arity == 5) {
          immediate(req.oauth2.client, req.oauth2.user, req.oauth2.req.scope, req.oauth2.req.type, immediated);
        } else if (arity == 4) {
          immediate(req.oauth2.client, req.oauth2.user, req.oauth2.req.scope, immediated);
        } else if (arity == 3) {
          immediate(req.oauth2.client, req.oauth2.user, immediated);
        } else { // arity == 2
          immediate(req.oauth2, immediated);
        }
      }
      
      try {
        var arity = validate.length;
        if (arity == 5) {
          validate(areq.clientID, areq.redirectURI, areq.scope, areq.type, validated);
        } else if (arity == 4) {
          validate(areq.clientID, areq.redirectURI, areq.scope, validated);
        } else if (arity == 3) {
          validate(areq.clientID, areq.redirectURI, validated);
        } else { // arity == 2
          validate(areq, validated);
        }
      } catch (ex) {
        return next(ex);
      }
    });
  };
};