349 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			349 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'use strict';
 | 
						|
 | 
						|
var PromiseA = require('bluebird');
 | 
						|
 | 
						|
function generateRescope(req, Models, decoded) {
 | 
						|
  var fullPpid = decoded.sub+'@'+decoded.iss;
 | 
						|
  var ppid = decoded.sub;
 | 
						|
  return function (/*sub*/) {
 | 
						|
    // TODO: this function is supposed to convert PPIDs of different parties to some account
 | 
						|
    // ID that allows application to keep track of permisions and what-not.
 | 
						|
    console.log('[rescope] Attempting ', fullPpid);
 | 
						|
    return Models.IssuerOauth3OrgGrants.find({ azpSub: fullPpid }).then(function (results) {
 | 
						|
      if (results[0]) {
 | 
						|
        console.log('[rescope] lucky duck: got it on the 1st try');
 | 
						|
        return results;
 | 
						|
      }
 | 
						|
 | 
						|
      // XXX BUG XXX
 | 
						|
      // should be able to distinguish between own ids and 3rd party via @whatever.com
 | 
						|
      return Models.IssuerOauth3OrgGrants.find({ azpSub: ppid });
 | 
						|
    }).then(function (results) {
 | 
						|
      var result = results[0];
 | 
						|
 | 
						|
      if (!result || !result.sub || !decoded.iss) {
 | 
						|
        console.log('[rescope] Not a 2nd party token...');
 | 
						|
        return Models.IssuerOauth3OrgProfiles.get(fullPpid);
 | 
						|
      }
 | 
						|
 | 
						|
      return result;
 | 
						|
    }).then(function (result) {
 | 
						|
      var err;
 | 
						|
 | 
						|
      if (!result || !result.sub || !decoded.iss) {
 | 
						|
        // XXX BUG XXX TODO swap this external ppid for an internal (and ask user to link with existing profile)
 | 
						|
        //req.oauth3.accountIdx = fullPpid;
 | 
						|
        console.log('[DEBUG] decoded:');
 | 
						|
        console.log(decoded);
 | 
						|
        console.log('[DEBUG] decoded.iss:', decoded.iss);
 | 
						|
        console.log('[DEBUG] fullPpid:', fullPpid);
 | 
						|
        console.log('[DEBUG] ppid:', ppid);
 | 
						|
 | 
						|
        err = new Error(
 | 
						|
          "TODO: No profile found with that credential. Would you like to create a new profile or link to an existing profile?"
 | 
						|
        );
 | 
						|
        err.code = "E_NO_PROFILE@oauth3.org"
 | 
						|
        throw err;
 | 
						|
        //return req.oauth3.token.sub + '@' + req.oauth3.token.iss;
 | 
						|
      }
 | 
						|
 | 
						|
      // XXX BUG XXX need to pass own url in to use as issuer for own tokens
 | 
						|
      req.oauth3.accountIdx = result.sub + '@' + (result.iss || decoded.iss);
 | 
						|
 | 
						|
      console.log('[rescope] result:');
 | 
						|
      console.log(result);
 | 
						|
      console.log('[rescope] req.oauth3.accountIdx:', req.oauth3.accountIdx);
 | 
						|
 | 
						|
      return req.oauth3.accountIdx;
 | 
						|
    });
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
function verifyToken(token, opts) {
 | 
						|
  opts = opts || { audiences: [], complete: false };
 | 
						|
  var jwt = require('jsonwebtoken');
 | 
						|
  var decoded;
 | 
						|
 | 
						|
  if (!token) {
 | 
						|
    return PromiseA.reject({
 | 
						|
      message: 'no token provided'
 | 
						|
    , code: 'E_NO_TOKEN'
 | 
						|
    , url: 'https://oauth3.org/docs/errors#E_NO_TOKEN'
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  try {
 | 
						|
    decoded = jwt.decode(token, {complete: true});
 | 
						|
  } catch (e) {}
 | 
						|
 | 
						|
  if (!decoded) {
 | 
						|
    return PromiseA.reject({
 | 
						|
      message: 'provided token not a JSON Web Token'
 | 
						|
    , code: 'E_NOT_JWT'
 | 
						|
    , url: 'https://oauth3.org/docs/errors#E_NOT_JWT'
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  var sub = decoded.payload.sub || decoded.payload.ppid || decoded.payload.appScopedId;
 | 
						|
  if (!sub) {
 | 
						|
    return PromiseA.reject({
 | 
						|
      message: 'token missing sub'
 | 
						|
    , code: 'E_MISSING_SUB'
 | 
						|
    , url: 'https://oauth3.org/docs/errors#E_MISSING_SUB'
 | 
						|
    });
 | 
						|
  }
 | 
						|
  var kid = decoded.header.kid || decoded.payload.kid;
 | 
						|
  if (!kid) {
 | 
						|
    return PromiseA.reject({
 | 
						|
      message: 'token missing kid'
 | 
						|
    , code: 'E_MISSING_KID'
 | 
						|
    , url: 'https://oauth3.org/docs/errors#E_MISSING_KID'
 | 
						|
    });
 | 
						|
  }
 | 
						|
  if (!decoded.payload.iss) {
 | 
						|
    return PromiseA.reject({
 | 
						|
      message: 'token missing iss'
 | 
						|
    , code: 'E_MISSING_ISS'
 | 
						|
    , url: 'https://oauth3.org/docs/errors#E_MISSING_ISS'
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  var audMatch = decoded.payload.aud && ('*' === decoded.payload.aud || opts.audiences.some(function (aud) { return -1 !== decoded.payload.aud.split(',').indexOf(aud); }));
 | 
						|
  var azpMatch = decoded.payload.azp && ('*' === decoded.payload.azp || opts.audiences.some(function (aud) { return -1 !== decoded.payload.azp.split(',').indexOf(aud); }));
 | 
						|
 | 
						|
  if (!audMatch) {
 | 
						|
    console.log("[verifyToken] 'aud' '" + decoded.payload.aud + "' does not match '" + opts.audiences.join(',') + "'");
 | 
						|
  }
 | 
						|
  // TODO needs an option to verify that the sender of the token was, in fact, the azp (i.e. the Origin and/or Referer Headers)
 | 
						|
  if (!azpMatch) {
 | 
						|
    console.log("[verifyToken] 'azp' '" + decoded.payload.azp + "' does not match '" + opts.audiences.join(',') + "'");
 | 
						|
  }
 | 
						|
 | 
						|
  if (!audMatch && !azpMatch) {
 | 
						|
    err = new Error(
 | 
						|
      "Application '" + req.experienceId + "' refused token because '" + decoded.payload.aud + "' is not an accepted audience (aud)"
 | 
						|
    + " and '" + decoded.payload.azp + "' is not an authorized party (azp)"
 | 
						|
    );
 | 
						|
    err.code = 'E_TOKEN_AUD';
 | 
						|
    err.url = 'https://oauth3.org/docs/errors#E_TOKEN_AUD'
 | 
						|
    return PromiseA.reject(err);
 | 
						|
  }
 | 
						|
 | 
						|
  var OAUTH3 = require('oauth3.js');
 | 
						|
  OAUTH3._hooks = require('oauth3.js/oauth3.node.storage.js');
 | 
						|
  return OAUTH3.discover(decoded.payload.iss).then(function (directives) {
 | 
						|
    var args = (directives || {}).retrieve_jwk;
 | 
						|
    if (typeof args === 'string') {
 | 
						|
      args = { url: args, method: 'GET' };
 | 
						|
    }
 | 
						|
    if (typeof (args || {}).url !== 'string') {
 | 
						|
      return PromiseA.reject({
 | 
						|
        message: 'token issuer does not support retrieving JWKs'
 | 
						|
      , code: 'E_INVALID_ISS'
 | 
						|
      , url: 'https://oauth3.org/docs/errors#E_INVALID_ISS'
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    var params = {
 | 
						|
      sub: sub
 | 
						|
    , kid: kid
 | 
						|
    };
 | 
						|
    var url = args.url;
 | 
						|
    var body;
 | 
						|
    Object.keys(params).forEach(function (key) {
 | 
						|
      if (url.indexOf(':'+key) !== -1) {
 | 
						|
        url = url.replace(':'+key, params[key]);
 | 
						|
        delete params[key];
 | 
						|
      }
 | 
						|
    });
 | 
						|
    if (Object.keys(params).length > 0) {
 | 
						|
      if ('GET' === (args.method || 'GET').toUpperCase()) {
 | 
						|
        url += '?' + OAUTH3.query.stringify(params);
 | 
						|
      } else {
 | 
						|
        body = params;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return OAUTH3.request({
 | 
						|
      url: OAUTH3.url.resolve(directives.api, url)
 | 
						|
    , method: args.method
 | 
						|
    , data: body
 | 
						|
    }).catch(function (err) {
 | 
						|
      return PromiseA.reject({
 | 
						|
        message: 'failed to retrieve public key from token issuer'
 | 
						|
      , code: 'E_NO_PUB_KEY'
 | 
						|
      , url: 'https://oauth3.org/docs/errors#E_NO_PUB_KEY'
 | 
						|
      , subErr: err.toString()
 | 
						|
      });
 | 
						|
    });
 | 
						|
  }, function (err) {
 | 
						|
    return PromiseA.reject({
 | 
						|
      message: 'token issuer is not a valid OAuth3 provider'
 | 
						|
    , code: 'E_INVALID_ISS'
 | 
						|
    , url: 'https://oauth3.org/docs/errors#E_INVALID_ISS'
 | 
						|
    , subErr: err.toString()
 | 
						|
    });
 | 
						|
  }).then(function (res) {
 | 
						|
    if (res.data.error) {
 | 
						|
      return PromiseA.reject(res.data.error);
 | 
						|
    }
 | 
						|
    var opts2 = {};
 | 
						|
    if (Array.isArray(res.data.alg)) {
 | 
						|
      opts2.algorithms = res.data.alg;
 | 
						|
    } else if (typeof res.data.alg === 'string') {
 | 
						|
      opts2.algorithms = [res.data.alg];
 | 
						|
    }
 | 
						|
 | 
						|
    try {
 | 
						|
      if (opts.complete) {
 | 
						|
        opts2.complete = true;
 | 
						|
      }
 | 
						|
      return jwt.verify(token, require('jwk-to-pem')(res.data), opts2);
 | 
						|
    } catch (err) {
 | 
						|
      if ('TokenExpiredError' === err.name) {
 | 
						|
        return PromiseA.reject({
 | 
						|
          message: 'TokenExpiredError: jwt expired'
 | 
						|
        , code: 'E_TOKEN_EXPIRED'
 | 
						|
        , url: 'https://oauth3.org/docs/errors#E_TOKEN_EXPIRED'
 | 
						|
        });
 | 
						|
      }
 | 
						|
 | 
						|
      return PromiseA.reject({
 | 
						|
        message: 'token verification failed'
 | 
						|
      , code: 'E_INVALID_TOKEN'
 | 
						|
      , url: 'https://oauth3.org/docs/errors#E_INVALID_TOKEN'
 | 
						|
      , subErr: err.toString()
 | 
						|
      });
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
function deepFreeze(obj) {
 | 
						|
  Object.keys(obj).forEach(function (key) {
 | 
						|
    if (obj[key] && typeof obj[key] === 'object') {
 | 
						|
      deepFreeze(obj[key]);
 | 
						|
    }
 | 
						|
  });
 | 
						|
  Object.freeze(obj);
 | 
						|
}
 | 
						|
 | 
						|
function fiddleOauth3(Models, req) {
 | 
						|
  var token = req.oauth3.encodedToken;
 | 
						|
 | 
						|
  req.oauth3.verifyAsync = function (jwt, opts) {
 | 
						|
    return verifyToken(jwt || token, opts || { audiences: [ req.experienceId ] });
 | 
						|
  };
 | 
						|
 | 
						|
  if (!token) {
 | 
						|
    return PromiseA.resolve(null);
 | 
						|
  }
 | 
						|
 | 
						|
  return verifyToken(token, { complete: false, audiences: [ req.experienceId ] }).then(function  (decoded) {
 | 
						|
    var err;
 | 
						|
    req.oauth3.token = decoded;
 | 
						|
 | 
						|
    if (!decoded) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    req.oauth3.ppid = decoded.sub;
 | 
						|
 | 
						|
    req.oauth3.id = decoded.sub + '@' + decoded.iss;
 | 
						|
    req.oauth3.sub = decoded.sub;
 | 
						|
    req.oauth3.iss = decoded.iss;
 | 
						|
    req.oauth3.azp = decoded.azp;
 | 
						|
    req.oauth3.aud = decoded.aud;
 | 
						|
 | 
						|
    req.oauth3.accountIdx = req.oauth3.id;
 | 
						|
 | 
						|
    req.oauth3.rescope = generateRescope(req, Models, decoded);
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
function cookieOauth3(Models, req, res, next) {
 | 
						|
  req.oauth3 = {};
 | 
						|
 | 
						|
  var cookieName = 'jwt';
 | 
						|
  var token = req.cookies[cookieName];
 | 
						|
 | 
						|
  req.oauth3.encodedToken = token;
 | 
						|
  fiddleOauth3(Models, req).then(function () {
 | 
						|
    deepFreeze(req.oauth3);
 | 
						|
    //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
 | 
						|
    next();
 | 
						|
  }, function (err) {
 | 
						|
    if ('E_NO_TOKEN' === err.code) {
 | 
						|
      next();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if ('E_TOKEN_EXPIRED' === err.code) {
 | 
						|
      res.clearCookie(cookieName);
 | 
						|
      next();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    console.error('[walnut] cookie lib/oauth3 error:');
 | 
						|
    console.error(err);
 | 
						|
    res.send(err);
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
function attachOauth3(Models, req, res, next) {
 | 
						|
  req.oauth3 = {};
 | 
						|
 | 
						|
  var token = null;
 | 
						|
  var parts;
 | 
						|
  var scheme;
 | 
						|
  var credentials;
 | 
						|
 | 
						|
  if (req.headers && req.headers.authorization) {
 | 
						|
    // Works for all of Authorization: Bearer {{ token }}, Token {{ token }}, JWT {{ token }}
 | 
						|
    parts = req.headers.authorization.split(' ');
 | 
						|
 | 
						|
    if (parts.length !== 2) {
 | 
						|
      return PromiseA.reject(new Error("malformed Authorization header"));
 | 
						|
    }
 | 
						|
 | 
						|
    scheme = parts[0];
 | 
						|
    credentials = parts[1];
 | 
						|
 | 
						|
    if (-1 !== ['token', 'bearer'].indexOf(scheme.toLowerCase())) {
 | 
						|
      token = credentials;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (req.body && req.body.access_token) {
 | 
						|
    if (token) { PromiseA.reject(new Error("token exists in header and body")); }
 | 
						|
    token = req.body.access_token;
 | 
						|
  }
 | 
						|
 | 
						|
  // TODO disallow query with req.method === 'GET'
 | 
						|
  // NOTE: the case of DDNS on routers requires a GET and access_token
 | 
						|
  // (cookies should be used for protected static assets)
 | 
						|
  if (req.query && req.query.access_token) {
 | 
						|
    if (token) { PromiseA.reject(new Error("token already exists in either header or body and also in query")); }
 | 
						|
    token = req.query.access_token;
 | 
						|
  }
 | 
						|
 | 
						|
  /*
 | 
						|
  err = new Error(challenge());
 | 
						|
  err.code = 'E_BEARER_REALM';
 | 
						|
 | 
						|
  if (!token) { return PromiseA.reject(err); }
 | 
						|
  */
 | 
						|
 | 
						|
  req.oauth3.encodedToken = token;
 | 
						|
  fiddleOauth3(Models, req).then(function () {
 | 
						|
    //deepFreeze(req.oauth3);
 | 
						|
    //Object.defineProperty(req, 'oauth3', {configurable: false, writable: false});
 | 
						|
    next();
 | 
						|
  }, function (err) {
 | 
						|
    console.error('[walnut] JWT lib/oauth3 error:');
 | 
						|
    console.error(err);
 | 
						|
    res.send(err);
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
module.exports.attachOauth3 = attachOauth3;
 | 
						|
module.exports.cookieOauth3 = cookieOauth3;
 | 
						|
module.exports.verifyToken = verifyToken;
 |