From 368fe2403e93c6dfc68bb9b978a543959c95027f Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Thu, 7 Jul 2016 14:20:36 +0200 Subject: [PATCH] Allow to setup more than one domain to validate oauth against --- NEWS.md | 3 + app/auth/oauth.js | 82 +++++++++++++---------- config/environments/production.js.example | 3 + config/environments/test.js.example | 3 + test/unit/oauth.test.js | 26 ++++++- 5 files changed, 80 insertions(+), 37 deletions(-) diff --git a/NEWS.md b/NEWS.md index 88862481..98f2ccd6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,9 @@ 1.33.1 - 2016-mm-dd ------------------- +New features: + * Allow to setup more than one domain to validate oauth against. + 1.33.0 - 2016-07-01 ------------------- diff --git a/app/auth/oauth.js b/app/auth/oauth.js index 6c8809c2..9370fe58 100644 --- a/app/auth/oauth.js +++ b/app/auth/oauth.js @@ -3,6 +3,8 @@ var _ = require('underscore'); var OAuthUtil = require('oauth-client'); var step = require('step'); var assert = require('assert'); +var CdbRequest = require('../models/cartodb_request'); +var cdbReq = new CdbRequest(); var oAuth = (function(){ var me = { @@ -60,79 +62,87 @@ var oAuth = (function(){ return removed; }; + me.getAllowedHosts= function() { + var oauthConfig = global.settings.oauth || {}; + return oauthConfig.allowedHosts || ['carto.com', 'cartodb.com']; + }; // do new fancy get User ID me.verifyRequest = function(req, metadataBackend, callback) { var that = this; //TODO: review this var httpProto = req.protocol; - var passed_tokens; - var ohash; + if(!httpProto || (httpProto !== 'http' && httpProto !== 'https')) { + var msg = "Unknown HTTP protocol " + httpProto + "."; + var unknownProtocolErr = new Error(msg); + unknownProtocolErr.http_status = 500; + return callback(unknownProtocolErr); + } + + var username = cdbReq.userByReq(req); + var requestTokens; var signature; step( function getTokensFromURL(){ return oAuth.parseTokens(req); }, - function getOAuthHash(err, data){ + function getOAuthHash(err, _requestTokens) { assert.ifError(err); // this is oauth request only if oauth headers are present - this.is_oauth_request = !_.isEmpty(data); + this.is_oauth_request = !_.isEmpty(_requestTokens); if (this.is_oauth_request) { - passed_tokens = data; - that.getOAuthHash(metadataBackend, passed_tokens.oauth_token, this); + requestTokens = _requestTokens; + that.getOAuthHash(metadataBackend, requestTokens.oauth_token, this); } else { return null; } }, - function regenerateSignature(err, data){ + function regenerateSignature(err, oAuthHash){ assert.ifError(err); if (!this.is_oauth_request) { return null; } - ohash = data; - var consumer = OAuthUtil.createConsumer(ohash.consumer_key, ohash.consumer_secret); - var access_token = OAuthUtil.createToken(ohash.access_token_token, ohash.access_token_secret); + var consumer = OAuthUtil.createConsumer(oAuthHash.consumer_key, oAuthHash.consumer_secret); + var access_token = OAuthUtil.createToken(oAuthHash.access_token_token, oAuthHash.access_token_secret); var signer = OAuthUtil.createHmac(consumer, access_token); var method = req.method; - var host = req.headers.host; + var hostsToValidate = {}; + var requestHost = req.headers.host; + hostsToValidate[requestHost] = true; + that.getAllowedHosts().forEach(function(allowedHost) { + hostsToValidate[username + '.' + allowedHost] = true; + }); - if(!httpProto || (httpProto !== 'http' && httpProto !== 'https')) { - var msg = "Unknown HTTP protocol " + httpProto + "."; - err = new Error(msg); - err.http_status = 500; - callback(err); - return; - } - - var path = httpProto + '://' + host + req.path; that.splitParams(req.query); - - // remove signature from passed_tokens - signature = passed_tokens.oauth_signature; - delete passed_tokens.oauth_signature; - - var joined = {}; - // remove oauth_signature from body if(req.body) { delete req.body.oauth_signature; } - _.extend(joined, req.body ? req.body : null); - _.extend(joined, passed_tokens); - _.extend(joined, req.query); + signature = requestTokens.oauth_signature; + // remove signature from requestTokens + delete requestTokens.oauth_signature; + var requestParams = _.extend({}, req.body, requestTokens, req.query); - return signer.sign(method, path, joined); + var hosts = Object.keys(hostsToValidate); + var requestSignatures = hosts.map(function(host) { + var url = httpProto + '://' + host + req.path; + return signer.sign(method, url, requestParams); + }); + + return requestSignatures.reduce(function(validSignature, requestSignature) { + if (signature === requestSignature && !_.isUndefined(requestSignature)) { + validSignature = true; + } + return validSignature; + }, false); }, - function checkSignature(err, data){ - assert.ifError(err); - - //console.log(data + " should equal the provided signature: " + signature); - callback(err, (signature === data && !_.isUndefined(data)) ? true : null); + function finishValidation(err, hasValidSignature) { + return callback(err, hasValidSignature || null); } ); }; diff --git a/config/environments/production.js.example b/config/environments/production.js.example index 09e1abe3..238571ff 100644 --- a/config/environments/production.js.example +++ b/config/environments/production.js.example @@ -85,4 +85,7 @@ module.exports.health = { username: 'development', query: 'select 1' }; +module.exports.oauth = { + allowedHosts: ['carto.com', 'cartodb.com'] +}; module.exports.disabled_file = 'pids/disabled'; diff --git a/config/environments/test.js.example b/config/environments/test.js.example index 314983a5..513169d9 100644 --- a/config/environments/test.js.example +++ b/config/environments/test.js.example @@ -74,4 +74,7 @@ module.exports.health = { username: 'vizzuality', query: 'select 1' }; +module.exports.oauth = { + allowedHosts: ['localhost.lan:8080', 'localhostdb.lan:8080'] +}; module.exports.disabled_file = 'pids/disabled'; diff --git a/test/unit/oauth.test.js b/test/unit/oauth.test.js index 9571cc50..c3795326 100644 --- a/test/unit/oauth.test.js +++ b/test/unit/oauth.test.js @@ -78,7 +78,7 @@ it('test can access oauth hash for a user based on access token (oauth_token)', }); it('test non existant oauth hash for a user based on oauth_token returns empty hash', function(done){ - var req = {query:{}, headers:{authorization:full_oauth_header}}; + var req = {query:{}, params: { user: 'vizzuality' }, headers:{authorization:full_oauth_header}}; var tokens = oAuth.parseTokens(req); oAuth.getOAuthHash(metadataBackend, tokens.oauth_token, function(err, data){ @@ -91,6 +91,7 @@ it('test non existant oauth hash for a user based on oauth_token returns empty h it('can return user for verified signature', function(done){ var req = {query:{}, headers:{authorization:real_oauth_header, host: 'vizzuality.testhost.lan' }, + params: { user: 'vizzuality' }, protocol: 'http', method: 'GET', path: '/api/v1/tables' @@ -103,9 +104,31 @@ it('can return user for verified signature', function(done){ }); }); +it('can return user for verified signature (for other allowed domains)', function(done){ + var oAuthGetAllowedHostsFn = oAuth.getAllowedHosts; + oAuth.getAllowedHosts = function() { + return ['testhost.lan', 'testhostdb.lan']; + }; + var req = {query:{}, + headers:{authorization:real_oauth_header, host: 'vizzuality.testhostdb.lan' }, + params: { user: 'vizzuality' }, + protocol: 'http', + method: 'GET', + path: '/api/v1/tables' + }; + + oAuth.verifyRequest(req, metadataBackend, function(err, data){ + oAuth.getAllowedHosts = oAuthGetAllowedHostsFn; + assert.ok(!err, err); + assert.equal(data, 1); + done(); + }); +}); + it('returns null user for unverified signatures', function(done){ var req = {query:{}, headers:{authorization:real_oauth_header, host: 'vizzuality.testyhost.lan' }, + params: { user: 'vizzuality' }, protocol: 'http', method: 'GET', path: '/api/v1/tables' @@ -121,6 +144,7 @@ it('returns null user for no oauth', function(done){ var req = { query:{}, headers:{}, + params: { user: 'vizzuality' }, protocol: 'http', method: 'GET', path: '/api/v1/tables'