'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;