implemented verification of token signed elsewhere

This commit is contained in:
tigerbot 2017-07-13 18:23:42 -06:00
parent 43a61546d8
commit 5053963874
4 changed files with 216 additions and 30 deletions

View File

@ -291,16 +291,15 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
return; return;
} }
// every grant in the array must be present // every grant in the array must be present, though some grants can be satisfied
if (!grants.every(function (grant) { // by multiple scopes.
var scopes = grant.split(/\|/g); var missing = grants.filter(function (grant) {
return scopes.some(function (scp) { return !grant.split('|').some(function (scp) {
return tokenScopes.some(function (s) { return tokenScopes.indexOf(scp) !== -1;
return scp === s;
});
}); });
})) { });
res.send({ error: { message: "Token does not contain valid grants: '" + grants + "'", code: "E_NO_GRANTS" } }); if (missing.length) {
res.send({ error: { message: "Token missing required grants: '" + missing.join(',') + "'", code: "E_NO_GRANTS" } });
return; return;
} }
@ -308,11 +307,7 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
}; };
}; };
var _getOauth3Controllers = pkgDeps.getOauth3Controllers = require('oauthcommon/example-oauthmodels').create( myApp.use('/', require('./oauth3').attachOauth3);
{ sqlite3Sock: xconfx.sqlite3Sock, ipcKey: xconfx.ipcKey }
).getControllers;
//require('oauthcommon').inject(packagedApi._getOauth3Controllers, packagedApi._api, pkgConf, pkgDeps);
require('oauthcommon').inject(_getOauth3Controllers, myApp/*, pkgConf, pkgDeps*/);
// TODO delete these caches when config changes // TODO delete these caches when config changes
var _stripe; var _stripe;
@ -725,8 +720,8 @@ module.exports.create = function (xconfx, apiFactories, apiDeps) {
// Canonical client names // Canonical client names
// example.com should use api.example.com/api for all requests // example.com should use api.example.com/api for all requests
// sub.example.com/api should resolve to sub.example.com // sub.example.com/api should resolve to sub.example.com
// example.com/subpath/api should resolve to example.com#subapp // example.com/subapp/api should resolve to example.com#subapp
// sub.example.com/subpath/api should resolve to sub.example.com#subapp // sub.example.com/subapp/api should resolve to sub.example.com#subapp
var clientUrih = req.hostname.replace(/^api\./, '') + req.url.replace(/\/api\/.*/, '/').replace(/\/+/g, '#').replace(/#$/, ''); var clientUrih = req.hostname.replace(/^api\./, '') + req.url.replace(/\/api\/.*/, '/').replace(/\/+/g, '#').replace(/#$/, '');
var clientApiUri = req.hostname + req.url.replace(/\/api\/.*/, '/').replace(/\/$/, ''); var clientApiUri = req.hostname + req.url.replace(/\/api\/.*/, '/').replace(/\/$/, '');
// Canonical package names // Canonical package names

201
lib/oauth3.js Normal file
View File

@ -0,0 +1,201 @@
'use strict';
var PromiseA = require('bluebird');
function extractAccessToken(req) {
var token;
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); }
*/
return PromiseA.resolve(token);
}
function verifyToken(token) {
var jwt = require('jsonwebtoken');
var decoded;
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 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
});
}, 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 opts = {};
if (Array.isArray(res.data.alg)) {
opts.algorithms = res.data.alg;
} else if (typeof res.data.alg === 'string') {
opts.algorithms = [res.data.alg];
}
try {
return jwt.verify(token, require('jwk-to-pem')(res.data), opts);
} catch (err) {
return PromiseA.reject({
message: 'token verification failed'
, code: 'E_INVALID_TOKEN'
, url: 'https://oauth3.org/docs/errors#E_INVALID_TOKEN'
, subErr: err.toString()
});
}
});
}
function attachOauth3(req, res, next) {
req.oauth3 = {};
extractAccessToken(req).then(function (token) {
if (!token) {
return null;
}
var decoded;
try {
decoded = require('jsonwebtoken').decode(token);
} 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 ppid = decoded.sub || decoded.ppid || decoded.appScopedId;
req.oauth3.encodedToken = token;
req.oauth3.token = decoded;
req.oauth3.ppid = ppid;
req.oauth3.verifyAsync = function () {
return verifyToken(token);
};
req.oauth3.rescope = function () {
// 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.
return PromiseA.resolve(ppid);
};
}).then(function () {
next();
}, function (err) {
res.send(err);
});
}
module.exports.attachOauth3 = attachOauth3;
module.exports.verifyToken = verifyToken;

View File

@ -55,19 +55,7 @@ function getApi(conf, pkgConf, pkgDeps, packagedApi) {
packagedApi._api = require('express-lazy')(); packagedApi._api = require('express-lazy')();
packagedApi._api_app = myApp; packagedApi._api_app = myApp;
//require('./oauth3-auth').inject(conf, packagedApi._api, pkgConf, pkgDeps); packagedApi._api.use('/', require('./oauth3').attachOauth3);
pkgDeps.getOauth3Controllers =
packagedApi._getOauth3Controllers = require('oauthcommon/example-oauthmodels').create(conf).getControllers;
require('oauthcommon').inject(packagedApi._getOauth3Controllers, packagedApi._api, pkgConf, pkgDeps);
// DEBUG
//
/*
packagedApi._api.use('/', function (req, res, next) {
console.log('[DEBUG pkgApiApp]', req.method, req.hostname, req.url);
next();
});
//*/
// TODO fix backwards compat // TODO fix backwards compat

View File

@ -52,6 +52,8 @@
"express": "4.x", "express": "4.x",
"express-lazy": "^1.1.1", "express-lazy": "^1.1.1",
"express-session": "^1.11.3", "express-session": "^1.11.3",
"jsonwebtoken": "^7.4.1",
"jwk-to-pem": "^1.2.6",
"mailchimp-api-v3": "^1.7.0", "mailchimp-api-v3": "^1.7.0",
"mandrill-api": "^1.0.45", "mandrill-api": "^1.0.45",
"masterquest-sqlite3": "git+https://git.daplie.com/node/masterquest-sqlite3.git", "masterquest-sqlite3": "git+https://git.daplie.com/node/masterquest-sqlite3.git",
@ -59,7 +61,7 @@
"multiparty": "^4.1.3", "multiparty": "^4.1.3",
"nodemailer": "^1.4.0", "nodemailer": "^1.4.0",
"nodemailer-mailgun-transport": "1.x", "nodemailer-mailgun-transport": "1.x",
"oauthcommon": "git+https://git.daplie.com/node/oauthcommon.git", "oauth3.js": "git+https://git.daplie.com/OAuth3/oauth3.js.git",
"serve-static": "1.x", "serve-static": "1.x",
"sqlite3-cluster": "git+https://git.daplie.com/coolaj86/sqlite3-cluster.git#v2", "sqlite3-cluster": "git+https://git.daplie.com/coolaj86/sqlite3-cluster.git#v2",
"stripe": "^4.22.0", "stripe": "^4.22.0",