From 878559329f94fd6003bd275401bab4192ad63660 Mon Sep 17 00:00:00 2001 From: shepp Date: Tue, 4 Oct 2016 16:11:33 -0400 Subject: [PATCH 01/20] bos auth initial commit --- middleware/bos-authentication.js | 125 +++++++++++++++++++++++++++++++ middleware/bos-passport.js | 17 +++++ services/bosPassport.js | 67 +++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 middleware/bos-authentication.js create mode 100644 middleware/bos-passport.js create mode 100644 services/bosPassport.js diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js new file mode 100644 index 0000000..fadbfc1 --- /dev/null +++ b/middleware/bos-authentication.js @@ -0,0 +1,125 @@ +var _ = require('lodash'); + +var config; +var log; +var loader; +var httpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']; + +module.exports = { + init : init +}; + +function init(app, config, logger, serviceLoader, swagger) { + config = config.get('authentication'); + log = logger; + loader = serviceLoader; + _.forEach(swagger.getSimpleSpecs(), function (api, name) { + var basePath = api.basePath || ''; + /* apply security requirements to each route path*/ + _.keys(api.paths).forEach(function (path) { + var pathObj = api.paths[path]; + var routePath = basePath + _convertPathToExpress(path); + + //loop for http method keys, like get an post + _.keys(pathObj).forEach(function (method) { + if (_.contains(httpMethods, method)) { + var operation = pathObj[method]; + if (operation['security']) { + _.keys(operation['security']).forEach(function (securityReq) { + _applySecurityRequirement(app, method, routePath, securityReq, + api.securityDefinitions[securityReq], operation['x-bos-permissions'][securityReq], + operation['security'][securityReq]); + }); + } + } + }); + }); + }); +} + +function _applySecurityRequirement(app, method, route, securityReq, securityDefn, requiredPermissions, requiredScopes) { + //wire up path with user defined authentication method for this req + if (config.authenticationMethods[securityReq]) { + var parts = config.authenticationMethods[securityReq].split('.'); + var service = loader.get(parts[0]); + if (!service) { + return log.warn('Could not find service module named "%s".', parts[0]); + } + var serviceMethod = service[parts[1]]; + if (!_.isFunction(serviceMethod)) { + return log.warn('Authentication function %s on module %s is missing or invalid.', + parts[1], parts[0]); + } + //scopes included here for security type oauth2 where authentication/authorization happens in one go + app[method].call(app, route, _.partialRight(serviceMethod, securityReq, + securityDefn, requiredScopes)); + //wire up path with user defined authorization method + if (config.authorizationMethods[securityReq]) { + parts = config.authorizationMethods[securityReq].split('.'); + service = loader.get(parts[0]); + if (!service) { + return log.warn('Could not find service module named "%s".', parts[0]); + } + serviceMethod = service[parts[1]]; + if (!_.isFunction(serviceMethod)) { + return log.warn('Authorization function %s on module %s is missing or invalid.', + parts[1], parts[0]); + } + var wrappedAuthorizationMethod = wrapAuthorizationMethod(serviceMethod, route, + securityDefn, requiredPermissions); + app[method].call(app, route, _.partialRight(wrappedAuthorizationMethod, route, + securityDefn, requiredPermissions)); + } else { + return log.warn('No authorization method found for security requirement %s', securityReq); + } + } else { + return log.warn('No authentication method defined for security requirement %s', securityReq); + } +} + +function wrapAuthorizationMethod(authorizationMethod, route, securityDefn, requiredPermissions) { + return function (req, res, next) { + var runTimeRequiredPermissions = _expandRouteInstancePermissions(requiredPermissions, route, req.path); + authorizationMethod.call(this, req, res, next, securityDefn, runTimeRequiredPermissions); + }; +} + +function _expandRouteInstancePermissions(perms, route, uri) { + /* relate the route path parameters to the url instance values + perms: ["api:read:{policyid}", "api:read:{claimid}"] + route: /api/v1/policies/:policyid/claims/:claimid + [ api,v1,policies,:policyid,claims,:claimid ] + uri: /api/v1/policies/SFIH1234534/claims/37103 + [ api,v1,policies,SFIH1234534,claims,37103 ] + */ + if (!_.isString(route) || !_.isString(uri)) { + return perms; + } + var routeParts = route.split('/'); + var uriParts = uri.split('/'); + + // [ [ ':policyid', 'SFIH1234534' ], [ ':claimid', '37103' ] ] + var pathIds = _.zip(routeParts, uriParts) + .filter(function (b) { + return _.startsWith(b[0], ':'); + }).map(function (path) { + // trim the : + path[0] = path[0].substr(1); + return path; + }); + + return _.map(perms, function (perm) { + var ePerm = perm; + _.forEach(pathIds, function (item) { + ePerm = ePerm.replace('{' + item[0] + '}', item[1]); + }); + return ePerm; + }); +} + +//swagger paths use {blah} while express uses :blah +function _convertPathToExpress(swaggerPath) { + var reg = /\{([^\}]+)\}/g; //match all {...} + swaggerPath = swaggerPath.replace(reg, ':$1'); + return swaggerPath; +} \ No newline at end of file diff --git a/middleware/bos-passport.js b/middleware/bos-passport.js new file mode 100644 index 0000000..ee80e87 --- /dev/null +++ b/middleware/bos-passport.js @@ -0,0 +1,17 @@ +var passport = require('passport'); + +module.exports = { + init : init +}; + +function init(app, config, bosPassport) { + app.use(passport.initialize()); + //if sessions are enabled and express session is also being used, + //express session middleware MUST be listed first in the middleware config + if (config.get('passport').options.session) { + app.use(passport.session()); + } + bosPassport.registerSecurityStrategies(); +} + +//need serializeUser and deserializeUser functions for session enabling diff --git a/services/bosPassport.js b/services/bosPassport.js new file mode 100644 index 0000000..8aa218a --- /dev/null +++ b/services/bosPassport.js @@ -0,0 +1,67 @@ +var _ = require('lodash'), + passport = require('passport'), + subRequire = require('../lib/subRequire'); + +var strategyMap; +var cfg; +var loader; + +module.exports = { + init : init, + registerSecurityStrategies: registerSecurityStrategies, + authenticate: authenticate +}; + +function init(config, serviceLoader) { + cfg = config.get('passport'); + loader = serviceLoader; +} + +function registerSecurityStrategies() { + var strategies = cfg.strategies; + _.forEach(strategies, function (strategy) { + //load passport strategy module + if (!strategyMap[strategy.module]) { + strategyMap[strategy.module] = subRequire(strategy.module, 'bos-passport').Strategy; + } + _.forEach(_.keys(strategy.options), function (opt) { + strategy.options[opt] = prepareOption(strategy.options[opt]); + }); + //strategy.id MUST be the same as the name of a security requirement in the swagger spec + passport.use(strategy.id, new strategyMap[strategy.module](strategy.options, strategy.verify)); + }); +} + +//securityReq -> strategyId should be a one to one mapping +function authenticate(req, res, next, securityReq, securityDefn) { + _.forEach(_.keys(cfg.options), function (opt) { + cfg.options[opt] = prepareOption(cfg.options[opt]); + }); + return passport.authenticate(securityReq, cfg.options); +} + +//fetch the option from the config, or if option is a service method, point to or call the method with supplied args +//maybe make the method args able to be fetched from the config? +//else just return the option +function prepareOption(opt) { + if (typeof opt === 'string') { + var configRegex = /^{{(.*)}}$/; + var result = configRegex.exec(opt); + if (result && result[1]) { + // This is a config-based option + return _.get(cfg, result[1]); + } + } else if (typeof opt === 'object') { + if (opt.service) { + var service = loader.get(opt.service); + var method = service[opt.method.name]; + if (method.execute) { + return method.apply(service, method.args); + } else { + return _.partial.apply(_, [method].concat(method.args)); + } + } + } + return opt; +} + From da8247406b7d2af3039dfeea178805657617f1fd Mon Sep 17 00:00:00 2001 From: shepp Date: Wed, 5 Oct 2016 14:52:58 -0400 Subject: [PATCH 02/20] add extractor functions for basic, apiKey --- middleware/bos-authentication.js | 64 +++++++++++++++++++++++++++++++- services/oauth2.js | 12 ++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 services/oauth2.js diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index fadbfc1..0b773e8 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -1,4 +1,5 @@ var _ = require('lodash'); +var base64URL = require('base64url'); var config; var log; @@ -38,7 +39,20 @@ function init(app, config, logger, serviceLoader, swagger) { } function _applySecurityRequirement(app, method, route, securityReq, securityDefn, requiredPermissions, requiredScopes) { - //wire up path with user defined authentication method for this req + var scheme = securityDefn.type; + switch (scheme) { + case 'basic': + app[method].call(app, route, basicExtractor()); + break; + case 'apiKey': + app[method].call(app, route, apiKeyExtractor(securityDefn)); //may also need a user provided 'verify' function here + break; + case 'oauth2': + break; + default: + return log.warn('unrecognized security scheme %s for route %s', scheme, route); + } + /*//wire up path with user defined authentication method for this req if (config.authenticationMethods[securityReq]) { var parts = config.authenticationMethods[securityReq].split('.'); var service = loader.get(parts[0]); @@ -74,7 +88,7 @@ function _applySecurityRequirement(app, method, route, securityReq, securityDefn } } else { return log.warn('No authentication method defined for security requirement %s', securityReq); - } + }*/ } function wrapAuthorizationMethod(authorizationMethod, route, securityDefn, requiredPermissions) { @@ -84,6 +98,52 @@ function wrapAuthorizationMethod(authorizationMethod, route, securityDefn, requi }; } +function basicExtractor() { + return function (req, res, next) { + //header should be of the form "Basic " + user:password as a base64 encoded string + var authHeader = req.getHeader('Authorization'); + var credentialsBase64 = authHeader.substring(authHeader.indexOf('Basic ') + 1); + var credentials = base64URL.decode(credentialsBase64).split(':'); + req.user = {name: credentials[0], password: credentials[1], realm: 'Basic'}; + next(); + }; +} + +function apiKeyExtractor(securityDefn, verify) { + return function (req, res, next) { + if (!verifyMD5Header(req.body, req.getHeader('Content-MD5'))) { + log.error('content md5 header for uri %s coming from %s did not match request body', req.path, req.ip); + res.status(401).send('content md5 header did not match request body'); + } + var apiId = 'where do we get this from?'; + var digest; + if (securityDefn.in === 'query') { + digest = req.query[securityDefn.name]; + } else if (securityDefn.in === 'header') { + digest = req.getHeader(securityDefn.name); + } else { + return log.warn('unknown location %s for apiKey. ' + + 'looks like open api specs may have changed on us', securityDefn.in); + } + //this would have to be a user provided function that + //fetches the user (and thus the private key that we need to compute the hash) from some data source + //we don't need this if we decide that we will let the user figure out how to verify the digest + verify(apiId, function (user) { + //regenerate hash with apiKey + //if (hash === digest) + // all good + // else you suck + req.user = user; + next(); + }); + }; +} + +function verifyMD5Header(body, md5) { + //calculate md5 of request body and verify that it equals the header + return true; +} + function _expandRouteInstancePermissions(perms, route, uri) { /* relate the route path parameters to the url instance values perms: ["api:read:{policyid}", "api:read:{claimid}"] diff --git a/services/oauth2.js b/services/oauth2.js new file mode 100644 index 0000000..e9b554e --- /dev/null +++ b/services/oauth2.js @@ -0,0 +1,12 @@ + +//look for token, if is there and hasn't expired then boom. If it has expired then refresh token +//look for authorization code, if not there redirect to authorization url with required scopes +//get auth code, send request to token url with auth code to get back token + +function init() { + +} + +function authenticate() { + +} From b0b2bdb0b801796d391b13cecb0536857d536d96 Mon Sep 17 00:00:00 2001 From: shepp Date: Thu, 6 Oct 2016 10:21:59 -0400 Subject: [PATCH 03/20] hook in passport --- middleware/bos-authentication.js | 19 +++++++++++++++---- services/bosPassport.js | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index 0b773e8..c242a38 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -5,6 +5,7 @@ var config; var log; var loader; var httpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']; +var passportEnabled; module.exports = { init : init @@ -12,6 +13,7 @@ module.exports = { function init(app, config, logger, serviceLoader, swagger) { config = config.get('authentication'); + passportEnabled = config.get('passport').enabled; log = logger; loader = serviceLoader; _.forEach(swagger.getSimpleSpecs(), function (api, name) { @@ -39,18 +41,23 @@ function init(app, config, logger, serviceLoader, swagger) { } function _applySecurityRequirement(app, method, route, securityReq, securityDefn, requiredPermissions, requiredScopes) { - var scheme = securityDefn.type; - switch (scheme) { + if (passportEnabled) { + var passportService = loader.get('bosPassport'); + passportService.authenticate(securityReq); + } else { + var scheme = securityDefn.type; + switch (scheme) { case 'basic': app[method].call(app, route, basicExtractor()); break; - case 'apiKey': - app[method].call(app, route, apiKeyExtractor(securityDefn)); //may also need a user provided 'verify' function here + case 'apiKey': //may also need a user provided 'verify' function here + app[method].call(app, route, apiKeyExtractor(securityDefn)); break; case 'oauth2': break; default: return log.warn('unrecognized security scheme %s for route %s', scheme, route); + } } /*//wire up path with user defined authentication method for this req if (config.authenticationMethods[securityReq]) { @@ -100,6 +107,7 @@ function wrapAuthorizationMethod(authorizationMethod, route, securityDefn, requi function basicExtractor() { return function (req, res, next) { + //if there is no auth header present, send back challenge 401 with www-authenticate header? //header should be of the form "Basic " + user:password as a base64 encoded string var authHeader = req.getHeader('Authorization'); var credentialsBase64 = authHeader.substring(authHeader.indexOf('Basic ') + 1); @@ -110,6 +118,7 @@ function basicExtractor() { } function apiKeyExtractor(securityDefn, verify) { + //if there is no apiKey present, send back challenge 401 with www-authenticate header? return function (req, res, next) { if (!verifyMD5Header(req.body, req.getHeader('Content-MD5'))) { log.error('content md5 header for uri %s coming from %s did not match request body', req.path, req.ip); @@ -130,6 +139,8 @@ function apiKeyExtractor(securityDefn, verify) { //we don't need this if we decide that we will let the user figure out how to verify the digest verify(apiId, function (user) { //regenerate hash with apiKey + //hash will include symmetric apiKey, one or more of: + //request method, content-md5 header, request uri, timestamp, socket.remoteAddress, req.ip, ip whitelist? //if (hash === digest) // all good // else you suck diff --git a/services/bosPassport.js b/services/bosPassport.js index 8aa218a..012bdc6 100644 --- a/services/bosPassport.js +++ b/services/bosPassport.js @@ -33,7 +33,7 @@ function registerSecurityStrategies() { } //securityReq -> strategyId should be a one to one mapping -function authenticate(req, res, next, securityReq, securityDefn) { +function authenticate(securityReq) { _.forEach(_.keys(cfg.options), function (opt) { cfg.options[opt] = prepareOption(cfg.options[opt]); }); From 1aeb50a9a0563550de71b5f58d63d2359a0ead29 Mon Sep 17 00:00:00 2001 From: shepp Date: Sat, 8 Oct 2016 10:00:53 -0400 Subject: [PATCH 04/20] fun stuff with oauth2 --- handlers/_oauth-redirect.js | 29 +++++++++++ middleware/bos-authentication.js | 39 ++++++++++----- services/oauth2.js | 83 +++++++++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 handlers/_oauth-redirect.js diff --git a/handlers/_oauth-redirect.js b/handlers/_oauth-redirect.js new file mode 100644 index 0000000..65c9c53 --- /dev/null +++ b/handlers/_oauth-redirect.js @@ -0,0 +1,29 @@ +var _ = require('lodash'); +var oauthService; + +module.exports = { + init : init +}; + +function init(app, oauth2) { + oauthService = oauth2; + app.get('/oauth-redirect', handleRedirect); +} + +function handleRedirect(req, res) { + if (!req.query.code) { + //should have auth code at this point + } else if (!oauthService.getRequestState(req.query.state)) {//check for XSRF + //log warning about possible xsrf attack + } else { + oauthService.getTokenData(req, res, function (tokenData) { + req.bosAuth.accessTokenData = tokenData; + var originalState = oauthService.getRequestState(req.query.state); + _.merge(req, originalState.req); + _.merge(res, originalState.res); + //this should make it so that an auth code will only get used once + oauthService.deleteRequestState(req.query.state); + originalState.next(); + }); + } +} diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index c242a38..10a6f2a 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -1,9 +1,9 @@ var _ = require('lodash'); var base64URL = require('base64url'); -var config; var log; var loader; +var oAuthService; var httpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']; var passportEnabled; @@ -12,7 +12,6 @@ module.exports = { }; function init(app, config, logger, serviceLoader, swagger) { - config = config.get('authentication'); passportEnabled = config.get('passport').enabled; log = logger; loader = serviceLoader; @@ -44,7 +43,7 @@ function _applySecurityRequirement(app, method, route, securityReq, securityDefn if (passportEnabled) { var passportService = loader.get('bosPassport'); passportService.authenticate(securityReq); - } else { + } else { //need to check for an active session here as well, because if there is one we want to skip all of this var scheme = securityDefn.type; switch (scheme) { case 'basic': @@ -54,14 +53,18 @@ function _applySecurityRequirement(app, method, route, securityReq, securityDefn app[method].call(app, route, apiKeyExtractor(securityDefn)); break; case 'oauth2': + if (!oAuthService) { + oAuthService = loader.get('oauth2'); + } + app[method].call(app, route, oauth2(route, securityDefn, requiredScopes)); break; default: return log.warn('unrecognized security scheme %s for route %s', scheme, route); } } /*//wire up path with user defined authentication method for this req - if (config.authenticationMethods[securityReq]) { - var parts = config.authenticationMethods[securityReq].split('.'); + if (cfg.authenticationMethods[securityReq]) { + var parts = cfg.authenticationMethods[securityReq].split('.'); var service = loader.get(parts[0]); if (!service) { return log.warn('Could not find service module named "%s".', parts[0]); @@ -75,8 +78,8 @@ function _applySecurityRequirement(app, method, route, securityReq, securityDefn app[method].call(app, route, _.partialRight(serviceMethod, securityReq, securityDefn, requiredScopes)); //wire up path with user defined authorization method - if (config.authorizationMethods[securityReq]) { - parts = config.authorizationMethods[securityReq].split('.'); + if (cfg.authorizationMethods[securityReq]) { + parts = cfg.authorizationMethods[securityReq].split('.'); service = loader.get(parts[0]); if (!service) { return log.warn('Could not find service module named "%s".', parts[0]); @@ -109,10 +112,10 @@ function basicExtractor() { return function (req, res, next) { //if there is no auth header present, send back challenge 401 with www-authenticate header? //header should be of the form "Basic " + user:password as a base64 encoded string - var authHeader = req.getHeader('Authorization'); + var authHeader = req.headers['Authorization']; var credentialsBase64 = authHeader.substring(authHeader.indexOf('Basic ') + 1); var credentials = base64URL.decode(credentialsBase64).split(':'); - req.user = {name: credentials[0], password: credentials[1], realm: 'Basic'}; + req.bos.user = {name: credentials[0], password: credentials[1], realm: 'Basic'}; next(); }; } @@ -120,7 +123,7 @@ function basicExtractor() { function apiKeyExtractor(securityDefn, verify) { //if there is no apiKey present, send back challenge 401 with www-authenticate header? return function (req, res, next) { - if (!verifyMD5Header(req.body, req.getHeader('Content-MD5'))) { + if (!verifyMD5Header(req.body, req.headers['Content-MD5'])) { log.error('content md5 header for uri %s coming from %s did not match request body', req.path, req.ip); res.status(401).send('content md5 header did not match request body'); } @@ -129,7 +132,7 @@ function apiKeyExtractor(securityDefn, verify) { if (securityDefn.in === 'query') { digest = req.query[securityDefn.name]; } else if (securityDefn.in === 'header') { - digest = req.getHeader(securityDefn.name); + digest = req.headers[securityDefn.name]; } else { return log.warn('unknown location %s for apiKey. ' + 'looks like open api specs may have changed on us', securityDefn.in); @@ -144,12 +147,24 @@ function apiKeyExtractor(securityDefn, verify) { //if (hash === digest) // all good // else you suck - req.user = user; + req.bosAuth.user = user; next(); }); }; } +function oauth2(route, securityDefn, scopes) { + return function (req, res, next) { + var oAuthInstance = oAuthService.getOAuthInstance(route); + if (!oAuthInstance) { + oAuthInstance = new oAuthService.OAuth2(securityDefn.authorizationUrl, + securityDefn.flow, securityDefn.tokenUrl, securityDefn.scopes); + oAuthService.addOAuthInstance(route, oAuthInstance); + } + oAuthInstance.startOAuth(req, res); + }; +} + function verifyMD5Header(body, md5) { //calculate md5 of request body and verify that it equals the header return true; diff --git a/services/oauth2.js b/services/oauth2.js index e9b554e..d8b13c2 100644 --- a/services/oauth2.js +++ b/services/oauth2.js @@ -1,12 +1,93 @@ +var _ = require('lodash'), + request = require('request'), + jwt = require('jsonwebtoken'); +var cfg; +var clientId; +var clientSecret; +var redirectUri; +//map of route to oauth2 instance in order to maintain state between requests during oauth process +var routeOAuthMap = {}; +var requestStateMap = {}; + +module.exports = { + init : init, + OAuth2: OAuth2 +}; //look for token, if is there and hasn't expired then boom. If it has expired then refresh token //look for authorization code, if not there redirect to authorization url with required scopes //get auth code, send request to token url with auth code to get back token -function init() { +function init(config) { + cfg = config.get('oauth'); + clientId = cfg.clientId; + clientSecret = cfg.clientSecret; + redirectUri = cfg.redirectUri; +} + +function OAuth2(authorizationUrl, flow, tokenUrl, scopes) { + this.authorizationUrl = authorizationUrl; + this.flow = flow; + this.tokenUrl = tokenUrl; + this.scopes = scopes; +} + +function redirectToAuthorizationUrl (req, res, stateId) { + var queryString = '?response_type=code&requestclientId='; + queryString += clientId; + queryString += '&redirect_uri=' + redirectUri; + queryString += '&scope=' + this.scopes.join(' '); + queryString += '&state=' + stateId; + res.statusCode = 302; + res.headers['location'] = this.authorizationUrl + queryString; + res.send(); +} +function addRequestState (stateId, req, res, next) { + requestStateMap[stateId] = {req: req, res: res, next: next}; } +OAuth2.prototype.startOAuth = function (req, res, next) { + if (this.flow === 'accessCode') { + redirectToAuthorizationUrl(req, res); + } else { + //unsupported flow + } + addRequestState(Math.random(), req, res, next); +}; + +OAuth2.prototype.addOAuthInstance = function (route, oauthInstance) { + routeOAuthMap[route] = oauthInstance; +}; + +OAuth2.prototype.getOAuthInstance = function (route) { + return routeOAuthMap[route]; +}; + +OAuth2.prototype.getRequestState = function (stateId) { + return requestStateMap[stateId]; +}; + +OAuth2.prototype.deleteRequestState = function (stateId) { + requestStateMap[stateId] = undefined; +}; + +OAuth2.prototype.getTokenData = function (req, res, callback) { + var form = {grant_type: 'authorization_code'}; + form.code = req.query.code; + form.clientId = clientId; + form.clientSecret = clientSecret; + form.redirectUri = redirectUri; + request.post({url: this.tokenUrl, form: form}, function (err, resp, body) { + /*check response status*/ + //we will let user defined middleware take it from here. + // At a minimum the response will contain parameters listed here: + // https://tools.ietf.org/html/rfc6749#section-5.1 + //anything else is beyond the oauth2 spec and is provider specific + callback(JSON.parse(body)); + }); +}; + function authenticate() { } From f9f393d9719d0c197a631766dca7241ba0c9e038 Mon Sep 17 00:00:00 2001 From: shepp Date: Mon, 10 Oct 2016 14:23:55 -0400 Subject: [PATCH 05/20] build apiKey extractor around RFC 7616. set consistent property on request object for all three auth schemes --- handlers/_oauth-redirect.js | 2 +- middleware/bos-authentication.js | 55 +++++++++++++++++--------------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/handlers/_oauth-redirect.js b/handlers/_oauth-redirect.js index 65c9c53..ed5f9e3 100644 --- a/handlers/_oauth-redirect.js +++ b/handlers/_oauth-redirect.js @@ -17,7 +17,7 @@ function handleRedirect(req, res) { //log warning about possible xsrf attack } else { oauthService.getTokenData(req, res, function (tokenData) { - req.bosAuth.accessTokenData = tokenData; + req.bos.authenticationData = tokenData; var originalState = oauthService.getRequestState(req.query.state); _.merge(req, originalState.req); _.merge(res, originalState.res); diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index 10a6f2a..92c2618 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -29,7 +29,7 @@ function init(app, config, logger, serviceLoader, swagger) { if (operation['security']) { _.keys(operation['security']).forEach(function (securityReq) { _applySecurityRequirement(app, method, routePath, securityReq, - api.securityDefinitions[securityReq], operation['x-bos-permissions'][securityReq], + api.securityDefinitions[securityReq], /*operation['x-bos-permissions'][securityReq],*/ operation['security'][securityReq]); }); } @@ -39,7 +39,8 @@ function init(app, config, logger, serviceLoader, swagger) { }); } -function _applySecurityRequirement(app, method, route, securityReq, securityDefn, requiredPermissions, requiredScopes) { +function _applySecurityRequirement(app, method, route, securityReq, + securityDefn, /*requiredPermissions,*/ requiredScopes) { if (passportEnabled) { var passportService = loader.get('bosPassport'); passportService.authenticate(securityReq); @@ -101,46 +102,53 @@ function _applySecurityRequirement(app, method, route, securityReq, securityDefn }*/ } -function wrapAuthorizationMethod(authorizationMethod, route, securityDefn, requiredPermissions) { +/*function wrapAuthorizationMethod(authorizationMethod, route, securityDefn, requiredPermissions) { return function (req, res, next) { var runTimeRequiredPermissions = _expandRouteInstancePermissions(requiredPermissions, route, req.path); authorizationMethod.call(this, req, res, next, securityDefn, runTimeRequiredPermissions); }; -} +}*/ function basicExtractor() { return function (req, res, next) { //if there is no auth header present, send back challenge 401 with www-authenticate header? + //now that I think about this, think this should be handled by user defined code + //because their is no way for us to know how to set the header fields (i.e. realm) //header should be of the form "Basic " + user:password as a base64 encoded string var authHeader = req.headers['Authorization']; - var credentialsBase64 = authHeader.substring(authHeader.indexOf('Basic ') + 1); + var credentialsBase64 = authHeader.substring(authHeader.split('Basic ')[1]); var credentials = base64URL.decode(credentialsBase64).split(':'); - req.bos.user = {name: credentials[0], password: credentials[1], realm: 'Basic'}; + req.bos.authenticationData = {username: credentials[0], password: credentials[1], scheme: 'Basic'}; next(); }; } -function apiKeyExtractor(securityDefn, verify) { +function apiKeyExtractor(securityDefn) { //if there is no apiKey present, send back challenge 401 with www-authenticate header? + //now that I think about this, think this should be handled by user defined code + //because their is no way for us to know how to set the header fields (i.e. realm) return function (req, res, next) { - if (!verifyMD5Header(req.body, req.headers['Content-MD5'])) { - log.error('content md5 header for uri %s coming from %s did not match request body', req.path, req.ip); - res.status(401).send('content md5 header did not match request body'); - } - var apiId = 'where do we get this from?'; - var digest; + //should be form of username="Mufasa", realm="myhost@example.com" + var authorizationHeaders = req.headers['Authorization'].split(', '); + var authenticationData = {scheme: 'apiKey'}; + authorizationHeaders.forEach(function (header) { + //should be form of username="Mufasa" + var keyValPair = header.split('='); + authenticationData[keyValPair[0]] = keyValPair[1].substring(1, keyValPair[1].length - 1); + }); if (securityDefn.in === 'query') { - digest = req.query[securityDefn.name]; + authenticationData.password = req.query[securityDefn.name]; } else if (securityDefn.in === 'header') { - digest = req.headers[securityDefn.name]; + authenticationData.password = req.headers[securityDefn.name]; } else { return log.warn('unknown location %s for apiKey. ' + 'looks like open api specs may have changed on us', securityDefn.in); } + req.bos.authenticationData = authenticationData; //this would have to be a user provided function that //fetches the user (and thus the private key that we need to compute the hash) from some data source //we don't need this if we decide that we will let the user figure out how to verify the digest - verify(apiId, function (user) { + /*verify(apiId, function (user) { //regenerate hash with apiKey //hash will include symmetric apiKey, one or more of: //request method, content-md5 header, request uri, timestamp, socket.remoteAddress, req.ip, ip whitelist? @@ -149,7 +157,7 @@ function apiKeyExtractor(securityDefn, verify) { // else you suck req.bosAuth.user = user; next(); - }); + });*/ }; } @@ -165,19 +173,14 @@ function oauth2(route, securityDefn, scopes) { }; } -function verifyMD5Header(body, md5) { - //calculate md5 of request body and verify that it equals the header - return true; -} - -function _expandRouteInstancePermissions(perms, route, uri) { - /* relate the route path parameters to the url instance values +/*function _expandRouteInstancePermissions(perms, route, uri) { + relate the route path parameters to the url instance values perms: ["api:read:{policyid}", "api:read:{claimid}"] route: /api/v1/policies/:policyid/claims/:claimid [ api,v1,policies,:policyid,claims,:claimid ] uri: /api/v1/policies/SFIH1234534/claims/37103 [ api,v1,policies,SFIH1234534,claims,37103 ] - */ + if (!_.isString(route) || !_.isString(uri)) { return perms; } @@ -201,7 +204,7 @@ function _expandRouteInstancePermissions(perms, route, uri) { }); return ePerm; }); -} +}*/ //swagger paths use {blah} while express uses :blah function _convertPathToExpress(swaggerPath) { From 4f7bf9283fab163d0baba97ea79df82f25d9b98b Mon Sep 17 00:00:00 2001 From: shepp Date: Tue, 11 Oct 2016 11:20:29 -0400 Subject: [PATCH 06/20] partial: testing auth --- examples/swagger/config/default.json | 2 +- examples/swagger/middleware/custom-auth.js | 13 ++++++++++ examples/swagger/swagger/api-v1.yaml | 2 ++ .../public/paths/superfuntime-{id}.yaml | 2 ++ .../swagger/public/security-definitions.yaml | 6 +++++ handlers/_oauth-redirect.js | 2 +- middleware/bos-authentication.js | 26 ++++++++++++------- 7 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 examples/swagger/middleware/custom-auth.js create mode 100644 examples/swagger/swagger/public/security-definitions.yaml diff --git a/examples/swagger/config/default.json b/examples/swagger/config/default.json index 5b41dd0..5d5563e 100644 --- a/examples/swagger/config/default.json +++ b/examples/swagger/config/default.json @@ -2,7 +2,7 @@ { "express": { "port": "3000", - "middleware": ["cors", "body-parser"], + "middleware": ["cors", "body-parser", "bos-authentication", "custom-auth"], "middleware$": ["errors"] }, "cors": { diff --git a/examples/swagger/middleware/custom-auth.js b/examples/swagger/middleware/custom-auth.js new file mode 100644 index 0000000..0451bf2 --- /dev/null +++ b/examples/swagger/middleware/custom-auth.js @@ -0,0 +1,13 @@ + +exports.init = function(app, logger) { + app.use(function (req, res, next) { + if (!req.bosAuth) { + next(); + } else if (!req.bosAuth.authenticationData) { + res.sendStatus(401); + } else { + //have some stuff here to perform app specific authentication + next(); + } + }); +}; diff --git a/examples/swagger/swagger/api-v1.yaml b/examples/swagger/swagger/api-v1.yaml index 9aa318e..ddc2272 100644 --- a/examples/swagger/swagger/api-v1.yaml +++ b/examples/swagger/swagger/api-v1.yaml @@ -8,6 +8,8 @@ produces: - application/json paths: $ref: 'public/paths.yaml' +securityDefinitions: + $ref: 'public/security-definitions.yaml' definitions: Contact: $ref: 'public\definitions\Contact.yaml' diff --git a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml index 2f163f1..ec409b6 100644 --- a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml +++ b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml @@ -5,6 +5,8 @@ get: - Fun Times parameters: - $ref: '../parameters/PathId.yaml' + security: + - apiKeyEx: [] responses: 200: schema: diff --git a/examples/swagger/swagger/public/security-definitions.yaml b/examples/swagger/swagger/public/security-definitions.yaml new file mode 100644 index 0000000..6c588b7 --- /dev/null +++ b/examples/swagger/swagger/public/security-definitions.yaml @@ -0,0 +1,6 @@ +basicEx: + type: basic +apiKeyEx: + type: apiKey + in: header + name: x-auth \ No newline at end of file diff --git a/handlers/_oauth-redirect.js b/handlers/_oauth-redirect.js index ed5f9e3..95579e2 100644 --- a/handlers/_oauth-redirect.js +++ b/handlers/_oauth-redirect.js @@ -17,7 +17,7 @@ function handleRedirect(req, res) { //log warning about possible xsrf attack } else { oauthService.getTokenData(req, res, function (tokenData) { - req.bos.authenticationData = tokenData; + req.authenticationData = tokenData; var originalState = oauthService.getRequestState(req.query.state); _.merge(req, originalState.req); _.merge(res, originalState.res); diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index 92c2618..a51a529 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -27,10 +27,13 @@ function init(app, config, logger, serviceLoader, swagger) { if (_.contains(httpMethods, method)) { var operation = pathObj[method]; if (operation['security']) { - _.keys(operation['security']).forEach(function (securityReq) { - _applySecurityRequirement(app, method, routePath, securityReq, - api.securityDefinitions[securityReq], /*operation['x-bos-permissions'][securityReq],*/ - operation['security'][securityReq]); + operation['security'].forEach(function (securityReq) { + _.forOwn(securityReq, function (scopes, securityDefn) { + _applySecurityRequirement(app, method, routePath, securityDefn, + api.securityDefinitions[securityDefn], + /*operation['x-bos-permissions'][securityReq],*/ + scopes); + }); }); } } @@ -115,10 +118,13 @@ function basicExtractor() { //now that I think about this, think this should be handled by user defined code //because their is no way for us to know how to set the header fields (i.e. realm) //header should be of the form "Basic " + user:password as a base64 encoded string - var authHeader = req.headers['Authorization']; - var credentialsBase64 = authHeader.substring(authHeader.split('Basic ')[1]); - var credentials = base64URL.decode(credentialsBase64).split(':'); - req.bos.authenticationData = {username: credentials[0], password: credentials[1], scheme: 'Basic'}; + req.bosAuth = {}; + var authHeader = req.headers['authorization'] ? req.headers['authorization'] : ''; + if (authHeader !== '') { + var credentialsBase64 = authHeader.substring(authHeader.split('Basic ')[1]); + var credentials = base64URL.decode(credentialsBase64).split(':'); + req.bosAuth.authenticationData = {username: credentials[0], password: credentials[1], scheme: 'Basic'}; + } next(); }; } @@ -129,7 +135,7 @@ function apiKeyExtractor(securityDefn) { //because their is no way for us to know how to set the header fields (i.e. realm) return function (req, res, next) { //should be form of username="Mufasa", realm="myhost@example.com" - var authorizationHeaders = req.headers['Authorization'].split(', '); + var authorizationHeaders = req.headers['authorization'].split(', '); var authenticationData = {scheme: 'apiKey'}; authorizationHeaders.forEach(function (header) { //should be form of username="Mufasa" @@ -144,7 +150,7 @@ function apiKeyExtractor(securityDefn) { return log.warn('unknown location %s for apiKey. ' + 'looks like open api specs may have changed on us', securityDefn.in); } - req.bos.authenticationData = authenticationData; + req.authenticationData = authenticationData; //this would have to be a user provided function that //fetches the user (and thus the private key that we need to compute the hash) from some data source //we don't need this if we decide that we will let the user figure out how to verify the digest From 54fe804720fc6a3820d9b232f8e1a515fc43a9bb Mon Sep 17 00:00:00 2001 From: shepp Date: Tue, 11 Oct 2016 17:38:45 -0400 Subject: [PATCH 07/20] allow custom security definitions and middleware --- examples/swagger/middleware/custom-auth.js | 24 +++- middleware/bos-authentication.js | 154 ++++++++++++++------- 2 files changed, 123 insertions(+), 55 deletions(-) diff --git a/examples/swagger/middleware/custom-auth.js b/examples/swagger/middleware/custom-auth.js index 0451bf2..111fd70 100644 --- a/examples/swagger/middleware/custom-auth.js +++ b/examples/swagger/middleware/custom-auth.js @@ -1,13 +1,25 @@ exports.init = function(app, logger) { app.use(function (req, res, next) { - if (!req.bosAuth) { - next(); - } else if (!req.bosAuth.authenticationData) { - res.sendStatus(401); - } else { - //have some stuff here to perform app specific authentication + if (!req.bosAuthenticationData) { next(); } + switch (req.bosAuthenticationData.type) { + + case 'basic': + if (!(req.bosAuthenticationData.username && req.bosAuthenticationData.password)) { + res.sendStatus(401); + } else { + next(); + } + break; + case 'apiKey': + if (!(req.bosAuthenticationData.password)) { + res.sendStatus(401); + } else { + next(); + } + break; + } }); }; diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index a51a529..62c7652 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -5,14 +5,14 @@ var log; var loader; var oAuthService; var httpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']; -var passportEnabled; +//map of security reqs to express middleware callbacks +var securityReqMap = {}; module.exports = { init : init }; function init(app, config, logger, serviceLoader, swagger) { - passportEnabled = config.get('passport').enabled; log = logger; loader = serviceLoader; _.forEach(swagger.getSimpleSpecs(), function (api, name) { @@ -27,6 +27,8 @@ function init(app, config, logger, serviceLoader, swagger) { if (_.contains(httpMethods, method)) { var operation = pathObj[method]; if (operation['security']) { + //need to make this a logical 'OR' + // so that only one security requirement must be satisfied on a route operation['security'].forEach(function (securityReq) { _.forOwn(securityReq, function (scopes, securityDefn) { _applySecurityRequirement(app, method, routePath, securityDefn, @@ -36,34 +38,80 @@ function init(app, config, logger, serviceLoader, swagger) { }); }); } + if (operation['x-bos-security']) { + //need to make this a logical 'OR' + // so that only one security requirement must be satisfied on a route + operation['x-bos-security'].forEach(function (securityReq) { + _.forOwn(securityReq, function (scopes, securityDefn) { + _applyCustomSecurityRequirement(app, method, routePath, securityDefn, + api['x-bos-securityDefinitions'][securityDefn], + /*operation['x-bos-permissions'][securityReq],*/ + scopes); + }); + }); + } } }); }); }); } +function _applyCustomSecurityRequirement(app, method, route, securityReq, + securityDefn, /*requiredPermissions,*/ requiredScopes) { + //load security def middleware + if (securityDefn['x-bos-middleware']) { + loader.loadConsumerModules('middleware', [securityDefn['x-bos-middleware']]); + loader.initConsumers('middleware', [securityDefn['x-bos-middleware']], function (err) { + if (!err) { + var customAuthMiddleware = loader.getConsumer('middleware', securityDefn['x-bos-middleware']); + if (customAuthMiddleware.authenticate) { + if (!securityReqMap[securityReq]) { + securityReqMap[securityReq] = + customAuthMiddleware.authenticate(securityReq, securityDefn, requiredScopes); + } + app[method].call(app, route, securityReqMap[securityReq]); + } + else { + log.warn('custom auth middleware %s missing authenticate method'); + } + } + else { + log.warn('Unable to find custom middleware %s for security defn %s', + securityDefn['x-bos-middleware'], securityReq); + } + }); + } else { + log.warn('No custom middleware defined for security defn %s', securityReq); + } +} + function _applySecurityRequirement(app, method, route, securityReq, securityDefn, /*requiredPermissions,*/ requiredScopes) { - if (passportEnabled) { - var passportService = loader.get('bosPassport'); - passportService.authenticate(securityReq); - } else { //need to check for an active session here as well, because if there is one we want to skip all of this - var scheme = securityDefn.type; - switch (scheme) { + //allow use of custom middleware even if a custom security definition was not used + if (securityDefn['x-bos-middleware']) { + _applyCustomSecurityRequirement(app, method, route, securityReq, + securityDefn, requiredScopes); + } else { + switch (securityDefn.type) { case 'basic': - app[method].call(app, route, basicExtractor()); + app[method].call(app, route, basicAuthentication(securityReq)); break; case 'apiKey': //may also need a user provided 'verify' function here - app[method].call(app, route, apiKeyExtractor(securityDefn)); + app[method].call(app, route, apiKeyAuthentication(securityReq, securityDefn)); break; case 'oauth2': - if (!oAuthService) { - oAuthService = loader.get('oauth2'); - } - app[method].call(app, route, oauth2(route, securityDefn, requiredScopes)); + /*if (!oAuthService) { + oAuthService = loader.get('oauth2'); + } + app[method].call(app, route, oauth2(route, securityDefn, requiredScopes));*/ + log.warn('No out of the box oauth2 implementation exists in BOS. ' + + 'You must define your own and reference it in the ' + + '"x-bos-middleware" property of the security definition %s', securityReq); break; default: - return log.warn('unrecognized security scheme %s for route %s', scheme, route); + return log.warn('unrecognized security type %s for security definition %s' + + 'You can provide a custom security definition in "x-bos-securityDefinitions" of your base spec', + securityDefn.type, securityReq); } } /*//wire up path with user defined authentication method for this req @@ -112,58 +160,66 @@ function _applySecurityRequirement(app, method, route, securityReq, }; }*/ -function basicExtractor() { +function basicAuthentication(securityReq) { return function (req, res, next) { - //if there is no auth header present, send back challenge 401 with www-authenticate header? - //now that I think about this, think this should be handled by user defined code - //because their is no way for us to know how to set the header fields (i.e. realm) //header should be of the form "Basic " + user:password as a base64 encoded string - req.bosAuth = {}; + req.bosAuthenticationData = {type: 'Basic', securityReq: securityReq}; var authHeader = req.headers['authorization'] ? req.headers['authorization'] : ''; if (authHeader !== '') { var credentialsBase64 = authHeader.substring(authHeader.split('Basic ')[1]); var credentials = base64URL.decode(credentialsBase64).split(':'); - req.bosAuth.authenticationData = {username: credentials[0], password: credentials[1], scheme: 'Basic'}; + req.bosAuthenticationData.username = credentials[0]; + req.bosAuthenticationData.password = credentials[1]; + } + if (!(req.bosAuthenticationData.username && req.bosAuthenticationData.password)) { + res.headers['WWW-Authenticate'] = 'Basic realm="' + securityReq + '"'; + //dont send 401 response yet, as user may want to provide additional info in the response } next(); }; } -function apiKeyExtractor(securityDefn) { - //if there is no apiKey present, send back challenge 401 with www-authenticate header? - //now that I think about this, think this should be handled by user defined code - //because their is no way for us to know how to set the header fields (i.e. realm) +function apiKeyAuthentication(securityReq, securityDefn) { return function (req, res, next) { - //should be form of username="Mufasa", realm="myhost@example.com" - var authorizationHeaders = req.headers['authorization'].split(', '); - var authenticationData = {scheme: 'apiKey'}; - authorizationHeaders.forEach(function (header) { - //should be form of username="Mufasa" - var keyValPair = header.split('='); - authenticationData[keyValPair[0]] = keyValPair[1].substring(1, keyValPair[1].length - 1); - }); + req.bosAuthenticationData = {type: 'apiKey', securityReq: securityReq}; + if (req.headers['authorization']) { + //should be form of username="Mufasa", realm="myhost@example.com" + //treating this like the digest scheme defined in the rfc + var authorizationHeaders = req.headers['authorization'].split(', '); + authorizationHeaders.forEach(function (header) { + //should be form of username="Mufasa" + var keyValPair = header.split('='); + req.bosAuthenticationData[keyValPair[0]] = keyValPair[1].substring(1, keyValPair[1].length - 1); + }); + } if (securityDefn.in === 'query') { - authenticationData.password = req.query[securityDefn.name]; - } else if (securityDefn.in === 'header') { - authenticationData.password = req.headers[securityDefn.name]; - } else { - return log.warn('unknown location %s for apiKey. ' + + req.bosAuthenticationData.password = req.query[securityDefn.name]; + } + else if (securityDefn.in === 'header') { + req.bosAuthenticationData.password = req.headers[securityDefn.name]; + } + else { + log.warn('unknown location %s for apiKey. ' + 'looks like open api specs may have changed on us', securityDefn.in); } - req.authenticationData = authenticationData; + if (!(req.bosAuthenticationData.password)) { + res.headers['WWW-Authenticate'] = 'Digest realm="' + securityReq + '"'; + //dont send 401 response yet, as user may want to provide additional info in the response + } + next(); //this would have to be a user provided function that //fetches the user (and thus the private key that we need to compute the hash) from some data source //we don't need this if we decide that we will let the user figure out how to verify the digest /*verify(apiId, function (user) { - //regenerate hash with apiKey - //hash will include symmetric apiKey, one or more of: - //request method, content-md5 header, request uri, timestamp, socket.remoteAddress, req.ip, ip whitelist? - //if (hash === digest) - // all good - // else you suck - req.bosAuth.user = user; - next(); - });*/ + //regenerate hash with apiKey + //hash will include symmetric apiKey, one or more of: + //request method, content-md5 header, request uri, timestamp, socket.remoteAddress, req.ip, ip whitelist? + //if (hash === digest) + // all good + // else you suck + req.bosAuth.user = user; + next(); + });*/ }; } @@ -175,7 +231,7 @@ function oauth2(route, securityDefn, scopes) { securityDefn.flow, securityDefn.tokenUrl, securityDefn.scopes); oAuthService.addOAuthInstance(route, oAuthInstance); } - oAuthInstance.startOAuth(req, res); + oAuthInstance.startOAuth(req, res, next); }; } From a57556f4e6a40400de2b5d7aa0578d21f242dc4c Mon Sep 17 00:00:00 2001 From: shepp Date: Thu, 13 Oct 2016 13:18:28 -0400 Subject: [PATCH 08/20] temp commit --- examples/swagger/handlers/api-v1.js | 26 +++++++++ examples/swagger/middleware/custom-auth.js | 8 +-- examples/swagger/swagger/api-v1.yaml | 2 + .../public/paths/superfuntime-{id}.yaml | 5 +- .../swagger/public/security-definitions.yaml | 2 +- .../public/x-bos-security-definitions.yaml | 5 ++ middleware/bos-authentication.js | 38 +++++++----- middleware/bos-passport.js | 58 +++++++++++++++++-- package.json | 5 +- services/hmacService.js | 4 ++ 10 files changed, 124 insertions(+), 29 deletions(-) create mode 100644 examples/swagger/swagger/public/x-bos-security-definitions.yaml create mode 100644 services/hmacService.js diff --git a/examples/swagger/handlers/api-v1.js b/examples/swagger/handlers/api-v1.js index 6f51b3c..12b9c09 100644 --- a/examples/swagger/handlers/api-v1.js +++ b/examples/swagger/handlers/api-v1.js @@ -1,3 +1,6 @@ +var sjcl = require('sjcl'), + urlParse = require('url-parse'); + exports.init = function() { }; @@ -14,3 +17,26 @@ exports.getFunTimeById = function(req, res, next) { }); }; +exports.addHmac = function (req, res, next) { + var apiId = '111'; + var apiKey = 'key'; + var contentMd5 = req.headers['Content-MD5'] || ''; + var contentType = req.headers['Content-Type'] || ''; + var dateString = new Date().toString(); + + var urlPath; + if (urlParse) { + urlPath = urlParse(req.url).pathname; + } + var stringToSign = req.method.toUpperCase() + '\n' + + contentMd5 + '\n' + + contentType + '\n' + + dateString + '\n' + + urlPath; + + var key = sjcl.codec.utf8String.toBits(apiKey); + var out = (new sjcl.misc.hmac(key, sjcl.hash.sha256)).mac(stringToSign); + var hmac = sjcl.codec.base64.fromBits(out); + req.headers.Authorization = 'SFI ' + apiId + ':' + hmac + ':' + dateString; +}; + diff --git a/examples/swagger/middleware/custom-auth.js b/examples/swagger/middleware/custom-auth.js index 111fd70..ceb0cac 100644 --- a/examples/swagger/middleware/custom-auth.js +++ b/examples/swagger/middleware/custom-auth.js @@ -2,22 +2,22 @@ exports.init = function(app, logger) { app.use(function (req, res, next) { if (!req.bosAuthenticationData) { - next(); + return next(); } switch (req.bosAuthenticationData.type) { - case 'basic': + case 'Basic': if (!(req.bosAuthenticationData.username && req.bosAuthenticationData.password)) { res.sendStatus(401); } else { - next(); + return next(); } break; case 'apiKey': if (!(req.bosAuthenticationData.password)) { res.sendStatus(401); } else { - next(); + return next(); } break; } diff --git a/examples/swagger/swagger/api-v1.yaml b/examples/swagger/swagger/api-v1.yaml index ddc2272..de1008c 100644 --- a/examples/swagger/swagger/api-v1.yaml +++ b/examples/swagger/swagger/api-v1.yaml @@ -10,6 +10,8 @@ paths: $ref: 'public/paths.yaml' securityDefinitions: $ref: 'public/security-definitions.yaml' +x-bos-securityDefinitions: + $ref: 'public/x-bos-security-definitions.yaml' definitions: Contact: $ref: 'public\definitions\Contact.yaml' diff --git a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml index ec409b6..2132710 100644 --- a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml +++ b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml @@ -1,12 +1,13 @@ get: summary: Get super fun time operationId: getFunTimeById + x-middleware: addHmac tags: - Fun Times parameters: - $ref: '../parameters/PathId.yaml' - security: - - apiKeyEx: [] + x-bos-security: + - hmac responses: 200: schema: diff --git a/examples/swagger/swagger/public/security-definitions.yaml b/examples/swagger/swagger/public/security-definitions.yaml index 6c588b7..c6cbdcf 100644 --- a/examples/swagger/swagger/public/security-definitions.yaml +++ b/examples/swagger/swagger/public/security-definitions.yaml @@ -3,4 +3,4 @@ basicEx: apiKeyEx: type: apiKey in: header - name: x-auth \ No newline at end of file + name: authorization \ No newline at end of file diff --git a/examples/swagger/swagger/public/x-bos-security-definitions.yaml b/examples/swagger/swagger/public/x-bos-security-definitions.yaml new file mode 100644 index 0000000..55612f6 --- /dev/null +++ b/examples/swagger/swagger/public/x-bos-security-definitions.yaml @@ -0,0 +1,5 @@ +hmac: + verify: hmacService.getApiUser + routeOptions: + session: false + module: passport-hmac-strategy diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index 62c7652..415fed4 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -27,8 +27,6 @@ function init(app, config, logger, serviceLoader, swagger) { if (_.contains(httpMethods, method)) { var operation = pathObj[method]; if (operation['security']) { - //need to make this a logical 'OR' - // so that only one security requirement must be satisfied on a route operation['security'].forEach(function (securityReq) { _.forOwn(securityReq, function (scopes, securityDefn) { _applySecurityRequirement(app, method, routePath, securityDefn, @@ -39,8 +37,6 @@ function init(app, config, logger, serviceLoader, swagger) { }); } if (operation['x-bos-security']) { - //need to make this a logical 'OR' - // so that only one security requirement must be satisfied on a route operation['x-bos-security'].forEach(function (securityReq) { _.forOwn(securityReq, function (scopes, securityDefn) { _applyCustomSecurityRequirement(app, method, routePath, securityDefn, @@ -162,17 +158,21 @@ function _applySecurityRequirement(app, method, route, securityReq, function basicAuthentication(securityReq) { return function (req, res, next) { + if (req.bosAuthenticationData && !res.getHeader('WWW-Authenticate')) { //already authenticated + return next(); + } //header should be of the form "Basic " + user:password as a base64 encoded string req.bosAuthenticationData = {type: 'Basic', securityReq: securityReq}; var authHeader = req.headers['authorization'] ? req.headers['authorization'] : ''; if (authHeader !== '') { - var credentialsBase64 = authHeader.substring(authHeader.split('Basic ')[1]); - var credentials = base64URL.decode(credentialsBase64).split(':'); + var credentialsBase64 = authHeader.split('Basic ')[1]; + var credentialsDecoded = base64URL.decode(credentialsBase64); + var credentials = credentialsDecoded.split(':'); req.bosAuthenticationData.username = credentials[0]; req.bosAuthenticationData.password = credentials[1]; } if (!(req.bosAuthenticationData.username && req.bosAuthenticationData.password)) { - res.headers['WWW-Authenticate'] = 'Basic realm="' + securityReq + '"'; + res.setHeader('WWW-Authenticate', 'Basic realm="' + securityReq + '"'); //dont send 401 response yet, as user may want to provide additional info in the response } next(); @@ -181,16 +181,22 @@ function basicAuthentication(securityReq) { function apiKeyAuthentication(securityReq, securityDefn) { return function (req, res, next) { + if (req.bosAuthenticationData && !res.getHeader('WWW-Authenticate')) { //already authenticated + return next(); + } req.bosAuthenticationData = {type: 'apiKey', securityReq: securityReq}; if (req.headers['authorization']) { - //should be form of username="Mufasa", realm="myhost@example.com" - //treating this like the digest scheme defined in the rfc - var authorizationHeaders = req.headers['authorization'].split(', '); - authorizationHeaders.forEach(function (header) { - //should be form of username="Mufasa" - var keyValPair = header.split('='); - req.bosAuthenticationData[keyValPair[0]] = keyValPair[1].substring(1, keyValPair[1].length - 1); - }); + var digestHeader = req.headers['authorization'].split('Digest ')[1]; + if (digestHeader) { + //should be form of username="Mufasa", realm="myhost@example.com" + //treating this like the digest scheme defined in the rfc + var authorizationHeaderFields = digestHeader.split(', '); + authorizationHeaderFields.forEach(function (header) { + //should be form of username="Mufasa" + var keyValPair = header.split('='); + req.bosAuthenticationData[keyValPair[0]] = keyValPair[1].substring(1, keyValPair[1].length - 1); + }); + } } if (securityDefn.in === 'query') { req.bosAuthenticationData.password = req.query[securityDefn.name]; @@ -203,7 +209,7 @@ function apiKeyAuthentication(securityReq, securityDefn) { 'looks like open api specs may have changed on us', securityDefn.in); } if (!(req.bosAuthenticationData.password)) { - res.headers['WWW-Authenticate'] = 'Digest realm="' + securityReq + '"'; + res.setHeader('WWW-Authenticate', 'Digest realm="' + securityReq + '"'); //dont send 401 response yet, as user may want to provide additional info in the response } next(); diff --git a/middleware/bos-passport.js b/middleware/bos-passport.js index ee80e87..86b95c4 100644 --- a/middleware/bos-passport.js +++ b/middleware/bos-passport.js @@ -1,17 +1,65 @@ -var passport = require('passport'); +var _ = require('lodash'), + passport = require('passport'), + subRequire = require('../lib/subRequire'); +var strategyMap; +var cfg; +var loader; module.exports = { - init : init + init : init, + authenticate: authenticate }; -function init(app, config, bosPassport) { +function init(app, config, serviceLoader) { + cfg = config.get('passport'); + loader = serviceLoader; app.use(passport.initialize()); //if sessions are enabled and express session is also being used, //express session middleware MUST be listed first in the middleware config - if (config.get('passport').options.session) { + if (cfg.session) { app.use(passport.session()); } - bosPassport.registerSecurityStrategies(); +} + +function authenticate(strategyId, strategy) { + //load passport strategy module + if (!strategyMap[strategy.module]) { + strategyMap[strategy.module] = subRequire(strategy.module, 'bos-passport').Strategy; + } + _.forEach(_.keys(strategy.options), function (opt) { + strategy.options[opt] = prepareOption(strategy.options[opt]); + }); + _.forEach(_.keys(strategy.routeOptions), function (opt) { + strategy.routeOptions[opt] = prepareOption(strategy.routeOptions[opt]); + }); + strategy.verify = prepareOption(strategy.verify); + passport.use(strategyId, new strategyMap[strategy.module](strategy.options, strategy.verify)); + passport.authenticate(strategyId, strategy.routeOptions); +} + +//fetch the option from the config, or if option is a service method, point to or call the method with supplied args +//maybe make the method args able to be fetched from the config? +//else just return the option +function prepareOption(opt) { + if (typeof opt === 'string') { + var configRegex = /^{{(.*)}}$/; + var result = configRegex.exec(opt); + if (result && result[1]) { + // This is a config-based option + return _.get(cfg, result[1]); + } + } else if (typeof opt === 'object') { + if (opt.service) { + var service = loader.get(opt.service); + var method = service[opt.method.name]; + if (method.execute) { + return method.apply(service, method.args); + } else { + return _.partial.apply(_, [method].concat(method.args)); + } + } + } + return opt; } //need serializeUser and deserializeUser functions for session enabling diff --git a/package.json b/package.json index f516be6..46f8f18 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,8 @@ "node-cache": "^3.0.0", "node-statsd": "^0.1.1", "on-headers": "^1.0.0", + "passport": "^0.3.2", + "passport-hmac-strategy": "^1.2.0", "prompt": "^0.2.14", "raw-body": "^2.0.2", "redis": "^2.4.2", @@ -72,7 +74,8 @@ "eslint": "^3.3.1", "istanbul": "^0.4.1", "mocha": "^3.0.2", - "mocha-jenkins-reporter": "^0.1.8" + "mocha-jenkins-reporter": "^0.1.8", + "sjcl": "^1.0.6" }, "files": [ "bin", diff --git a/services/hmacService.js b/services/hmacService.js new file mode 100644 index 0000000..43961d9 --- /dev/null +++ b/services/hmacService.js @@ -0,0 +1,4 @@ + +exports.getApiUser = function (apiId, done) { + done(null, {name: 'joe', password: 'peschi'}); +}; \ No newline at end of file From 59f4133502f1208ba25dde550160d8ab8cfbd2b0 Mon Sep 17 00:00:00 2001 From: shepp Date: Thu, 13 Oct 2016 14:21:41 -0400 Subject: [PATCH 09/20] wire up passport --- examples/swagger/handlers/api-v1.js | 1 + .../swagger/middleware}/bos-passport.js | 6 +++--- .../swagger/swagger/public/paths/superfuntime-{id}.yaml | 2 +- .../swagger/swagger/public/x-bos-security-definitions.yaml | 7 ++++++- middleware/bos-authentication.js | 3 ++- 5 files changed, 13 insertions(+), 6 deletions(-) rename {middleware => examples/swagger/middleware}/bos-passport.js (93%) diff --git a/examples/swagger/handlers/api-v1.js b/examples/swagger/handlers/api-v1.js index 12b9c09..cfeb51c 100644 --- a/examples/swagger/handlers/api-v1.js +++ b/examples/swagger/handlers/api-v1.js @@ -38,5 +38,6 @@ exports.addHmac = function (req, res, next) { var out = (new sjcl.misc.hmac(key, sjcl.hash.sha256)).mac(stringToSign); var hmac = sjcl.codec.base64.fromBits(out); req.headers.Authorization = 'SFI ' + apiId + ':' + hmac + ':' + dateString; + next(); }; diff --git a/middleware/bos-passport.js b/examples/swagger/middleware/bos-passport.js similarity index 93% rename from middleware/bos-passport.js rename to examples/swagger/middleware/bos-passport.js index 86b95c4..497676c 100644 --- a/middleware/bos-passport.js +++ b/examples/swagger/middleware/bos-passport.js @@ -1,7 +1,7 @@ var _ = require('lodash'), passport = require('passport'), - subRequire = require('../lib/subRequire'); -var strategyMap; + subRequire = require('../../../lib/subRequire'); +var strategyMap= {}; var cfg; var loader; @@ -34,7 +34,7 @@ function authenticate(strategyId, strategy) { }); strategy.verify = prepareOption(strategy.verify); passport.use(strategyId, new strategyMap[strategy.module](strategy.options, strategy.verify)); - passport.authenticate(strategyId, strategy.routeOptions); + return passport.authenticate(strategyId, strategy.routeOptions); } //fetch the option from the config, or if option is a service method, point to or call the method with supplied args diff --git a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml index 2132710..b43d2ca 100644 --- a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml +++ b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml @@ -7,7 +7,7 @@ get: parameters: - $ref: '../parameters/PathId.yaml' x-bos-security: - - hmac + - hmac: [] responses: 200: schema: diff --git a/examples/swagger/swagger/public/x-bos-security-definitions.yaml b/examples/swagger/swagger/public/x-bos-security-definitions.yaml index 55612f6..d2bee21 100644 --- a/examples/swagger/swagger/public/x-bos-security-definitions.yaml +++ b/examples/swagger/swagger/public/x-bos-security-definitions.yaml @@ -1,5 +1,10 @@ hmac: - verify: hmacService.getApiUser + verify: + service: hmacService + method: + name: getApiUser + execute: false routeOptions: session: false module: passport-hmac-strategy + x-bos-middleware: bos-passport diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index 415fed4..beb49c5 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -1,5 +1,6 @@ var _ = require('lodash'); var base64URL = require('base64url'); +var path = require('path'); var log; var loader; @@ -56,7 +57,7 @@ function _applyCustomSecurityRequirement(app, method, route, securityReq, securityDefn, /*requiredPermissions,*/ requiredScopes) { //load security def middleware if (securityDefn['x-bos-middleware']) { - loader.loadConsumerModules('middleware', [securityDefn['x-bos-middleware']]); + loader.loadConsumerModules('middleware', [path.join(global.__appDir, 'middleware', securityDefn['x-bos-middleware'])]); loader.initConsumers('middleware', [securityDefn['x-bos-middleware']], function (err) { if (!err) { var customAuthMiddleware = loader.getConsumer('middleware', securityDefn['x-bos-middleware']); From 06214a5d17fa5f7b7745620e8bc7675438a3c986 Mon Sep 17 00:00:00 2001 From: shepp Date: Fri, 14 Oct 2016 09:13:17 -0400 Subject: [PATCH 10/20] example working for passport-hmac-auth --- examples/swagger/config/default.json | 2 +- examples/swagger/handlers/api-v1.js | 27 +---------------- examples/swagger/middleware/addHmac.js | 30 +++++++++++++++++++ examples/swagger/middleware/bos-passport.js | 6 ++-- examples/swagger/services/hmacService.js | 10 +++++++ .../public/paths/superfuntime-{id}.yaml | 1 - .../public/x-bos-security-definitions.yaml | 1 + middleware/bos-authentication.js | 3 +- services/hmacService.js | 4 --- 9 files changed, 48 insertions(+), 36 deletions(-) create mode 100644 examples/swagger/middleware/addHmac.js create mode 100644 examples/swagger/services/hmacService.js delete mode 100644 services/hmacService.js diff --git a/examples/swagger/config/default.json b/examples/swagger/config/default.json index 5d5563e..dac6c3f 100644 --- a/examples/swagger/config/default.json +++ b/examples/swagger/config/default.json @@ -2,7 +2,7 @@ { "express": { "port": "3000", - "middleware": ["cors", "body-parser", "bos-authentication", "custom-auth"], + "middleware": ["cors", "body-parser", "addHmac", "bos-authentication", "custom-auth"], "middleware$": ["errors"] }, "cors": { diff --git a/examples/swagger/handlers/api-v1.js b/examples/swagger/handlers/api-v1.js index cfeb51c..da0d8a8 100644 --- a/examples/swagger/handlers/api-v1.js +++ b/examples/swagger/handlers/api-v1.js @@ -1,5 +1,3 @@ -var sjcl = require('sjcl'), - urlParse = require('url-parse'); exports.init = function() { @@ -10,34 +8,11 @@ exports.getFunTimeById = function(req, res, next) { 'curiousPeople': [ { 'kind': 'OtherPerson', - 'curiousPersonReqField': 'hey!', + 'curiousPersonReqField': 'hey?', 'enthusiasticPersonReqField': 'hola!' } ] }); }; -exports.addHmac = function (req, res, next) { - var apiId = '111'; - var apiKey = 'key'; - var contentMd5 = req.headers['Content-MD5'] || ''; - var contentType = req.headers['Content-Type'] || ''; - var dateString = new Date().toString(); - - var urlPath; - if (urlParse) { - urlPath = urlParse(req.url).pathname; - } - var stringToSign = req.method.toUpperCase() + '\n' + - contentMd5 + '\n' + - contentType + '\n' + - dateString + '\n' + - urlPath; - - var key = sjcl.codec.utf8String.toBits(apiKey); - var out = (new sjcl.misc.hmac(key, sjcl.hash.sha256)).mac(stringToSign); - var hmac = sjcl.codec.base64.fromBits(out); - req.headers.Authorization = 'SFI ' + apiId + ':' + hmac + ':' + dateString; - next(); -}; diff --git a/examples/swagger/middleware/addHmac.js b/examples/swagger/middleware/addHmac.js new file mode 100644 index 0000000..823844d --- /dev/null +++ b/examples/swagger/middleware/addHmac.js @@ -0,0 +1,30 @@ +var sjcl = require('sjcl'), + urlParse = require('url-parse'); + +exports.init = function (app) { + app.use(function (req, res, next) { + if (req.path.indexOf('superfuntime') !== -1) { + var apiId = '285cd308-1564-4090-b3b4-ce5cfa697c4c'; + var apiKey = 'JfOJ15SI7EGjDLX1h8zPB19Zr88ONMPKbBQJozMI0Ag'; + var contentMd5 = req.headers['Content-MD5'] || ''; + var contentType = req.headers['Content-Type'] || ''; + var dateString = new Date().toString(); + + var urlPath; + if (urlParse) { + urlPath = urlParse(req.url).pathname; + } + var stringToSign = req.method.toUpperCase() + '\n' + + contentMd5 + '\n' + + contentType + '\n' + + dateString + '\n' + + urlPath; + + var key = sjcl.codec.utf8String.toBits(apiKey); + var out = (new sjcl.misc.hmac(key, sjcl.hash.sha256)).mac(stringToSign); + var hmac = sjcl.codec.base64.fromBits(out); + req.headers.Authorization = 'SFI ' + apiId + ':' + hmac + ':' + dateString; + } + next(); + }); +}; diff --git a/examples/swagger/middleware/bos-passport.js b/examples/swagger/middleware/bos-passport.js index 497676c..35a39de 100644 --- a/examples/swagger/middleware/bos-passport.js +++ b/examples/swagger/middleware/bos-passport.js @@ -52,10 +52,10 @@ function prepareOption(opt) { if (opt.service) { var service = loader.get(opt.service); var method = service[opt.method.name]; - if (method.execute) { - return method.apply(service, method.args); + if (opt.method.execute) { + return method.apply(service, opt.method.args); } else { - return _.partial.apply(_, [method].concat(method.args)); + return _.partial.apply(_, [method].concat(opt.method.args)); } } } diff --git a/examples/swagger/services/hmacService.js b/examples/swagger/services/hmacService.js new file mode 100644 index 0000000..e9d8bea --- /dev/null +++ b/examples/swagger/services/hmacService.js @@ -0,0 +1,10 @@ + +exports.init = function () { + +}; + +exports.getApiUser = function (apiId, done) { + done(null, {name: 'joe', getApiKey: function () { + return 'JfOJ15SI7EGjDLX1h8zPB19Zr88ONMPKbBQJozMI0Ag'; + }}); +}; \ No newline at end of file diff --git a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml index b43d2ca..a8c6e52 100644 --- a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml +++ b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml @@ -1,7 +1,6 @@ get: summary: Get super fun time operationId: getFunTimeById - x-middleware: addHmac tags: - Fun Times parameters: diff --git a/examples/swagger/swagger/public/x-bos-security-definitions.yaml b/examples/swagger/swagger/public/x-bos-security-definitions.yaml index d2bee21..9f915ac 100644 --- a/examples/swagger/swagger/public/x-bos-security-definitions.yaml +++ b/examples/swagger/swagger/public/x-bos-security-definitions.yaml @@ -4,6 +4,7 @@ hmac: method: name: getApiUser execute: false + args: [] routeOptions: session: false module: passport-hmac-strategy diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index beb49c5..a74e422 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -57,7 +57,8 @@ function _applyCustomSecurityRequirement(app, method, route, securityReq, securityDefn, /*requiredPermissions,*/ requiredScopes) { //load security def middleware if (securityDefn['x-bos-middleware']) { - loader.loadConsumerModules('middleware', [path.join(global.__appDir, 'middleware', securityDefn['x-bos-middleware'])]); + loader.loadConsumerModules('middleware', + [path.join(global.__appDir, 'middleware', securityDefn['x-bos-middleware'])]); loader.initConsumers('middleware', [securityDefn['x-bos-middleware']], function (err) { if (!err) { var customAuthMiddleware = loader.getConsumer('middleware', securityDefn['x-bos-middleware']); diff --git a/services/hmacService.js b/services/hmacService.js deleted file mode 100644 index 43961d9..0000000 --- a/services/hmacService.js +++ /dev/null @@ -1,4 +0,0 @@ - -exports.getApiUser = function (apiId, done) { - done(null, {name: 'joe', password: 'peschi'}); -}; \ No newline at end of file From 513f15c26a8bc7e671ce756bb70afb3030170774 Mon Sep 17 00:00:00 2001 From: shepp Date: Mon, 17 Oct 2016 15:00:28 -0400 Subject: [PATCH 11/20] oauth POC --- examples/swagger/config/default.json | 15 ++- .../oauth-provider-get-access-token.js | 20 +++ .../handlers/oauth-provider-get-auth-code.js | 9 ++ examples/swagger/middleware/bos-passport.js | 5 +- .../public/paths/superfuntime-{id}.yaml | 6 +- .../swagger/public/security-definitions.yaml | 13 +- handlers/_oauth-redirect.js | 16 +-- middleware/bos-authentication.js | 52 +++++--- services/oauth2.js | 117 ++++++++++++------ 9 files changed, 174 insertions(+), 79 deletions(-) create mode 100644 examples/swagger/handlers/oauth-provider-get-access-token.js create mode 100644 examples/swagger/handlers/oauth-provider-get-auth-code.js diff --git a/examples/swagger/config/default.json b/examples/swagger/config/default.json index dac6c3f..7ad8ee8 100644 --- a/examples/swagger/config/default.json +++ b/examples/swagger/config/default.json @@ -2,8 +2,8 @@ { "express": { "port": "3000", - "middleware": ["cors", "body-parser", "addHmac", "bos-authentication", "custom-auth"], - "middleware$": ["errors"] + "middleware": ["cors", "body-parser", "session", "addHmac", "bos-authentication", "custom-auth"], + "middleware$": [] }, "cors": { "origin": "*" @@ -15,6 +15,17 @@ "cluster": { "maxWorkers": 1 + }, + + "session": { + "keys": ["sessionKey"] + }, + + "oauth": { + "clientId": "826839015121-rs7t7ick1ib0uvui9iihbb82gcqg64r9.apps.googleusercontent.com", + "clientSecret": "secret", + "redirectURI": "/oauth-redirect" } + } \ No newline at end of file diff --git a/examples/swagger/handlers/oauth-provider-get-access-token.js b/examples/swagger/handlers/oauth-provider-get-access-token.js new file mode 100644 index 0000000..7821ec5 --- /dev/null +++ b/examples/swagger/handlers/oauth-provider-get-access-token.js @@ -0,0 +1,20 @@ + +var sendAccessToken = function (req, res) { + res.json({accessToken: 'access'}); +}; + +var redirectWithAccessToken = function (req, res) { + /* + A real oauth provider would use this in the implicit flow to validate user credentials, + authorize the app to access whatever scopes were requested. Then it would redirect to + the redirect_uri (present in this request), adding the access token as a uri fragment. + User defined code must handle this redirect with some client side javascript to extract the + token from the uri fragment. + */ + res.sendStatus(200); +}; + +exports.init = function (app) { + app.post('/access-token', sendAccessToken); + app.get('/access-token', redirectWithAccessToken); +}; diff --git a/examples/swagger/handlers/oauth-provider-get-auth-code.js b/examples/swagger/handlers/oauth-provider-get-auth-code.js new file mode 100644 index 0000000..2dfc606 --- /dev/null +++ b/examples/swagger/handlers/oauth-provider-get-auth-code.js @@ -0,0 +1,9 @@ +var sendAuthCode = function (req, res, next) { + res.statusCode = 302; + res.setHeader('location', req.query.redirect_uri + '?state=' + req.query.state + '&code=authcode'); + res.send(); +}; + +exports.init = function (app) { + app.get('/auth-code', sendAuthCode); +}; diff --git a/examples/swagger/middleware/bos-passport.js b/examples/swagger/middleware/bos-passport.js index 35a39de..fcf7cdc 100644 --- a/examples/swagger/middleware/bos-passport.js +++ b/examples/swagger/middleware/bos-passport.js @@ -52,10 +52,11 @@ function prepareOption(opt) { if (opt.service) { var service = loader.get(opt.service); var method = service[opt.method.name]; + var args = opt.method.args ? opt.method.args : []; if (opt.method.execute) { - return method.apply(service, opt.method.args); + return method.apply(service, args); } else { - return _.partial.apply(_, [method].concat(opt.method.args)); + return _.partial.apply(_, [method].concat(args)); } } } diff --git a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml index a8c6e52..2842348 100644 --- a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml +++ b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml @@ -5,8 +5,10 @@ get: - Fun Times parameters: - $ref: '../parameters/PathId.yaml' - x-bos-security: - - hmac: [] + security: + - oauthEx: [] + #x-bos-security: + #- hmac: [] responses: 200: schema: diff --git a/examples/swagger/swagger/public/security-definitions.yaml b/examples/swagger/swagger/public/security-definitions.yaml index c6cbdcf..85a62e4 100644 --- a/examples/swagger/swagger/public/security-definitions.yaml +++ b/examples/swagger/swagger/public/security-definitions.yaml @@ -3,4 +3,15 @@ basicEx: apiKeyEx: type: apiKey in: header - name: authorization \ No newline at end of file + name: authorization +oauthEx: + type: oauth2 + flow: accessCode + authorizationUrl: http://localhost:3000/auth-code + tokenUrl: http://localhost:3000/access-token + scopes: {} +oauthImplicitEx: + type: oauth2 + flow: implicit + authorizationUrl: http://localhost:3000/access-token + scopes: {} \ No newline at end of file diff --git a/handlers/_oauth-redirect.js b/handlers/_oauth-redirect.js index 95579e2..f03001a 100644 --- a/handlers/_oauth-redirect.js +++ b/handlers/_oauth-redirect.js @@ -11,19 +11,5 @@ function init(app, oauth2) { } function handleRedirect(req, res) { - if (!req.query.code) { - //should have auth code at this point - } else if (!oauthService.getRequestState(req.query.state)) {//check for XSRF - //log warning about possible xsrf attack - } else { - oauthService.getTokenData(req, res, function (tokenData) { - req.authenticationData = tokenData; - var originalState = oauthService.getRequestState(req.query.state); - _.merge(req, originalState.req); - _.merge(res, originalState.res); - //this should make it so that an auth code will only get used once - oauthService.deleteRequestState(req.query.state); - originalState.next(); - }); - } + oauthService.accessCodeRedirect(req, res); } diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index a74e422..3f7d331 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -79,7 +79,10 @@ function _applyCustomSecurityRequirement(app, method, route, securityReq, } }); } else { - log.warn('No custom middleware defined for security defn %s', securityReq); + log.info('No custom middleware defined for security defn %s. ' + + 'Attempting to use built in middleware...', securityReq); + _applySecurityRequirement(app, method, route, securityReq, + securityDefn, requiredScopes); } } @@ -98,13 +101,13 @@ function _applySecurityRequirement(app, method, route, securityReq, app[method].call(app, route, apiKeyAuthentication(securityReq, securityDefn)); break; case 'oauth2': - /*if (!oAuthService) { - oAuthService = loader.get('oauth2'); - } - app[method].call(app, route, oauth2(route, securityDefn, requiredScopes));*/ - log.warn('No out of the box oauth2 implementation exists in BOS. ' + + if (!oAuthService) { + oAuthService = loader.get('oauth2'); + } + app[method].call(app, route, oauth2(securityReq, securityDefn, requiredScopes)); + /*log.warn('No out of the box oauth2 implementation exists in BOS. ' + 'You must define your own and reference it in the ' + - '"x-bos-middleware" property of the security definition %s', securityReq); + '"x-bos-middleware" property of the security definition %s', securityReq);*/ break; default: return log.warn('unrecognized security type %s for security definition %s' + @@ -165,7 +168,7 @@ function basicAuthentication(securityReq) { } //header should be of the form "Basic " + user:password as a base64 encoded string req.bosAuthenticationData = {type: 'Basic', securityReq: securityReq}; - var authHeader = req.headers['authorization'] ? req.headers['authorization'] : ''; + var authHeader = req.get('authorization') ? req.get('authorization') : ''; if (authHeader !== '') { var credentialsBase64 = authHeader.split('Basic ')[1]; var credentialsDecoded = base64URL.decode(credentialsBase64); @@ -187,8 +190,8 @@ function apiKeyAuthentication(securityReq, securityDefn) { return next(); } req.bosAuthenticationData = {type: 'apiKey', securityReq: securityReq}; - if (req.headers['authorization']) { - var digestHeader = req.headers['authorization'].split('Digest ')[1]; + if (req.get('authorization')) { + var digestHeader = req.get('authorization').split('Digest ')[1]; if (digestHeader) { //should be form of username="Mufasa", realm="myhost@example.com" //treating this like the digest scheme defined in the rfc @@ -204,7 +207,7 @@ function apiKeyAuthentication(securityReq, securityDefn) { req.bosAuthenticationData.password = req.query[securityDefn.name]; } else if (securityDefn.in === 'header') { - req.bosAuthenticationData.password = req.headers[securityDefn.name]; + req.bosAuthenticationData.password = req.get(securityDefn.name); } else { log.warn('unknown location %s for apiKey. ' + @@ -231,15 +234,32 @@ function apiKeyAuthentication(securityReq, securityDefn) { }; } -function oauth2(route, securityDefn, scopes) { +function oauth2(securityReq, securityDefn, scopes) { return function (req, res, next) { - var oAuthInstance = oAuthService.getOAuthInstance(route); + if (securityDefn.flow === 'accessCode') { + if (!req.session) { + log.error('oauth requires that session be enabled'); + return next(); + } + if (req.session.bosAuthenticationData) { //already authenticated + return next(); + } + } else if (req.get('authorization')) { //implicit + req.bosAuthenticationData.password = + req.get('authorization').split('Bearer ')[1]; //we assume bearer token type which is the most common + if (req.bosAuthenticationData.password) { //already authenticated + //user defined code will be responsible for validating this token + //which they absolutely should do because it did not come directly from oauth provider + return next(); + } + } + var oAuthInstance = oAuthService.getOAuthInstance(securityReq); if (!oAuthInstance) { oAuthInstance = new oAuthService.OAuth2(securityDefn.authorizationUrl, - securityDefn.flow, securityDefn.tokenUrl, securityDefn.scopes); - oAuthService.addOAuthInstance(route, oAuthInstance); + securityDefn.flow, securityDefn.tokenUrl); + oAuthService.addOAuthInstance(securityReq, oAuthInstance); } - oAuthInstance.startOAuth(req, res, next); + oAuthInstance.startOAuth(securityReq, scopes, req, res); }; } diff --git a/services/oauth2.js b/services/oauth2.js index d8b13c2..ece781d 100644 --- a/services/oauth2.js +++ b/services/oauth2.js @@ -1,75 +1,111 @@ -var _ = require('lodash'), - request = require('request'), - jwt = require('jsonwebtoken'); +var request = require('request'); var cfg; var clientId; var clientSecret; -var redirectUri; -//map of route to oauth2 instance in order to maintain state between requests during oauth process +var redirectURI; +var implicitRedirectUri; +//map of security req to oauth2 instance var routeOAuthMap = {}; -var requestStateMap = {}; module.exports = { init : init, - OAuth2: OAuth2 + OAuth2: OAuth2, + accessCodeRedirect: accessCodeRedirect, + addOAuthInstance: addOAuthInstance, + getOAuthInstance: getOAuthInstance }; -//look for token, if is there and hasn't expired then boom. If it has expired then refresh token -//look for authorization code, if not there redirect to authorization url with required scopes -//get auth code, send request to token url with auth code to get back token -function init(config) { +function addOAuthInstance (securityReq, oauthInstance) { + routeOAuthMap[securityReq] = oauthInstance; +} + +function getOAuthInstance (securityReq) { + return routeOAuthMap[securityReq]; +} + +function init(config, logger) { cfg = config.get('oauth'); - clientId = cfg.clientId; - clientSecret = cfg.clientSecret; - redirectUri = cfg.redirectUri; + if (cfg) { + clientId = cfg.clientId; + clientSecret = cfg.clientSecret; + redirectURI = cfg.redirectURI; + implicitRedirectUri = cfg.implicitRedirectUri; + if (config.get('express').middleware.indexOf('session') < 0) { + logger.warn('oauth requires that session be enabled.'); + } + } } -function OAuth2(authorizationUrl, flow, tokenUrl, scopes) { +function OAuth2(authorizationUrl, flow, tokenUrl) { this.authorizationUrl = authorizationUrl; this.flow = flow; this.tokenUrl = tokenUrl; - this.scopes = scopes; + this.stateIds = {}; } -function redirectToAuthorizationUrl (req, res, stateId) { - var queryString = '?response_type=code&requestclientId='; +function accessCodeRedirect(req, res) { + var securityReq = req.query.state.split('-')[0]; + var oauth = getOAuthInstance(securityReq); + if (!req.query.code) { + //should have auth code at this point + } else if (!oauth.isValidState(req.query.state)) {//check for XSRF + //log warning about possible xsrf attack + } else { + oauth.getTokenData(req, res, function (tokenData) { + req.session.bosAuthenticationData = tokenData; + req.sessionOptions.httpOnly = false; //needs to be set for CORS + //this should make it so that an auth code will only get used once + oauth.deleteRequestState(req.query.state); + res.sendStatus(200); + }); + } +} + +OAuth2.prototype.redirectToAuthorizationUrl = function (req, res, scopes, stateId) { + var queryString = '?response_type=code&client_id='; queryString += clientId; - queryString += '&redirect_uri=' + redirectUri; - queryString += '&scope=' + this.scopes.join(' '); + queryString += '&redirect_uri=' + redirectURI; + queryString += '&scope=' + scopes.join(' '); queryString += '&state=' + stateId; - res.statusCode = 302; - res.headers['location'] = this.authorizationUrl + queryString; + res.status(302); + res.setHeader('location', this.authorizationUrl + queryString); res.send(); -} +}; -function addRequestState (stateId, req, res, next) { - requestStateMap[stateId] = {req: req, res: res, next: next}; -} +OAuth2.prototype.redirectToAuthorizationUrlImplicit = function (req, res, scopes, stateId) { + var queryString = '?response_type=token&client_id='; + queryString += clientId; + queryString += '&redirect_uri=' + implicitRedirectUri; + queryString += '&scope=' + scopes.join(' '); + queryString += '&state=' + stateId; + res.status(302); + res.setHeader('location', this.authorizationUrl + queryString); + res.send(); +}; -OAuth2.prototype.startOAuth = function (req, res, next) { +OAuth2.prototype.startOAuth = function (securityReq, scopes, req, res) { + var stateId = securityReq + '-' + Math.random(); if (this.flow === 'accessCode') { - redirectToAuthorizationUrl(req, res); + this.addRequestState(stateId, req, res); + this.redirectToAuthorizationUrl(req, res, scopes, stateId); + } else if (this.flow === 'implicit') { //user defined redirect should validate the state parameter + this.redirectToAuthorizationUrlImplicit(req, res, scopes, stateId); } else { //unsupported flow } - addRequestState(Math.random(), req, res, next); -}; - -OAuth2.prototype.addOAuthInstance = function (route, oauthInstance) { - routeOAuthMap[route] = oauthInstance; }; -OAuth2.prototype.getOAuthInstance = function (route) { - return routeOAuthMap[route]; +OAuth2.prototype.addRequestState = function (stateId) { + this.stateIds[stateId] = true; }; -OAuth2.prototype.getRequestState = function (stateId) { - return requestStateMap[stateId]; +OAuth2.prototype.isValidState = function (stateId) { + return this.stateIds[stateId]; }; OAuth2.prototype.deleteRequestState = function (stateId) { - requestStateMap[stateId] = undefined; + this.stateIds[stateId] = undefined; }; OAuth2.prototype.getTokenData = function (req, res, callback) { @@ -77,7 +113,7 @@ OAuth2.prototype.getTokenData = function (req, res, callback) { form.code = req.query.code; form.clientId = clientId; form.clientSecret = clientSecret; - form.redirectUri = redirectUri; + form.redirectURI = redirectURI; request.post({url: this.tokenUrl, form: form}, function (err, resp, body) { /*check response status*/ //we will let user defined middleware take it from here. @@ -88,6 +124,5 @@ OAuth2.prototype.getTokenData = function (req, res, callback) { }); }; -function authenticate() { -} + From 67bdb704c2d1da8d3b454e0a08b7ed73878fac8a Mon Sep 17 00:00:00 2001 From: shepp Date: Mon, 17 Oct 2016 16:24:50 -0400 Subject: [PATCH 12/20] get oauth implicit flow example to work with swagger ui --- .../handlers/oauth-provider-get-access-token.js | 5 ++++- examples/swagger/middleware/custom-auth.js | 7 +++++++ .../swagger/public/paths/superfuntime-{id}.yaml | 2 +- middleware/bos-authentication.js | 15 +-------------- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/examples/swagger/handlers/oauth-provider-get-access-token.js b/examples/swagger/handlers/oauth-provider-get-access-token.js index 7821ec5..e7d0606 100644 --- a/examples/swagger/handlers/oauth-provider-get-access-token.js +++ b/examples/swagger/handlers/oauth-provider-get-access-token.js @@ -11,7 +11,10 @@ var redirectWithAccessToken = function (req, res) { User defined code must handle this redirect with some client side javascript to extract the token from the uri fragment. */ - res.sendStatus(200); + res.statusCode = 302; + res.setHeader('location', req.query.redirect_uri + '#state=' + req.query.state + + '&token_type=bearer&access_token=access_token'); + res.send(); }; exports.init = function (app) { diff --git a/examples/swagger/middleware/custom-auth.js b/examples/swagger/middleware/custom-auth.js index ceb0cac..2b3ad94 100644 --- a/examples/swagger/middleware/custom-auth.js +++ b/examples/swagger/middleware/custom-auth.js @@ -20,6 +20,13 @@ exports.init = function(app, logger) { return next(); } break; + case 'oauth2': + if (!(req.bosAuthenticationData.password)) { + res.sendStatus(401); + } else { + return next(); + } + break; } }); }; diff --git a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml index 2842348..c3f98a3 100644 --- a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml +++ b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml @@ -6,7 +6,7 @@ get: parameters: - $ref: '../parameters/PathId.yaml' security: - - oauthEx: [] + - oauthImplicitEx: [] #x-bos-security: #- hmac: [] responses: diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index 3f7d331..bb57148 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -163,9 +163,6 @@ function _applySecurityRequirement(app, method, route, securityReq, function basicAuthentication(securityReq) { return function (req, res, next) { - if (req.bosAuthenticationData && !res.getHeader('WWW-Authenticate')) { //already authenticated - return next(); - } //header should be of the form "Basic " + user:password as a base64 encoded string req.bosAuthenticationData = {type: 'Basic', securityReq: securityReq}; var authHeader = req.get('authorization') ? req.get('authorization') : ''; @@ -176,19 +173,12 @@ function basicAuthentication(securityReq) { req.bosAuthenticationData.username = credentials[0]; req.bosAuthenticationData.password = credentials[1]; } - if (!(req.bosAuthenticationData.username && req.bosAuthenticationData.password)) { - res.setHeader('WWW-Authenticate', 'Basic realm="' + securityReq + '"'); - //dont send 401 response yet, as user may want to provide additional info in the response - } next(); }; } function apiKeyAuthentication(securityReq, securityDefn) { return function (req, res, next) { - if (req.bosAuthenticationData && !res.getHeader('WWW-Authenticate')) { //already authenticated - return next(); - } req.bosAuthenticationData = {type: 'apiKey', securityReq: securityReq}; if (req.get('authorization')) { var digestHeader = req.get('authorization').split('Digest ')[1]; @@ -213,10 +203,6 @@ function apiKeyAuthentication(securityReq, securityDefn) { log.warn('unknown location %s for apiKey. ' + 'looks like open api specs may have changed on us', securityDefn.in); } - if (!(req.bosAuthenticationData.password)) { - res.setHeader('WWW-Authenticate', 'Digest realm="' + securityReq + '"'); - //dont send 401 response yet, as user may want to provide additional info in the response - } next(); //this would have to be a user provided function that //fetches the user (and thus the private key that we need to compute the hash) from some data source @@ -236,6 +222,7 @@ function apiKeyAuthentication(securityReq, securityDefn) { function oauth2(securityReq, securityDefn, scopes) { return function (req, res, next) { + req.bosAuthenticationData = {type: 'oauth2', securityReq: securityReq}; if (securityDefn.flow === 'accessCode') { if (!req.session) { log.error('oauth requires that session be enabled'); From df71547510fcab436fc649244ca2274cce110592 Mon Sep 17 00:00:00 2001 From: shepp Date: Tue, 18 Oct 2016 11:40:49 -0400 Subject: [PATCH 13/20] cleanup --- .../swagger/handlers}/_oauth-redirect.js | 1 - .../public/paths/superfuntime-{id}.yaml | 10 ++- services/bosPassport.js | 67 ------------------- 3 files changed, 8 insertions(+), 70 deletions(-) rename {handlers => examples/swagger/handlers}/_oauth-redirect.js (90%) delete mode 100644 services/bosPassport.js diff --git a/handlers/_oauth-redirect.js b/examples/swagger/handlers/_oauth-redirect.js similarity index 90% rename from handlers/_oauth-redirect.js rename to examples/swagger/handlers/_oauth-redirect.js index f03001a..e28dd79 100644 --- a/handlers/_oauth-redirect.js +++ b/examples/swagger/handlers/_oauth-redirect.js @@ -1,4 +1,3 @@ -var _ = require('lodash'); var oauthService; module.exports = { diff --git a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml index c3f98a3..43d2f23 100644 --- a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml +++ b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml @@ -5,8 +5,14 @@ get: - Fun Times parameters: - $ref: '../parameters/PathId.yaml' - security: - - oauthImplicitEx: [] + #security: + #- oauthImplicitEx: [] + #security: + #- oauthEx: [] + #security: + #- basicEx: [] + #security: + #- apiKeyEx: [] #x-bos-security: #- hmac: [] responses: diff --git a/services/bosPassport.js b/services/bosPassport.js deleted file mode 100644 index 012bdc6..0000000 --- a/services/bosPassport.js +++ /dev/null @@ -1,67 +0,0 @@ -var _ = require('lodash'), - passport = require('passport'), - subRequire = require('../lib/subRequire'); - -var strategyMap; -var cfg; -var loader; - -module.exports = { - init : init, - registerSecurityStrategies: registerSecurityStrategies, - authenticate: authenticate -}; - -function init(config, serviceLoader) { - cfg = config.get('passport'); - loader = serviceLoader; -} - -function registerSecurityStrategies() { - var strategies = cfg.strategies; - _.forEach(strategies, function (strategy) { - //load passport strategy module - if (!strategyMap[strategy.module]) { - strategyMap[strategy.module] = subRequire(strategy.module, 'bos-passport').Strategy; - } - _.forEach(_.keys(strategy.options), function (opt) { - strategy.options[opt] = prepareOption(strategy.options[opt]); - }); - //strategy.id MUST be the same as the name of a security requirement in the swagger spec - passport.use(strategy.id, new strategyMap[strategy.module](strategy.options, strategy.verify)); - }); -} - -//securityReq -> strategyId should be a one to one mapping -function authenticate(securityReq) { - _.forEach(_.keys(cfg.options), function (opt) { - cfg.options[opt] = prepareOption(cfg.options[opt]); - }); - return passport.authenticate(securityReq, cfg.options); -} - -//fetch the option from the config, or if option is a service method, point to or call the method with supplied args -//maybe make the method args able to be fetched from the config? -//else just return the option -function prepareOption(opt) { - if (typeof opt === 'string') { - var configRegex = /^{{(.*)}}$/; - var result = configRegex.exec(opt); - if (result && result[1]) { - // This is a config-based option - return _.get(cfg, result[1]); - } - } else if (typeof opt === 'object') { - if (opt.service) { - var service = loader.get(opt.service); - var method = service[opt.method.name]; - if (method.execute) { - return method.apply(service, method.args); - } else { - return _.partial.apply(_, [method].concat(method.args)); - } - } - } - return opt; -} - From 43488c569eb2ad81de2742cb0d3980389ba532e4 Mon Sep 17 00:00:00 2001 From: shepp Date: Wed, 19 Oct 2016 09:36:11 -0400 Subject: [PATCH 14/20] add securityDefn to authData --- examples/swagger/middleware/custom-auth.js | 16 +++++++++++++--- middleware/bos-authentication.js | 5 +++-- services/oauth2.js | 19 ++++++++++--------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/examples/swagger/middleware/custom-auth.js b/examples/swagger/middleware/custom-auth.js index 2b3ad94..e8bc32f 100644 --- a/examples/swagger/middleware/custom-auth.js +++ b/examples/swagger/middleware/custom-auth.js @@ -21,10 +21,20 @@ exports.init = function(app, logger) { } break; case 'oauth2': - if (!(req.bosAuthenticationData.password)) { - res.sendStatus(401); + if (req.bosAuthenticationData.securityDefn.flow === 'implicit') { + if (!(req.bosAuthenticationData.password)) { + res.sendStatus(401); + } + else { + return next(); + } } else { - return next(); + if (!(req.session.bosAuthenticationData)) { + res.sendStatus(401); + } + else { + return next(); + } } break; } diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index bb57148..099aa31 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -168,6 +168,7 @@ function basicAuthentication(securityReq) { var authHeader = req.get('authorization') ? req.get('authorization') : ''; if (authHeader !== '') { var credentialsBase64 = authHeader.split('Basic ')[1]; + //look into decodeuricomponent var credentialsDecoded = base64URL.decode(credentialsBase64); var credentials = credentialsDecoded.split(':'); req.bosAuthenticationData.username = credentials[0]; @@ -179,7 +180,7 @@ function basicAuthentication(securityReq) { function apiKeyAuthentication(securityReq, securityDefn) { return function (req, res, next) { - req.bosAuthenticationData = {type: 'apiKey', securityReq: securityReq}; + req.bosAuthenticationData = {type: 'apiKey', securityReq: securityReq, securityDefn: securityDefn}; if (req.get('authorization')) { var digestHeader = req.get('authorization').split('Digest ')[1]; if (digestHeader) { @@ -222,7 +223,6 @@ function apiKeyAuthentication(securityReq, securityDefn) { function oauth2(securityReq, securityDefn, scopes) { return function (req, res, next) { - req.bosAuthenticationData = {type: 'oauth2', securityReq: securityReq}; if (securityDefn.flow === 'accessCode') { if (!req.session) { log.error('oauth requires that session be enabled'); @@ -232,6 +232,7 @@ function oauth2(securityReq, securityDefn, scopes) { return next(); } } else if (req.get('authorization')) { //implicit + req.bosAuthenticationData = {type: 'oauth2', securityReq: securityReq, securityDefn: securityDefn}; req.bosAuthenticationData.password = req.get('authorization').split('Bearer ')[1]; //we assume bearer token type which is the most common if (req.bosAuthenticationData.password) { //already authenticated diff --git a/services/oauth2.js b/services/oauth2.js index ece781d..4dfdf01 100644 --- a/services/oauth2.js +++ b/services/oauth2.js @@ -4,9 +4,8 @@ var cfg; var clientId; var clientSecret; var redirectURI; -var implicitRedirectUri; //map of security req to oauth2 instance -var routeOAuthMap = {}; +var secRecOAuthMap = {}; module.exports = { init : init, @@ -17,11 +16,11 @@ module.exports = { }; function addOAuthInstance (securityReq, oauthInstance) { - routeOAuthMap[securityReq] = oauthInstance; + secRecOAuthMap[securityReq] = oauthInstance; } function getOAuthInstance (securityReq) { - return routeOAuthMap[securityReq]; + return secRecOAuthMap[securityReq]; } function init(config, logger) { @@ -30,7 +29,6 @@ function init(config, logger) { clientId = cfg.clientId; clientSecret = cfg.clientSecret; redirectURI = cfg.redirectURI; - implicitRedirectUri = cfg.implicitRedirectUri; if (config.get('express').middleware.indexOf('session') < 0) { logger.warn('oauth requires that session be enabled.'); } @@ -53,8 +51,10 @@ function accessCodeRedirect(req, res) { //log warning about possible xsrf attack } else { oauth.getTokenData(req, res, function (tokenData) { - req.session.bosAuthenticationData = tokenData; - req.sessionOptions.httpOnly = false; //needs to be set for CORS + req.session.bosAuthenticationData = {type: 'oauth2', securityReq: securityReq, + securityDefn: {authorizationUrl: oauth.authorizationUrl, tokenUrl: oauth.tokenUrl, flow: 'accessCode'}}; + req.session.bosAuthenticationData.tokenData = tokenData; + //req.sessionOptions.httpOnly = false; //needs to be set for CORS //this should make it so that an auth code will only get used once oauth.deleteRequestState(req.query.state); res.sendStatus(200); @@ -74,14 +74,15 @@ OAuth2.prototype.redirectToAuthorizationUrl = function (req, res, scopes, stateI }; OAuth2.prototype.redirectToAuthorizationUrlImplicit = function (req, res, scopes, stateId) { - var queryString = '?response_type=token&client_id='; + /*var queryString = '?response_type=token&client_id='; queryString += clientId; queryString += '&redirect_uri=' + implicitRedirectUri; queryString += '&scope=' + scopes.join(' '); queryString += '&state=' + stateId; res.status(302); res.setHeader('location', this.authorizationUrl + queryString); - res.send(); + res.send();*/ + res.sendStatus(401); }; OAuth2.prototype.startOAuth = function (securityReq, scopes, req, res) { From e8204955f677e259fc1bcb07e65a32badb922076 Mon Sep 17 00:00:00 2001 From: shepp Date: Mon, 24 Oct 2016 16:10:21 -0400 Subject: [PATCH 15/20] added support for multiple security requirements on a route, tests for basic and apiKey --- examples/swagger/middleware/custom-auth.js | 57 +++---- .../public/paths/superfuntime-{id}.yaml | 5 +- .../swagger/public/security-definitions.yaml | 2 +- middleware/bos-authentication.js | 62 +++++--- services/oauth2.js | 2 - .../fixtures/serverAuth/config/default.json | 31 ++++ .../fixtures/serverAuth/handlers/api-v1.js | 26 ++++ .../serverAuth/middleware/custom-auth.js | 43 ++++++ .../fixtures/serverAuth/swagger/api-v1.yaml | 28 ++++ .../swagger/public/definitions/Contact.yaml | 13 ++ .../public/definitions/CuriousPerson.yaml | 13 ++ .../definitions/EnthusiasticPerson.yaml | 14 ++ .../public/definitions/OtherPerson.yaml | 14 ++ .../swagger/public/definitions/Person.yaml | 10 ++ .../public/definitions/SuperFunTime.yaml | 8 + .../swagger/public/parameters/PathId.yaml | 6 + .../serverAuth/swagger/public/paths.yaml | 3 + .../public/paths/superfuntime-{id}.yaml | 63 ++++++++ .../swagger/public/security-definitions.yaml | 17 +++ .../public/x-bos-security-definitions.yaml | 11 ++ test/integration/testAuth.js | 144 ++++++++++++++++++ 21 files changed, 522 insertions(+), 50 deletions(-) create mode 100644 test/integration/fixtures/serverAuth/config/default.json create mode 100644 test/integration/fixtures/serverAuth/handlers/api-v1.js create mode 100644 test/integration/fixtures/serverAuth/middleware/custom-auth.js create mode 100644 test/integration/fixtures/serverAuth/swagger/api-v1.yaml create mode 100644 test/integration/fixtures/serverAuth/swagger/public/definitions/Contact.yaml create mode 100644 test/integration/fixtures/serverAuth/swagger/public/definitions/CuriousPerson.yaml create mode 100644 test/integration/fixtures/serverAuth/swagger/public/definitions/EnthusiasticPerson.yaml create mode 100644 test/integration/fixtures/serverAuth/swagger/public/definitions/OtherPerson.yaml create mode 100644 test/integration/fixtures/serverAuth/swagger/public/definitions/Person.yaml create mode 100644 test/integration/fixtures/serverAuth/swagger/public/definitions/SuperFunTime.yaml create mode 100644 test/integration/fixtures/serverAuth/swagger/public/parameters/PathId.yaml create mode 100644 test/integration/fixtures/serverAuth/swagger/public/paths.yaml create mode 100644 test/integration/fixtures/serverAuth/swagger/public/paths/superfuntime-{id}.yaml create mode 100644 test/integration/fixtures/serverAuth/swagger/public/security-definitions.yaml create mode 100644 test/integration/fixtures/serverAuth/swagger/public/x-bos-security-definitions.yaml create mode 100644 test/integration/testAuth.js diff --git a/examples/swagger/middleware/custom-auth.js b/examples/swagger/middleware/custom-auth.js index e8bc32f..e4152db 100644 --- a/examples/swagger/middleware/custom-auth.js +++ b/examples/swagger/middleware/custom-auth.js @@ -1,42 +1,43 @@ +var _ = require('lodash'); exports.init = function(app, logger) { app.use(function (req, res, next) { if (!req.bosAuthenticationData) { return next(); } - switch (req.bosAuthenticationData.type) { + _.forEach(req.bosAuthenticationData, function (authData) { + switch (authData.type) { - case 'Basic': - if (!(req.bosAuthenticationData.username && req.bosAuthenticationData.password)) { - res.sendStatus(401); - } else { - return next(); - } - break; - case 'apiKey': - if (!(req.bosAuthenticationData.password)) { - res.sendStatus(401); - } else { - return next(); - } - break; - case 'oauth2': - if (req.bosAuthenticationData.securityDefn.flow === 'implicit') { - if (!(req.bosAuthenticationData.password)) { - res.sendStatus(401); - } - else { - return next(); + case 'basic': + if (!(authData.username && authData.password)) { + res.setHeader('WWW-Authenticate', 'Basic realm="' + authData.securityReq + '"'); + res.status(401).send(); + return false; } - } else { - if (!(req.session.bosAuthenticationData)) { - res.sendStatus(401); + break; + case 'apiKey': + if (!authData.password) { + res.status(401).send(); + return false; } - else { - return next(); + break; + case 'oauth2': + if (authData.securityDefn.flow === 'implicit') { + if (!(authData.password)) { + res.sendStatus(401); + return false; + } + } else { + if (!(authData.tokenData)) { + res.sendStatus(401); + return false; + } } + break; } - break; + }); + if (!res.headersSent) { + next(); } }); }; diff --git a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml index 43d2f23..75bc511 100644 --- a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml +++ b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml @@ -9,8 +9,9 @@ get: #- oauthImplicitEx: [] #security: #- oauthEx: [] - #security: - #- basicEx: [] + security: + - basicEx: [] + - apiKeyEx: [] #security: #- apiKeyEx: [] #x-bos-security: diff --git a/examples/swagger/swagger/public/security-definitions.yaml b/examples/swagger/swagger/public/security-definitions.yaml index 85a62e4..e1ada60 100644 --- a/examples/swagger/swagger/public/security-definitions.yaml +++ b/examples/swagger/swagger/public/security-definitions.yaml @@ -3,7 +3,7 @@ basicEx: apiKeyEx: type: apiKey in: header - name: authorization + name: x-key oauthEx: type: oauth2 flow: accessCode diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index 099aa31..19c298e 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -163,25 +163,36 @@ function _applySecurityRequirement(app, method, route, securityReq, function basicAuthentication(securityReq) { return function (req, res, next) { - //header should be of the form "Basic " + user:password as a base64 encoded string - req.bosAuthenticationData = {type: 'Basic', securityReq: securityReq}; + if (!req.bosAuthenticationData) { + req.bosAuthenticationData = []; + } + var authenticationData = {type: 'basic', securityReq: securityReq}; + req.bosAuthenticationData.push(authenticationData); var authHeader = req.get('authorization') ? req.get('authorization') : ''; - if (authHeader !== '') { + if (authHeader !== '') { //header should be of the form "Basic " + user:password as a base64 encoded string var credentialsBase64 = authHeader.split('Basic ')[1]; //look into decodeuricomponent var credentialsDecoded = base64URL.decode(credentialsBase64); var credentials = credentialsDecoded.split(':'); - req.bosAuthenticationData.username = credentials[0]; - req.bosAuthenticationData.password = credentials[1]; + authenticationData.username = credentials[0]; + authenticationData.password = credentials[1]; } + /*if (!(req.bosAuthenticationData.username && req.bosAuthenticationData.password)) { + res.setHeader('WWW-Authenticate', 'Basic realm="' + securityReq + '"'); + return res.status(401).send(); + }*/ next(); }; } function apiKeyAuthentication(securityReq, securityDefn) { return function (req, res, next) { - req.bosAuthenticationData = {type: 'apiKey', securityReq: securityReq, securityDefn: securityDefn}; - if (req.get('authorization')) { + if (!req.bosAuthenticationData) { + req.bosAuthenticationData = []; + } + var authenticationData = {type: securityDefn.type, securityReq: securityReq, securityDefn: securityDefn}; + req.bosAuthenticationData.push(authenticationData); + /*if (req.get('authorization')) { var digestHeader = req.get('authorization').split('Digest ')[1]; if (digestHeader) { //should be form of username="Mufasa", realm="myhost@example.com" @@ -190,20 +201,25 @@ function apiKeyAuthentication(securityReq, securityDefn) { authorizationHeaderFields.forEach(function (header) { //should be form of username="Mufasa" var keyValPair = header.split('='); - req.bosAuthenticationData[keyValPair[0]] = keyValPair[1].substring(1, keyValPair[1].length - 1); + authenticationData[keyValPair[0]] = keyValPair[1].substring(1, keyValPair[1].length - 1); }); + return next(); } - } + }*/ if (securityDefn.in === 'query') { - req.bosAuthenticationData.password = req.query[securityDefn.name]; + authenticationData.password = req.query[securityDefn.name]; } else if (securityDefn.in === 'header') { - req.bosAuthenticationData.password = req.get(securityDefn.name); + authenticationData.password = req.get(securityDefn.name); } else { log.warn('unknown location %s for apiKey. ' + 'looks like open api specs may have changed on us', securityDefn.in); } + /*if (!(req.bosAuthenticationData.password)) { + res.setHeader('WWW-Authenticate', 'Digest realm="' + securityReq + '"'); + return res.status(401).send(); + }*/ next(); //this would have to be a user provided function that //fetches the user (and thus the private key that we need to compute the hash) from some data source @@ -223,19 +239,31 @@ function apiKeyAuthentication(securityReq, securityDefn) { function oauth2(securityReq, securityDefn, scopes) { return function (req, res, next) { + if (!req.bosAuthenticationData) { + req.bosAuthenticationData = []; + } if (securityDefn.flow === 'accessCode') { if (!req.session) { - log.error('oauth requires that session be enabled'); - return next(); + log.error('oauth2 accessCode flow requires that session be enabled'); + return res.sendStatus(401); } - if (req.session.bosAuthenticationData) { //already authenticated + else if (req.session.bosAuthenticationData) { //already authenticated + req.bosAuthenticationData.push(req.session.bosAuthenticationData); return next(); + } else { + req.session.bosAuthenticationData = { + type: securityDefn.type, + securityReq: securityReq, + securityDefn: securityDefn + }; + req.bosAuthenticationData.push(req.session.bosAuthenticationData); } } else if (req.get('authorization')) { //implicit - req.bosAuthenticationData = {type: 'oauth2', securityReq: securityReq, securityDefn: securityDefn}; - req.bosAuthenticationData.password = + var authenticationData = {type: securityDefn.type, securityReq: securityReq, securityDefn: securityDefn}; + req.bosAuthenticationData.push(authenticationData); + authenticationData.password = req.get('authorization').split('Bearer ')[1]; //we assume bearer token type which is the most common - if (req.bosAuthenticationData.password) { //already authenticated + if (authenticationData.password) { //already authenticated //user defined code will be responsible for validating this token //which they absolutely should do because it did not come directly from oauth provider return next(); diff --git a/services/oauth2.js b/services/oauth2.js index 4dfdf01..f1031e8 100644 --- a/services/oauth2.js +++ b/services/oauth2.js @@ -51,8 +51,6 @@ function accessCodeRedirect(req, res) { //log warning about possible xsrf attack } else { oauth.getTokenData(req, res, function (tokenData) { - req.session.bosAuthenticationData = {type: 'oauth2', securityReq: securityReq, - securityDefn: {authorizationUrl: oauth.authorizationUrl, tokenUrl: oauth.tokenUrl, flow: 'accessCode'}}; req.session.bosAuthenticationData.tokenData = tokenData; //req.sessionOptions.httpOnly = false; //needs to be set for CORS //this should make it so that an auth code will only get used once diff --git a/test/integration/fixtures/serverAuth/config/default.json b/test/integration/fixtures/serverAuth/config/default.json new file mode 100644 index 0000000..26b6d90 --- /dev/null +++ b/test/integration/fixtures/serverAuth/config/default.json @@ -0,0 +1,31 @@ +//Default config for my app +{ + "express": { + "port": "5000", + "middleware": ["cors", "body-parser", "session", "bos-authentication", "custom-auth"], + "middleware$": [] + }, + "cors": { + "origin": "*" + }, + + "body-parser": { + "json": {} + }, + + "cluster": { + "maxWorkers": 1 + }, + + "session": { + "keys": ["sessionKey"] + }, + + "oauth": { + "clientId": "826839015121-rs7t7ick1ib0uvui9iihbb82gcqg64r9.apps.googleusercontent.com", + "clientSecret": "secret", + "redirectURI": "/oauth-redirect" + } + + +} \ No newline at end of file diff --git a/test/integration/fixtures/serverAuth/handlers/api-v1.js b/test/integration/fixtures/serverAuth/handlers/api-v1.js new file mode 100644 index 0000000..e253f80 --- /dev/null +++ b/test/integration/fixtures/serverAuth/handlers/api-v1.js @@ -0,0 +1,26 @@ + +exports.init = function() { + +}; + +exports.getFunTimeById = function(req, res, next) { + res.status(200).json({ + 'curiousPeople': [ + { + 'kind': 'OtherPerson', + 'curiousPersonReqField': 'hey?', + 'enthusiasticPersonReqField': 'hola!' + } + ] + }); +}; + +exports.addFunTime = function(req, res, next) { + res.status(204).send(); +}; + +exports.deleteFunTimeById = function(req, res, next) { + res.status(204).send(); +}; + + diff --git a/test/integration/fixtures/serverAuth/middleware/custom-auth.js b/test/integration/fixtures/serverAuth/middleware/custom-auth.js new file mode 100644 index 0000000..e4152db --- /dev/null +++ b/test/integration/fixtures/serverAuth/middleware/custom-auth.js @@ -0,0 +1,43 @@ +var _ = require('lodash'); + +exports.init = function(app, logger) { + app.use(function (req, res, next) { + if (!req.bosAuthenticationData) { + return next(); + } + _.forEach(req.bosAuthenticationData, function (authData) { + switch (authData.type) { + + case 'basic': + if (!(authData.username && authData.password)) { + res.setHeader('WWW-Authenticate', 'Basic realm="' + authData.securityReq + '"'); + res.status(401).send(); + return false; + } + break; + case 'apiKey': + if (!authData.password) { + res.status(401).send(); + return false; + } + break; + case 'oauth2': + if (authData.securityDefn.flow === 'implicit') { + if (!(authData.password)) { + res.sendStatus(401); + return false; + } + } else { + if (!(authData.tokenData)) { + res.sendStatus(401); + return false; + } + } + break; + } + }); + if (!res.headersSent) { + next(); + } + }); +}; diff --git a/test/integration/fixtures/serverAuth/swagger/api-v1.yaml b/test/integration/fixtures/serverAuth/swagger/api-v1.yaml new file mode 100644 index 0000000..de1008c --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/api-v1.yaml @@ -0,0 +1,28 @@ +swagger: '2.0' +info: + version: 2.0.0-rc.1 + title: Test API + +basePath: /api/v1 +produces: + - application/json +paths: + $ref: 'public/paths.yaml' +securityDefinitions: + $ref: 'public/security-definitions.yaml' +x-bos-securityDefinitions: + $ref: 'public/x-bos-security-definitions.yaml' +definitions: + Contact: + $ref: 'public\definitions\Contact.yaml' + CuriousPerson: + $ref: 'public\definitions\CuriousPerson.yaml' + EnthusiasticPerson: + $ref: 'public\definitions\EnthusiasticPerson.yaml' + OtherPerson: + $ref: 'public\definitions\OtherPerson.yaml' + Person: + $ref: 'public\definitions\Person.yaml' + SuperFunTime: + $ref: 'public\definitions\SuperFunTime.yaml' + diff --git a/test/integration/fixtures/serverAuth/swagger/public/definitions/Contact.yaml b/test/integration/fixtures/serverAuth/swagger/public/definitions/Contact.yaml new file mode 100644 index 0000000..6c64e61 --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/public/definitions/Contact.yaml @@ -0,0 +1,13 @@ +properties: + firstName: + type: string + lastName: + type: string + email: + type: string + format: email + gender: + type: string + enum: + - Female + - Male \ No newline at end of file diff --git a/test/integration/fixtures/serverAuth/swagger/public/definitions/CuriousPerson.yaml b/test/integration/fixtures/serverAuth/swagger/public/definitions/CuriousPerson.yaml new file mode 100644 index 0000000..7c30f36 --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/public/definitions/CuriousPerson.yaml @@ -0,0 +1,13 @@ +allOf: + - $ref: 'EnthusiasticPerson.yaml' + - type: object + required: + - kind + - curiousPersonReqField + properties: + kind: + type: string + enum: + - CuriousPerson + curiousPersonReqField: + type: string diff --git a/test/integration/fixtures/serverAuth/swagger/public/definitions/EnthusiasticPerson.yaml b/test/integration/fixtures/serverAuth/swagger/public/definitions/EnthusiasticPerson.yaml new file mode 100644 index 0000000..01351c5 --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/public/definitions/EnthusiasticPerson.yaml @@ -0,0 +1,14 @@ +allOf: + - $ref: 'Person.yaml' + - type: object + required: + - kind + - enthusiasticPersonReqField + properties: + kind: + type: string + enum: + - EnthusiasticPerson + enthusiasticPersonReqField: + type: string + diff --git a/test/integration/fixtures/serverAuth/swagger/public/definitions/OtherPerson.yaml b/test/integration/fixtures/serverAuth/swagger/public/definitions/OtherPerson.yaml new file mode 100644 index 0000000..e7d44d6 --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/public/definitions/OtherPerson.yaml @@ -0,0 +1,14 @@ +allOf: + - $ref: 'CuriousPerson.yaml' + - type: object + required: + - kind + - notes + properties: + kind: + type: string + enum: + - OtherPerson + notes: + type: string + diff --git a/test/integration/fixtures/serverAuth/swagger/public/definitions/Person.yaml b/test/integration/fixtures/serverAuth/swagger/public/definitions/Person.yaml new file mode 100644 index 0000000..d6add6a --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/public/definitions/Person.yaml @@ -0,0 +1,10 @@ +allOf: + - $ref: 'Contact.yaml' + - type: object + discriminator: kind + required: + - kind + properties: + kind: + type: string + diff --git a/test/integration/fixtures/serverAuth/swagger/public/definitions/SuperFunTime.yaml b/test/integration/fixtures/serverAuth/swagger/public/definitions/SuperFunTime.yaml new file mode 100644 index 0000000..6f44781 --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/public/definitions/SuperFunTime.yaml @@ -0,0 +1,8 @@ +type: object +properties: + curiousPeople: + type: array + items: + $ref: 'CuriousPerson.yaml' + + diff --git a/test/integration/fixtures/serverAuth/swagger/public/parameters/PathId.yaml b/test/integration/fixtures/serverAuth/swagger/public/parameters/PathId.yaml new file mode 100644 index 0000000..73c5b26 --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/public/parameters/PathId.yaml @@ -0,0 +1,6 @@ +name: id +in: path +required: true +type: string +description: | + The ID of the resource to which the call applies. diff --git a/test/integration/fixtures/serverAuth/swagger/public/paths.yaml b/test/integration/fixtures/serverAuth/swagger/public/paths.yaml new file mode 100644 index 0000000..ff6ef4f --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/public/paths.yaml @@ -0,0 +1,3 @@ +/superfuntime/{id}: + $ref: 'paths/superfuntime-{id}.yaml' + diff --git a/test/integration/fixtures/serverAuth/swagger/public/paths/superfuntime-{id}.yaml b/test/integration/fixtures/serverAuth/swagger/public/paths/superfuntime-{id}.yaml new file mode 100644 index 0000000..8f20aec --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/public/paths/superfuntime-{id}.yaml @@ -0,0 +1,63 @@ +get: + summary: Get super fun time + operationId: getFunTimeById + tags: + - Fun Times + parameters: + - $ref: '../parameters/PathId.yaml' + #security: + #- oauthImplicitEx: [] + #security: + #- oauthEx: [] + security: + - basicEx: [] + #security: + #- apiKeyEx: [] + #x-bos-security: + #- hmac: [] + responses: + 200: + schema: + $ref: '../definitions/SuperFunTime.yaml' + description: I want a super fun time +post: + summary: Add fun time + operationId: addFunTime + tags: + - Fun Times + parameters: + - $ref: '../parameters/PathId.yaml' + #security: + #- oauthImplicitEx: [] + #security: + #- oauthEx: [] + #security: + #- basicEx: [] + security: + - apiKeyEx: [] + #x-bos-security: + #- hmac: [] + responses: + default: + description: you did it! +delete: + summary: delete super fun time :( + operationId: deleteFunTimeById + tags: + - Fun Times + parameters: + - $ref: '../parameters/PathId.yaml' + #security: + #- oauthImplicitEx: [] + #security: + #- oauthEx: [] + security: + - basicEx: [] + - apiKeyEx: [] + #security: + #- apiKeyEx: [] + #x-bos-security: + #- hmac: [] + responses: + default: + description: you did it! diff --git a/test/integration/fixtures/serverAuth/swagger/public/security-definitions.yaml b/test/integration/fixtures/serverAuth/swagger/public/security-definitions.yaml new file mode 100644 index 0000000..e1ada60 --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/public/security-definitions.yaml @@ -0,0 +1,17 @@ +basicEx: + type: basic +apiKeyEx: + type: apiKey + in: header + name: x-key +oauthEx: + type: oauth2 + flow: accessCode + authorizationUrl: http://localhost:3000/auth-code + tokenUrl: http://localhost:3000/access-token + scopes: {} +oauthImplicitEx: + type: oauth2 + flow: implicit + authorizationUrl: http://localhost:3000/access-token + scopes: {} \ No newline at end of file diff --git a/test/integration/fixtures/serverAuth/swagger/public/x-bos-security-definitions.yaml b/test/integration/fixtures/serverAuth/swagger/public/x-bos-security-definitions.yaml new file mode 100644 index 0000000..9f915ac --- /dev/null +++ b/test/integration/fixtures/serverAuth/swagger/public/x-bos-security-definitions.yaml @@ -0,0 +1,11 @@ +hmac: + verify: + service: hmacService + method: + name: getApiUser + execute: false + args: [] + routeOptions: + session: false + module: passport-hmac-strategy + x-bos-middleware: bos-passport diff --git a/test/integration/testAuth.js b/test/integration/testAuth.js new file mode 100644 index 0000000..e2d604c --- /dev/null +++ b/test/integration/testAuth.js @@ -0,0 +1,144 @@ +var request = require('request'), + assert = require('assert'), + util = require('./launchUtil'); + +describe('test basic auth', function () { + this.timeout(5000); + before(function (done) { + util.launch('serverAuth', done); + }); + + after(function (done) { + util.finish(done); + }); + + it('should fail without credentials', function (done) { + request('http://localhost:' + (process.env.PORT || 5000) + '/api/v1/superfuntime/2', + function (err, resp, body) { + assert.equal(resp.statusCode, 401); + assert.ok(resp.headers['www-authenticate'].indexOf('Basic') >= 0); + done(); + }); + }); + + it('should succeed with credentials', function (done) { + request.get('http://localhost:' + (process.env.PORT || 5000) + '/api/v1/superfuntime/2', + { + auth: { + user: 'username', pass: 'password', sendImmediately: false + } + }, + function (err, resp, body) { + assert.equal(resp.statusCode, 200); + done(); + }); + }); +}); + +describe('test apiKey auth', function () { + this.timeout(5000); + before(function (done) { + util.launch('serverAuth', done); + }); + + after(function (done) { + util.finish(done); + }); + + it('should fail without credentials', function (done) { + request.post('http://localhost:' + (process.env.PORT || 5000) + '/api/v1/superfuntime/2', + { + body: { + 'kind': 'OtherPerson', + 'curiousPersonReqField': 'hey?', + 'enthusiasticPersonReqField': 'hola!' + }, + json: true + }, + function (err, resp, body) { + assert.equal(resp.statusCode, 401); + done(); + }); + }); + + it('should succeed with credentials', function (done) { + request.post('http://localhost:' + (process.env.PORT || 5000) + '/api/v1/superfuntime/2', + { + body: { + 'kind': 'OtherPerson', + 'curiousPersonReqField': 'hey?', + 'enthusiasticPersonReqField': 'hola!' + }, + json: true, + headers: { + 'x-key': 'api secret key' + } + }, + function (err, resp, body) { + assert.equal(resp.statusCode, 204); + done(); + }); + }); +}); + +describe('test basic and apiKey auth together on same operation', function () { + this.timeout(5000); + before(function (done) { + util.launch('serverAuth', done); + }); + + after(function (done) { + util.finish(done); + }); + + it('should fail without any credentials', function (done) { + request.delete('http://localhost:' + (process.env.PORT || 5000) + '/api/v1/superfuntime/2', + function (err, resp, body) { + assert.equal(resp.statusCode, 401); + done(); + }); + }); + + it('should fail without basic credentials', function (done) { + request.delete('http://localhost:' + (process.env.PORT || 5000) + '/api/v1/superfuntime/2', + { + headers: { + 'x-key': 'api secret key' + } + }, + function (err, resp, body) { + assert.equal(resp.statusCode, 401); + done(); + }); + }); + + it('should fail without apiKey credentials', function (done) { + request.delete('http://localhost:' + (process.env.PORT || 5000) + '/api/v1/superfuntime/2', + { + auth: { + user: 'username', pass: 'password', sendImmediately: false + } + }, + function (err, resp, body) { + assert.equal(resp.statusCode, 401); + done(); + }); + }); + + it('should succeed with credentials', function (done) { + request.delete('http://localhost:' + (process.env.PORT || 5000) + '/api/v1/superfuntime/2', + { + auth: { + user: 'username', pass: 'password', sendImmediately: false + }, + headers: { + 'x-key': 'api secret key' + } + }, + function (err, resp, body) { + assert.equal(resp.statusCode, 204); + done(); + }); + }); +}); + From 32516a1e66593e4ad345af47ac43e9c3a018def5 Mon Sep 17 00:00:00 2001 From: shepp Date: Wed, 26 Oct 2016 09:35:20 -0400 Subject: [PATCH 16/20] set authenticate header in implicit flow --- services/oauth2.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/services/oauth2.js b/services/oauth2.js index f1031e8..d599a82 100644 --- a/services/oauth2.js +++ b/services/oauth2.js @@ -4,6 +4,7 @@ var cfg; var clientId; var clientSecret; var redirectURI; +var implicitRedirectUri; //map of security req to oauth2 instance var secRecOAuthMap = {}; @@ -29,6 +30,7 @@ function init(config, logger) { clientId = cfg.clientId; clientSecret = cfg.clientSecret; redirectURI = cfg.redirectURI; + implicitRedirectUri = cfg.implicitRedirectUri; if (config.get('express').middleware.indexOf('session') < 0) { logger.warn('oauth requires that session be enabled.'); } @@ -72,15 +74,16 @@ OAuth2.prototype.redirectToAuthorizationUrl = function (req, res, scopes, stateI }; OAuth2.prototype.redirectToAuthorizationUrlImplicit = function (req, res, scopes, stateId) { - /*var queryString = '?response_type=token&client_id='; + var queryString = '?response_type=token&client_id='; queryString += clientId; queryString += '&redirect_uri=' + implicitRedirectUri; queryString += '&scope=' + scopes.join(' '); queryString += '&state=' + stateId; - res.status(302); - res.setHeader('location', this.authorizationUrl + queryString); - res.send();*/ - res.sendStatus(401); + //normally we would send back a redirect here, but since we anticipate this being used on a mobile platform + //we will send a 401 with the oauth provider url in the authenticate header, client will handle sending the request + res.status(401); + res.setHeader('www-authenticate', 'OAuth2 ' + this.authorizationUrl + queryString); + res.send(); }; OAuth2.prototype.startOAuth = function (securityReq, scopes, req, res) { From 07783d1c4cd5059915a50aa8139590cc09e816f7 Mon Sep 17 00:00:00 2001 From: shepp Date: Wed, 2 Nov 2016 15:42:12 -0400 Subject: [PATCH 17/20] pulled out bos-passport into it's own module --- examples/swagger/middleware/bos-passport.js | 66 ------------------- .../public/paths/superfuntime-{id}.yaml | 6 +- index.js | 2 + middleware/bos-authentication.js | 53 ++++++++------- package.json | 2 - 5 files changed, 35 insertions(+), 94 deletions(-) delete mode 100644 examples/swagger/middleware/bos-passport.js diff --git a/examples/swagger/middleware/bos-passport.js b/examples/swagger/middleware/bos-passport.js deleted file mode 100644 index fcf7cdc..0000000 --- a/examples/swagger/middleware/bos-passport.js +++ /dev/null @@ -1,66 +0,0 @@ -var _ = require('lodash'), - passport = require('passport'), - subRequire = require('../../../lib/subRequire'); -var strategyMap= {}; -var cfg; -var loader; - -module.exports = { - init : init, - authenticate: authenticate -}; - -function init(app, config, serviceLoader) { - cfg = config.get('passport'); - loader = serviceLoader; - app.use(passport.initialize()); - //if sessions are enabled and express session is also being used, - //express session middleware MUST be listed first in the middleware config - if (cfg.session) { - app.use(passport.session()); - } -} - -function authenticate(strategyId, strategy) { - //load passport strategy module - if (!strategyMap[strategy.module]) { - strategyMap[strategy.module] = subRequire(strategy.module, 'bos-passport').Strategy; - } - _.forEach(_.keys(strategy.options), function (opt) { - strategy.options[opt] = prepareOption(strategy.options[opt]); - }); - _.forEach(_.keys(strategy.routeOptions), function (opt) { - strategy.routeOptions[opt] = prepareOption(strategy.routeOptions[opt]); - }); - strategy.verify = prepareOption(strategy.verify); - passport.use(strategyId, new strategyMap[strategy.module](strategy.options, strategy.verify)); - return passport.authenticate(strategyId, strategy.routeOptions); -} - -//fetch the option from the config, or if option is a service method, point to or call the method with supplied args -//maybe make the method args able to be fetched from the config? -//else just return the option -function prepareOption(opt) { - if (typeof opt === 'string') { - var configRegex = /^{{(.*)}}$/; - var result = configRegex.exec(opt); - if (result && result[1]) { - // This is a config-based option - return _.get(cfg, result[1]); - } - } else if (typeof opt === 'object') { - if (opt.service) { - var service = loader.get(opt.service); - var method = service[opt.method.name]; - var args = opt.method.args ? opt.method.args : []; - if (opt.method.execute) { - return method.apply(service, args); - } else { - return _.partial.apply(_, [method].concat(args)); - } - } - } - return opt; -} - -//need serializeUser and deserializeUser functions for session enabling diff --git a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml index 75bc511..ffc0f9f 100644 --- a/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml +++ b/examples/swagger/swagger/public/paths/superfuntime-{id}.yaml @@ -9,9 +9,9 @@ get: #- oauthImplicitEx: [] #security: #- oauthEx: [] - security: - - basicEx: [] - - apiKeyEx: [] + #security: + #- basicEx: [] + #- apiKeyEx: [] #security: #- apiKeyEx: [] #x-bos-security: diff --git a/index.js b/index.js index 7d31303..006a67d 100644 --- a/index.js +++ b/index.js @@ -363,3 +363,5 @@ module.exports.testUtility = function () { return require('./testlib/util'); }; +module.exports.subRequire = require('./lib/subRequire'); + diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index 19c298e..29da957 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -1,19 +1,18 @@ var _ = require('lodash'); var base64URL = require('base64url'); -var path = require('path'); var log; var loader; var oAuthService; var httpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']; -//map of security reqs to express middleware callbacks -var securityReqMap = {}; +//map of middleware ids to 'true' meaning that they have been initialized +var middlewareInitMap = {}; module.exports = { init : init }; -function init(app, config, logger, serviceLoader, swagger) { +function init(app, logger, serviceLoader, swagger) { log = logger; loader = serviceLoader; _.forEach(swagger.getSimpleSpecs(), function (api, name) { @@ -57,27 +56,27 @@ function _applyCustomSecurityRequirement(app, method, route, securityReq, securityDefn, /*requiredPermissions,*/ requiredScopes) { //load security def middleware if (securityDefn['x-bos-middleware']) { - loader.loadConsumerModules('middleware', - [path.join(global.__appDir, 'middleware', securityDefn['x-bos-middleware'])]); - loader.initConsumers('middleware', [securityDefn['x-bos-middleware']], function (err) { - if (!err) { - var customAuthMiddleware = loader.getConsumer('middleware', securityDefn['x-bos-middleware']); - if (customAuthMiddleware.authenticate) { - if (!securityReqMap[securityReq]) { - securityReqMap[securityReq] = - customAuthMiddleware.authenticate(securityReq, securityDefn, requiredScopes); - } - app[method].call(app, route, securityReqMap[securityReq]); + var customAuthMiddleware = loader.getConsumer('middleware', securityDefn['x-bos-middleware']); + if (!customAuthMiddleware) { + loader.loadConsumerModules('middleware', + [securityDefn['x-bos-middleware']]); + customAuthMiddleware = loader.getConsumer('middleware', securityDefn['x-bos-middleware']); + } + if (!middlewareInitMap[securityDefn['x-bos-middleware']]) { + loader.initConsumers('middleware', [securityDefn['x-bos-middleware']], function (err) { + if (!err) { + wireAuthenticateToRoute(app, method, route, securityReq, + securityDefn, requiredScopes, customAuthMiddleware); } else { - log.warn('custom auth middleware %s missing authenticate method'); + log.warn('Unable to initialize custom middleware %s for security defn %s', + securityDefn['x-bos-middleware'], securityReq); } - } - else { - log.warn('Unable to find custom middleware %s for security defn %s', - securityDefn['x-bos-middleware'], securityReq); - } - }); + }); + } else { + wireAuthenticateToRoute(app, method, route, securityReq, + securityDefn, requiredScopes, customAuthMiddleware); + } } else { log.info('No custom middleware defined for security defn %s. ' + 'Attempting to use built in middleware...', securityReq); @@ -86,6 +85,15 @@ function _applyCustomSecurityRequirement(app, method, route, securityReq, } } +function wireAuthenticateToRoute(app, method, route, securityReq, securityDefn, requiredScopes, customAuthMiddleware) { + if (customAuthMiddleware.authenticate) { + app[method].call(app, route, customAuthMiddleware.authenticate(securityReq, securityDefn, requiredScopes)); + } + else { + log.warn('custom auth middleware %s missing authenticate method'); + } +} + function _applySecurityRequirement(app, method, route, securityReq, securityDefn, /*requiredPermissions,*/ requiredScopes) { //allow use of custom middleware even if a custom security definition was not used @@ -171,7 +179,6 @@ function basicAuthentication(securityReq) { var authHeader = req.get('authorization') ? req.get('authorization') : ''; if (authHeader !== '') { //header should be of the form "Basic " + user:password as a base64 encoded string var credentialsBase64 = authHeader.split('Basic ')[1]; - //look into decodeuricomponent var credentialsDecoded = base64URL.decode(credentialsBase64); var credentials = credentialsDecoded.split(':'); authenticationData.username = credentials[0]; diff --git a/package.json b/package.json index 97c3757..7760311 100644 --- a/package.json +++ b/package.json @@ -53,8 +53,6 @@ "node-cache": "^3.0.0", "node-statsd": "^0.1.1", "on-headers": "^1.0.0", - "passport": "^0.3.2", - "passport-hmac-strategy": "^1.2.0", "prompt": "^0.2.14", "raw-body": "^2.0.2", "redis": "^2.4.2", From 042a47b0e67e9d0a8811bcab79efaf88ff526e12 Mon Sep 17 00:00:00 2001 From: shepp Date: Mon, 7 Nov 2016 10:07:23 -0500 Subject: [PATCH 18/20] ref compiler enabled in example to avoid conflicts with master --- examples/swagger/config/default.json | 17 +++++++++++++++++ examples/swagger/swagger/api-v1.yaml | 18 +++++++++++------- services/oauth2.js | 4 ++-- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/examples/swagger/config/default.json b/examples/swagger/config/default.json index 56b8b0b..88c1e58 100644 --- a/examples/swagger/config/default.json +++ b/examples/swagger/config/default.json @@ -26,6 +26,23 @@ "clientId": "826839015121-rs7t7ick1ib0uvui9iihbb82gcqg64r9.apps.googleusercontent.com", "clientSecret": "secret", "redirectURI": "/oauth-redirect" + }, + + "swagger": { + "refCompiler": { + "petstore": { + "baseSpecFile": "petstore.json", + "refDirs": [ + "v1-assets" + ] + }, + "api-v1": { + "baseSpecFile": "api-v1.yaml", + "refDirs": [ + "public" + ] + } + } } diff --git a/examples/swagger/swagger/api-v1.yaml b/examples/swagger/swagger/api-v1.yaml index de1008c..6468a4b 100644 --- a/examples/swagger/swagger/api-v1.yaml +++ b/examples/swagger/swagger/api-v1.yaml @@ -12,17 +12,21 @@ securityDefinitions: $ref: 'public/security-definitions.yaml' x-bos-securityDefinitions: $ref: 'public/x-bos-security-definitions.yaml' + +### ref-compiler: BEGIN definitions: Contact: - $ref: 'public\definitions\Contact.yaml' + $ref: 'public/definitions/Contact.yaml' CuriousPerson: - $ref: 'public\definitions\CuriousPerson.yaml' + $ref: 'public/definitions/CuriousPerson.yaml' EnthusiasticPerson: - $ref: 'public\definitions\EnthusiasticPerson.yaml' + $ref: 'public/definitions/EnthusiasticPerson.yaml' OtherPerson: - $ref: 'public\definitions\OtherPerson.yaml' + $ref: 'public/definitions/OtherPerson.yaml' Person: - $ref: 'public\definitions\Person.yaml' + $ref: 'public/definitions/Person.yaml' SuperFunTime: - $ref: 'public\definitions\SuperFunTime.yaml' - + $ref: 'public/definitions/SuperFunTime.yaml' +parameters: + PathId: + $ref: 'public/parameters/PathId.yaml' diff --git a/services/oauth2.js b/services/oauth2.js index d599a82..510bd8f 100644 --- a/services/oauth2.js +++ b/services/oauth2.js @@ -31,9 +31,9 @@ function init(config, logger) { clientSecret = cfg.clientSecret; redirectURI = cfg.redirectURI; implicitRedirectUri = cfg.implicitRedirectUri; - if (config.get('express').middleware.indexOf('session') < 0) { + /*if (config.get('express').middleware.indexOf('session') < 0) { logger.warn('oauth requires that session be enabled.'); - } + }*/ } } From 7da021fb106da3cd02c39154a52f57569bd83656 Mon Sep 17 00:00:00 2001 From: shepp Date: Mon, 7 Nov 2016 12:33:55 -0500 Subject: [PATCH 19/20] remove oauth for now --- examples/swagger/config/default.json | 6 - examples/swagger/handlers/_oauth-redirect.js | 14 -- .../oauth-provider-get-access-token.js | 23 ---- .../handlers/oauth-provider-get-auth-code.js | 9 -- middleware/bos-authentication.js | 13 +- package.json | 3 +- services/oauth2.js | 130 ------------------ 7 files changed, 7 insertions(+), 191 deletions(-) delete mode 100644 examples/swagger/handlers/_oauth-redirect.js delete mode 100644 examples/swagger/handlers/oauth-provider-get-access-token.js delete mode 100644 examples/swagger/handlers/oauth-provider-get-auth-code.js delete mode 100644 services/oauth2.js diff --git a/examples/swagger/config/default.json b/examples/swagger/config/default.json index 88c1e58..03ffa5b 100644 --- a/examples/swagger/config/default.json +++ b/examples/swagger/config/default.json @@ -22,12 +22,6 @@ "keys": ["sessionKey"] }, - "oauth": { - "clientId": "826839015121-rs7t7ick1ib0uvui9iihbb82gcqg64r9.apps.googleusercontent.com", - "clientSecret": "secret", - "redirectURI": "/oauth-redirect" - }, - "swagger": { "refCompiler": { "petstore": { diff --git a/examples/swagger/handlers/_oauth-redirect.js b/examples/swagger/handlers/_oauth-redirect.js deleted file mode 100644 index e28dd79..0000000 --- a/examples/swagger/handlers/_oauth-redirect.js +++ /dev/null @@ -1,14 +0,0 @@ -var oauthService; - -module.exports = { - init : init -}; - -function init(app, oauth2) { - oauthService = oauth2; - app.get('/oauth-redirect', handleRedirect); -} - -function handleRedirect(req, res) { - oauthService.accessCodeRedirect(req, res); -} diff --git a/examples/swagger/handlers/oauth-provider-get-access-token.js b/examples/swagger/handlers/oauth-provider-get-access-token.js deleted file mode 100644 index e7d0606..0000000 --- a/examples/swagger/handlers/oauth-provider-get-access-token.js +++ /dev/null @@ -1,23 +0,0 @@ - -var sendAccessToken = function (req, res) { - res.json({accessToken: 'access'}); -}; - -var redirectWithAccessToken = function (req, res) { - /* - A real oauth provider would use this in the implicit flow to validate user credentials, - authorize the app to access whatever scopes were requested. Then it would redirect to - the redirect_uri (present in this request), adding the access token as a uri fragment. - User defined code must handle this redirect with some client side javascript to extract the - token from the uri fragment. - */ - res.statusCode = 302; - res.setHeader('location', req.query.redirect_uri + '#state=' + req.query.state + - '&token_type=bearer&access_token=access_token'); - res.send(); -}; - -exports.init = function (app) { - app.post('/access-token', sendAccessToken); - app.get('/access-token', redirectWithAccessToken); -}; diff --git a/examples/swagger/handlers/oauth-provider-get-auth-code.js b/examples/swagger/handlers/oauth-provider-get-auth-code.js deleted file mode 100644 index 2dfc606..0000000 --- a/examples/swagger/handlers/oauth-provider-get-auth-code.js +++ /dev/null @@ -1,9 +0,0 @@ -var sendAuthCode = function (req, res, next) { - res.statusCode = 302; - res.setHeader('location', req.query.redirect_uri + '?state=' + req.query.state + '&code=authcode'); - res.send(); -}; - -exports.init = function (app) { - app.get('/auth-code', sendAuthCode); -}; diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index 29da957..e8a62c8 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -3,7 +3,6 @@ var base64URL = require('base64url'); var log; var loader; -var oAuthService; var httpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']; //map of middleware ids to 'true' meaning that they have been initialized var middlewareInitMap = {}; @@ -109,13 +108,13 @@ function _applySecurityRequirement(app, method, route, securityReq, app[method].call(app, route, apiKeyAuthentication(securityReq, securityDefn)); break; case 'oauth2': - if (!oAuthService) { + /*if (!oAuthService) { oAuthService = loader.get('oauth2'); } - app[method].call(app, route, oauth2(securityReq, securityDefn, requiredScopes)); - /*log.warn('No out of the box oauth2 implementation exists in BOS. ' + + app[method].call(app, route, oauth2(securityReq, securityDefn, requiredScopes));*/ + log.warn('No out of the box oauth2 implementation exists in BOS. ' + 'You must define your own and reference it in the ' + - '"x-bos-middleware" property of the security definition %s', securityReq);*/ + '"x-bos-middleware" property of the security definition %s', securityReq); break; default: return log.warn('unrecognized security type %s for security definition %s' + @@ -244,7 +243,7 @@ function apiKeyAuthentication(securityReq, securityDefn) { }; } -function oauth2(securityReq, securityDefn, scopes) { +/*function oauth2(securityReq, securityDefn, scopes) { return function (req, res, next) { if (!req.bosAuthenticationData) { req.bosAuthenticationData = []; @@ -284,7 +283,7 @@ function oauth2(securityReq, securityDefn, scopes) { } oAuthInstance.startOAuth(securityReq, scopes, req, res); }; -} +}*/ /*function _expandRouteInstancePermissions(perms, route, uri) { relate the route path parameters to the url instance values diff --git a/package.json b/package.json index 2e894b5..07b4629 100644 --- a/package.json +++ b/package.json @@ -72,8 +72,7 @@ "eslint": "^3.3.1", "istanbul": "^0.4.1", "mocha": "^3.0.2", - "mocha-jenkins-reporter": "^0.1.8", - "sjcl": "^1.0.6" + "mocha-jenkins-reporter": "^0.1.8" }, "files": [ "bin", diff --git a/services/oauth2.js b/services/oauth2.js deleted file mode 100644 index 510bd8f..0000000 --- a/services/oauth2.js +++ /dev/null @@ -1,130 +0,0 @@ -var request = require('request'); - -var cfg; -var clientId; -var clientSecret; -var redirectURI; -var implicitRedirectUri; -//map of security req to oauth2 instance -var secRecOAuthMap = {}; - -module.exports = { - init : init, - OAuth2: OAuth2, - accessCodeRedirect: accessCodeRedirect, - addOAuthInstance: addOAuthInstance, - getOAuthInstance: getOAuthInstance -}; - -function addOAuthInstance (securityReq, oauthInstance) { - secRecOAuthMap[securityReq] = oauthInstance; -} - -function getOAuthInstance (securityReq) { - return secRecOAuthMap[securityReq]; -} - -function init(config, logger) { - cfg = config.get('oauth'); - if (cfg) { - clientId = cfg.clientId; - clientSecret = cfg.clientSecret; - redirectURI = cfg.redirectURI; - implicitRedirectUri = cfg.implicitRedirectUri; - /*if (config.get('express').middleware.indexOf('session') < 0) { - logger.warn('oauth requires that session be enabled.'); - }*/ - } -} - -function OAuth2(authorizationUrl, flow, tokenUrl) { - this.authorizationUrl = authorizationUrl; - this.flow = flow; - this.tokenUrl = tokenUrl; - this.stateIds = {}; -} - -function accessCodeRedirect(req, res) { - var securityReq = req.query.state.split('-')[0]; - var oauth = getOAuthInstance(securityReq); - if (!req.query.code) { - //should have auth code at this point - } else if (!oauth.isValidState(req.query.state)) {//check for XSRF - //log warning about possible xsrf attack - } else { - oauth.getTokenData(req, res, function (tokenData) { - req.session.bosAuthenticationData.tokenData = tokenData; - //req.sessionOptions.httpOnly = false; //needs to be set for CORS - //this should make it so that an auth code will only get used once - oauth.deleteRequestState(req.query.state); - res.sendStatus(200); - }); - } -} - -OAuth2.prototype.redirectToAuthorizationUrl = function (req, res, scopes, stateId) { - var queryString = '?response_type=code&client_id='; - queryString += clientId; - queryString += '&redirect_uri=' + redirectURI; - queryString += '&scope=' + scopes.join(' '); - queryString += '&state=' + stateId; - res.status(302); - res.setHeader('location', this.authorizationUrl + queryString); - res.send(); -}; - -OAuth2.prototype.redirectToAuthorizationUrlImplicit = function (req, res, scopes, stateId) { - var queryString = '?response_type=token&client_id='; - queryString += clientId; - queryString += '&redirect_uri=' + implicitRedirectUri; - queryString += '&scope=' + scopes.join(' '); - queryString += '&state=' + stateId; - //normally we would send back a redirect here, but since we anticipate this being used on a mobile platform - //we will send a 401 with the oauth provider url in the authenticate header, client will handle sending the request - res.status(401); - res.setHeader('www-authenticate', 'OAuth2 ' + this.authorizationUrl + queryString); - res.send(); -}; - -OAuth2.prototype.startOAuth = function (securityReq, scopes, req, res) { - var stateId = securityReq + '-' + Math.random(); - if (this.flow === 'accessCode') { - this.addRequestState(stateId, req, res); - this.redirectToAuthorizationUrl(req, res, scopes, stateId); - } else if (this.flow === 'implicit') { //user defined redirect should validate the state parameter - this.redirectToAuthorizationUrlImplicit(req, res, scopes, stateId); - } else { - //unsupported flow - } -}; - -OAuth2.prototype.addRequestState = function (stateId) { - this.stateIds[stateId] = true; -}; - -OAuth2.prototype.isValidState = function (stateId) { - return this.stateIds[stateId]; -}; - -OAuth2.prototype.deleteRequestState = function (stateId) { - this.stateIds[stateId] = undefined; -}; - -OAuth2.prototype.getTokenData = function (req, res, callback) { - var form = {grant_type: 'authorization_code'}; - form.code = req.query.code; - form.clientId = clientId; - form.clientSecret = clientSecret; - form.redirectURI = redirectURI; - request.post({url: this.tokenUrl, form: form}, function (err, resp, body) { - /*check response status*/ - //we will let user defined middleware take it from here. - // At a minimum the response will contain parameters listed here: - // https://tools.ietf.org/html/rfc6749#section-5.1 - //anything else is beyond the oauth2 spec and is provider specific - callback(JSON.parse(body)); - }); -}; - - - From 175717bf115bc44210d60edcaa869fb4f5852031 Mon Sep 17 00:00:00 2001 From: shepp Date: Tue, 15 Nov 2016 13:35:28 -0500 Subject: [PATCH 20/20] mostly code cleanup --- middleware/bos-authentication.js | 152 ++++--------------------------- 1 file changed, 17 insertions(+), 135 deletions(-) diff --git a/middleware/bos-authentication.js b/middleware/bos-authentication.js index e8a62c8..0a5a753 100644 --- a/middleware/bos-authentication.js +++ b/middleware/bos-authentication.js @@ -17,34 +17,28 @@ function init(app, logger, serviceLoader, swagger) { _.forEach(swagger.getSimpleSpecs(), function (api, name) { var basePath = api.basePath || ''; /* apply security requirements to each route path*/ - _.keys(api.paths).forEach(function (path) { + _.forEach(_.keys(api.paths), function (path) { var pathObj = api.paths[path]; var routePath = basePath + _convertPathToExpress(path); - //loop for http method keys, like get an post - _.keys(pathObj).forEach(function (method) { + //loop for http method keys, like get and post + _.forEach(_.keys(pathObj), function (method) { if (_.contains(httpMethods, method)) { var operation = pathObj[method]; - if (operation['security']) { - operation['security'].forEach(function (securityReq) { - _.forOwn(securityReq, function (scopes, securityDefn) { - _applySecurityRequirement(app, method, routePath, securityDefn, - api.securityDefinitions[securityDefn], - /*operation['x-bos-permissions'][securityReq],*/ - scopes); - }); + _.forEach(operation['security'], function (securityReq) { + _.forOwn(securityReq, function (scopes, securityDefn) { + _applySecurityRequirement(app, method, routePath, securityDefn, + api.securityDefinitions[securityDefn], + scopes); }); - } - if (operation['x-bos-security']) { - operation['x-bos-security'].forEach(function (securityReq) { - _.forOwn(securityReq, function (scopes, securityDefn) { - _applyCustomSecurityRequirement(app, method, routePath, securityDefn, - api['x-bos-securityDefinitions'][securityDefn], - /*operation['x-bos-permissions'][securityReq],*/ - scopes); - }); + }); + _.forEach(operation['x-bos-security'], function (securityReq) { + _.forOwn(securityReq, function (scopes, securityDefn) { + _applyCustomSecurityRequirement(app, method, routePath, securityDefn, + api['x-bos-securityDefinitions'][securityDefn], + scopes); }); - } + }); } }); }); @@ -52,7 +46,7 @@ function init(app, logger, serviceLoader, swagger) { } function _applyCustomSecurityRequirement(app, method, route, securityReq, - securityDefn, /*requiredPermissions,*/ requiredScopes) { + securityDefn, requiredScopes) { //load security def middleware if (securityDefn['x-bos-middleware']) { var customAuthMiddleware = loader.getConsumer('middleware', securityDefn['x-bos-middleware']); @@ -94,7 +88,7 @@ function wireAuthenticateToRoute(app, method, route, securityReq, securityDefn, } function _applySecurityRequirement(app, method, route, securityReq, - securityDefn, /*requiredPermissions,*/ requiredScopes) { + securityDefn, requiredScopes) { //allow use of custom middleware even if a custom security definition was not used if (securityDefn['x-bos-middleware']) { _applyCustomSecurityRequirement(app, method, route, securityReq, @@ -122,52 +116,8 @@ function _applySecurityRequirement(app, method, route, securityReq, securityDefn.type, securityReq); } } - /*//wire up path with user defined authentication method for this req - if (cfg.authenticationMethods[securityReq]) { - var parts = cfg.authenticationMethods[securityReq].split('.'); - var service = loader.get(parts[0]); - if (!service) { - return log.warn('Could not find service module named "%s".', parts[0]); - } - var serviceMethod = service[parts[1]]; - if (!_.isFunction(serviceMethod)) { - return log.warn('Authentication function %s on module %s is missing or invalid.', - parts[1], parts[0]); - } - //scopes included here for security type oauth2 where authentication/authorization happens in one go - app[method].call(app, route, _.partialRight(serviceMethod, securityReq, - securityDefn, requiredScopes)); - //wire up path with user defined authorization method - if (cfg.authorizationMethods[securityReq]) { - parts = cfg.authorizationMethods[securityReq].split('.'); - service = loader.get(parts[0]); - if (!service) { - return log.warn('Could not find service module named "%s".', parts[0]); - } - serviceMethod = service[parts[1]]; - if (!_.isFunction(serviceMethod)) { - return log.warn('Authorization function %s on module %s is missing or invalid.', - parts[1], parts[0]); - } - var wrappedAuthorizationMethod = wrapAuthorizationMethod(serviceMethod, route, - securityDefn, requiredPermissions); - app[method].call(app, route, _.partialRight(wrappedAuthorizationMethod, route, - securityDefn, requiredPermissions)); - } else { - return log.warn('No authorization method found for security requirement %s', securityReq); - } - } else { - return log.warn('No authentication method defined for security requirement %s', securityReq); - }*/ } -/*function wrapAuthorizationMethod(authorizationMethod, route, securityDefn, requiredPermissions) { - return function (req, res, next) { - var runTimeRequiredPermissions = _expandRouteInstancePermissions(requiredPermissions, route, req.path); - authorizationMethod.call(this, req, res, next, securityDefn, runTimeRequiredPermissions); - }; -}*/ - function basicAuthentication(securityReq) { return function (req, res, next) { if (!req.bosAuthenticationData) { @@ -183,10 +133,6 @@ function basicAuthentication(securityReq) { authenticationData.username = credentials[0]; authenticationData.password = credentials[1]; } - /*if (!(req.bosAuthenticationData.username && req.bosAuthenticationData.password)) { - res.setHeader('WWW-Authenticate', 'Basic realm="' + securityReq + '"'); - return res.status(401).send(); - }*/ next(); }; } @@ -198,20 +144,6 @@ function apiKeyAuthentication(securityReq, securityDefn) { } var authenticationData = {type: securityDefn.type, securityReq: securityReq, securityDefn: securityDefn}; req.bosAuthenticationData.push(authenticationData); - /*if (req.get('authorization')) { - var digestHeader = req.get('authorization').split('Digest ')[1]; - if (digestHeader) { - //should be form of username="Mufasa", realm="myhost@example.com" - //treating this like the digest scheme defined in the rfc - var authorizationHeaderFields = digestHeader.split(', '); - authorizationHeaderFields.forEach(function (header) { - //should be form of username="Mufasa" - var keyValPair = header.split('='); - authenticationData[keyValPair[0]] = keyValPair[1].substring(1, keyValPair[1].length - 1); - }); - return next(); - } - }*/ if (securityDefn.in === 'query') { authenticationData.password = req.query[securityDefn.name]; } @@ -222,24 +154,7 @@ function apiKeyAuthentication(securityReq, securityDefn) { log.warn('unknown location %s for apiKey. ' + 'looks like open api specs may have changed on us', securityDefn.in); } - /*if (!(req.bosAuthenticationData.password)) { - res.setHeader('WWW-Authenticate', 'Digest realm="' + securityReq + '"'); - return res.status(401).send(); - }*/ next(); - //this would have to be a user provided function that - //fetches the user (and thus the private key that we need to compute the hash) from some data source - //we don't need this if we decide that we will let the user figure out how to verify the digest - /*verify(apiId, function (user) { - //regenerate hash with apiKey - //hash will include symmetric apiKey, one or more of: - //request method, content-md5 header, request uri, timestamp, socket.remoteAddress, req.ip, ip whitelist? - //if (hash === digest) - // all good - // else you suck - req.bosAuth.user = user; - next(); - });*/ }; } @@ -285,39 +200,6 @@ function apiKeyAuthentication(securityReq, securityDefn) { }; }*/ -/*function _expandRouteInstancePermissions(perms, route, uri) { - relate the route path parameters to the url instance values - perms: ["api:read:{policyid}", "api:read:{claimid}"] - route: /api/v1/policies/:policyid/claims/:claimid - [ api,v1,policies,:policyid,claims,:claimid ] - uri: /api/v1/policies/SFIH1234534/claims/37103 - [ api,v1,policies,SFIH1234534,claims,37103 ] - - if (!_.isString(route) || !_.isString(uri)) { - return perms; - } - var routeParts = route.split('/'); - var uriParts = uri.split('/'); - - // [ [ ':policyid', 'SFIH1234534' ], [ ':claimid', '37103' ] ] - var pathIds = _.zip(routeParts, uriParts) - .filter(function (b) { - return _.startsWith(b[0], ':'); - }).map(function (path) { - // trim the : - path[0] = path[0].substr(1); - return path; - }); - - return _.map(perms, function (perm) { - var ePerm = perm; - _.forEach(pathIds, function (item) { - ePerm = ePerm.replace('{' + item[0] + '}', item[1]); - }); - return ePerm; - }); -}*/ - //swagger paths use {blah} while express uses :blah function _convertPathToExpress(swaggerPath) { var reg = /\{([^\}]+)\}/g; //match all {...}