diff --git a/lib/authorizer/index.js b/lib/authorizer/index.js index cfad4cf0b..917433126 100644 --- a/lib/authorizer/index.js +++ b/lib/authorizer/index.js @@ -88,7 +88,8 @@ _.forEach({ oauth2: require('./oauth2'), ntlm: require('./ntlm'), apikey: require('./apikey'), - edgegrid: require('./edgegrid') + edgegrid: require('./edgegrid'), + oci: require('./oci-v1') }, AuthLoader.addHandler); /** diff --git a/lib/authorizer/oci-v1.js b/lib/authorizer/oci-v1.js new file mode 100644 index 000000000..5f8427e38 --- /dev/null +++ b/lib/authorizer/oci-v1.js @@ -0,0 +1,292 @@ +/** + * @fileOverview + * + * Implements the Oracle Cloud Infrastructure Signature v1 authentication method. + * Specification document: https://docs.cloud.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm + */ +var sshpk = require('sshpk'), + UrlParser = require('url'), + crypto = require('crypto'), + Headers = require('./util').Headers, + _ = require('lodash'), + httpSignature = require('http-signature'), + urlEncoder = require('postman-url-encoder'), + RequestBody = require('postman-collection').RequestBody, + bodyBuilder = require('../requester/core-body-builder'); + + +const HEADER_CONTENT_SHA = 'x-content-sha256', + HEADER_CONTENT_LEN = 'content-length', + HEADER_CONTENT_TYPE = 'content-type', + EMPTY_SHA = '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=', + HEADER_AUTHORIZATION = 'authorization', + HEADER_DATE = 'x-date', + HEADER_HOST = 'host', + CONTENT_TYPE_JSON = 'application/json', + EXTRA_HEADER_METHODS = ['POST', 'PUT', 'PATCH'], + PSEUDOHEADER_REQUEST_TARGET = '(request-target)'; + +// -------------------- POLYFILL for input to httpSignature.sign --------------------------- + +class SignerRequest { + constructor (method, url, headers) { + this.headers = headers; + this.method = method; + this.path = UrlParser.parse(url).path; + } + + getHeader (name) { + return this.headers.get(name); + } + + setHeader (name, value) { + this.headers.set(name, value); + } +} + +function computeBodyHash (body, algorithm, digestEncoding, callback) { + if (!(body && algorithm && digestEncoding) || body.isEmpty()) { return callback(); } + + var hash = crypto.createHash(algorithm), + originalReadStream, + rawBody, + urlencodedBody, + graphqlBody; + + + if (body.mode === RequestBody.MODES.raw) { + rawBody = bodyBuilder.raw(body.raw).body; + hash.update(rawBody); + + // hash.update('\n'); + return callback(hash.digest(digestEncoding), rawBody.length); + } + + if (body.mode === RequestBody.MODES.urlencoded) { + urlencodedBody = bodyBuilder.urlencoded(body.urlencoded).form; + urlencodedBody = urlEncoder.encodeQueryString(urlencodedBody); + hash.update(urlencodedBody); + hash.update('\n'); + + return callback(hash.digest(digestEncoding), urlencodedBody.length + 1); + } + + if (body.mode === RequestBody.MODES.file) { + originalReadStream = _.get(body, 'file.content'); + + if (!originalReadStream) { + return callback(); + } + + return originalReadStream.cloneReadStream(function (err, clonedStream) { + if (err) { return callback(); } + var streamContentLength = 0; + + clonedStream.on('data', function (chunk) { + hash.update(chunk); + streamContentLength += chunk.length; + }); + + clonedStream.on('end', function () { + hash.update('\n'); + callback(hash.digest(digestEncoding), streamContentLength); + }); + }); + } + + if (body.mode === RequestBody.MODES.graphql) { + graphqlBody = bodyBuilder.graphql(body.graphql).body; + hash.update(graphqlBody); + hash.update('\n'); + + return callback(hash.digest(digestEncoding), graphqlBody.length + 1); + } + + // @todo: Figure out a way to calculate hash for formdata body type. + + // ensure that callback is called if body.mode doesn't match with any of the above modes + return callback(); +} +// ------------------------------------------------------------ + +// eslint-disable-next-line one-var +var convertToDomHeadersObject = function (postmanHeaders) { + let rawHeaders = {}; + + postmanHeaders.all().forEach(function (each) { + rawHeaders[each.key.toLowerCase()] = each.value; + }); + + return new Headers(rawHeaders); + }, + getBodyDetails = function (body, callback) { + computeBodyHash(body, 'sha256', 'base64', callback); + }, + processRequestForOci = function (request, auth, callback) { + getBodyDetails(request.body, function (bodyHash, bodyContentLength) { + const rawRequestUri = request.url.getRaw(), + domHeadersObject = convertToDomHeadersObject(request.headers), + uppercaseMethod = request.method.toUpperCase(), + minimumHeadersToSign = [HEADER_DATE, PSEUDOHEADER_REQUEST_TARGET, HEADER_HOST], + keyId = auth.get('tenancy') + '/' + auth.get('user') + '/' + auth.get('fingerprint'), + privateKeyBuffer = sshpk.parsePrivateKey(auth.get('privatekey'), 'auto').toBuffer('pem', {}); + + if (!domHeadersObject.has(HEADER_HOST)) { + const url = UrlParser.parse(rawRequestUri); + + if (url.host) { + domHeadersObject.set(HEADER_HOST, url.host); + } + else { + return callback(new Error('Cannot parse host from url')); + } + } + + if (!domHeadersObject.has(HEADER_DATE)) { + domHeadersObject.set(HEADER_DATE, new Date().toUTCString()); + } + let headersToSign = [...minimumHeadersToSign]; + + if (!auth.get('forceDisableBodyHashing') && bodyHash && bodyContentLength && + _.includes(EXTRA_HEADER_METHODS, uppercaseMethod)) { + if (!domHeadersObject.has(HEADER_CONTENT_TYPE)) { + domHeadersObject.set(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON); + } + else if (domHeadersObject.get(HEADER_CONTENT_TYPE) !== CONTENT_TYPE_JSON) { + return callback(new Error(`Only ${CONTENT_TYPE_JSON} is supported by OCI Auth`)); + } + + if (bodyHash) { + domHeadersObject.set(HEADER_CONTENT_SHA, bodyHash); + } + if (bodyContentLength === 0) { + // if buffer is empty, it can only be an empty string payload + domHeadersObject.set(HEADER_CONTENT_SHA, EMPTY_SHA); + } + if (!domHeadersObject.has(HEADER_CONTENT_LEN)) { + domHeadersObject.set(HEADER_CONTENT_LEN, `${bodyContentLength}`); + } + headersToSign = headersToSign.concat(HEADER_CONTENT_TYPE, HEADER_CONTENT_LEN, HEADER_CONTENT_SHA); + } + + httpSignature.sign(new SignerRequest(uppercaseMethod, rawRequestUri, domHeadersObject), { + key: privateKeyBuffer, + keyId: keyId, + headers: headersToSign + }); + // NOTE: OCI needs 'authorization' but our version of httpSignature (1.3.1) + // puts signature in 'Authorization' + // eslint-disable-next-line one-var + const AUTH_HEADER_BACKWARD_COMPATIBLE = 'Authorization', + authorizationHeader = domHeadersObject.get(AUTH_HEADER_BACKWARD_COMPATIBLE); + + domHeadersObject.set(HEADER_AUTHORIZATION, + authorizationHeader.replace('Signature ', 'Signature version="1",')); + domHeadersObject.delete(AUTH_HEADER_BACKWARD_COMPATIBLE); + + return callback(domHeadersObject); + }); + }; + + +/** + * @implements {AuthHandlerInterface} + */ +module.exports = { + /** + * @property {AuthHandlerInterface~AuthManifest} + */ + // TODO: Fix the manifest + manifest: { + info: { + name: 'oci-v1', + version: '1.0.0' + }, + updates: [ + { + property: HEADER_AUTHORIZATION, + type: 'header' + }, + { + property: HEADER_DATE, + type: 'header' + }, + { + property: HEADER_CONTENT_LEN, + type: 'header' + }, + { + property: HEADER_CONTENT_TYPE, + type: 'header' + }, + { + property: HEADER_CONTENT_SHA, + type: 'header' + } + ] + }, + + /** + * Initializes a item (fetches all required parameters, etc) before the actual authorization step. + * + * @param {AuthInterface} auth AuthInterface instance created with request auth + * @param {Response} response Response of intermediate request (it any) + * @param {AuthHandlerInterface~authInitHookCallback} done Callback function called with error as first argument + */ + init: function (auth, response, done) { + done(null); + }, + + /** + * Checks the item, and fetches any parameters that are not already provided. + * + * @param {AuthInterface} auth AuthInterface instance created with request auth + * @param {AuthHandlerInterface~authPreHookCallback} done Callback function called with error, success and request + */ + pre: function (auth, done) { + done(null, Boolean(auth.get('tenancy') && auth.get('user') && + auth.get('fingerprint') && auth.get('privatekey'))); + }, + + /** + * Verifies whether the request was successful after being sent. + * + * @param {AuthInterface} auth AuthInterface instance created with request auth + * @param {Requester} response Response of the request + * @param {AuthHandlerInterface~authPostHookCallback} done Callback function called with error and success + */ + post: function (auth, response, done) { + done(null, true); + }, + + /** + * Signs a request. + * + * @param {AuthInterface} auth AuthInterface instance created with request auth + * @param {Request} request Request to be sent + * @param {AuthHandlerInterface~authSignHookCallback} done Callback function + */ + sign: function (auth, request, done) { + processRequestForOci(request, auth, function (result) { + if (!result) { return done(); } + if (result instanceof Error) { return done(result); } + const newHeaders = result, + setHeaderIfAvailable = function (headerName) { + const currentHeader = newHeaders.get(headerName); + + if (currentHeader) { + request.addHeader({ + key: headerName, + value: currentHeader, + system: true + }); + } + }, + allPossibleHeaders = [HEADER_AUTHORIZATION, HEADER_DATE, HEADER_CONTENT_LEN, + HEADER_CONTENT_SHA, HEADER_CONTENT_TYPE]; + + allPossibleHeaders.forEach(setHeaderIfAvailable); + done(); + }); + } +}; diff --git a/lib/authorizer/util.js b/lib/authorizer/util.js new file mode 100644 index 000000000..258d0f32a --- /dev/null +++ b/lib/authorizer/util.js @@ -0,0 +1,60 @@ + +function Headers (values) { + this._values = values || {}; +} + +Headers.prototype.append = function (name, value) { + this._values[name] = value; +}; + +Headers.prototype.delete = function (name) { + delete this._values[name]; +}; + +Headers.prototype.entries = function () { + const result = []; + + for (let key in this._values) { + result.push([key, this._values[key]]); + } + + return result; +}; + +Headers.prototype.get = function (name) { + return this._values[name]; +}; + +Headers.prototype.has = function (name) { + return name in this._values; +}; + +Headers.prototype.keys = function () { + const result = []; + + for (let key in this._values) { + result.push(key); + } + + return result; +}; + +Headers.prototype.set = function (name, value) { + this._values[name] = value; +}; + +// eslint-disable-next-line no-unused-vars +Headers.prototype.values = function (name, value) { + const result = []; + + // eslint-disable-next-line guard-for-in + for (let key in this._values) { + result.push(this._values[key]); + } + + return result; +}; + +module.exports = { + Headers: Headers +}; diff --git a/package-lock.json b/package-lock.json index f5e1c6fab..809c3b1b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3481,9 +3481,9 @@ "integrity": "sha1-qVPKZwB4Zp3eFCzomUAbnW6F07Q=" }, "http-signature": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.5.tgz", - "integrity": "sha512-NwoTQYSJoFt34jSBbwzDHDofoA61NGXzu6wXh95o1Ry62EnmKjXb/nR/RknLeZ3G/uGwrlKNY2z7uPt+Cdl7Tw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.1.tgz", + "integrity": "sha512-Y29YKEc8MQsjch/VzkUVJ+2MXd9WcR42fK5u36CZf4G8bXw2DXMTWuESiB0R6m59JAWxlPPw5/Fri/t/AyyueA==", "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -5400,17 +5400,6 @@ "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "resolve": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz", - "integrity": "sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - } } }, "normalize-path": { diff --git a/package.json b/package.json index c6c78fa19..8421e5abd 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,9 @@ "resolve-from": "5.0.0", "serialised-error": "1.1.3", "tough-cookie": "3.0.1", - "uuid": "3.4.0" + "uuid": "3.4.0", + "http-signature": "1.3.1", + "sshpk": "1.16.1" }, "devDependencies": { "@postman/shipit": "0.2.0", diff --git a/test/fixtures/auth-requests.json b/test/fixtures/auth-requests.json index 9c6e940b1..120755e13 100644 --- a/test/fixtures/auth-requests.json +++ b/test/fixtures/auth-requests.json @@ -247,5 +247,51 @@ "raw": "Hello World!!" }, "description": "" + }, + "ociWithoutBody": { + "auth": { + "type": "oci", + "oci": { + "tenancy": "ocid1.tenancy.oc1..aaaaaaaa5h4h55rwzjwrufcfaqp4h5gxcqv65ozk7rytrvcltwujfj45g25a", + "user": "ocid1.user.oc1..aaaaaaaax4esyzwnhlxdb6h5dfopzkuw5kujtffz3y4yvczqnjwlpbnk7q7q", + "fingerprint": "42:34:8b:df:0a:9c:b0:c8:b6:ac:7a:c2:ce:7e:45:85", + "privatekey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAvOxs9ybstMcalgCEbNIHeNh7oqVBLAC0+bhi7KEr2xYFSQws\nu4MdPXqSCTQu1/4fsMoCbRLZMRmC8YonjjhdRyVFZrAkE5Q9WfF3N40s/GVK8Myb\nWxBS5hZnc4td/d+OoGLE5aKNwaFNoTn7iDCU1JfG+d4l7Z5twzHESs3XnzNSeRms\n/MAFk3zdjlpOm9pbafFqFoim6Be8dRXtWU0xN5VRHD3txisQJlcLVNMm6IK9UWUg\nJzmn8d5ASwvU75/nJKc8DLoQ0cVeYaIhXWGME6ApXyb62zKmccPpkRzRZSJYPeUo\nLK2JC/w/os1NAi0mTGb/KqOzd7BJf1RO0SQYHwIDAQABAoIBAGRS4KJ2hyiQuquB\nZtEqz1D7FNyQCQXG8lPR+KZUCCf1j6T01CRXQgXNuxMsJNwKVOXawGMPqUqLk5+U\nj+DUSaRoRGx9iQrko5fJxFc5nvQBDkTFQpKfHoaUyybukWEpgGHnUUvogIKcl87Q\nLaRqt/4VA1LZByciBIOtP+HBYYlmGlg6UDeOFTku9nFbraz8FlzqLAy7xSUB3aDs\n1JeDwR9PBQsRG72GQkJR/EmPbD2Y/r+zmZP25/97pouO/ji9DJwDcVc1qFpn4JKf\nLABBUEwfWEIWa4cefbDi0dCzVKxrS/RU9EYB6HtMklcy3qyxbdcLmFY8qgsxjG8K\nw2iD2iECgYEA+AtmXff5lnSZq3CJhcxBpi+MuCByWH7gB2/G50XSduRmW+lzI6iv\n+r8ADEI87+KBIju++qNvj886N9H1sm+k6j3nEE51i2nwwKB79Xzu0J23+XMSCV0D\nYb6bvNogGCOhKslQNC51ZvgsZlTu6F8/EkPlboEL8E2h4eYwx3L6UbcCgYEAwvub\nB+rhUk4+TwXVnfNZEcmQiz90yU7CI/TjDrHVq+ZJIKRHILB+7Mt7Mpi+7Ie2Cx/f\nA1Alv8I44wmLhZX63Du9hwoz3t4Y0+AiT/RMp3EkmxKIkPFZNKsmY659dNzROMG9\nEVYEmBYode17mrSc4UJi8I4iHCv1A83BC3DhzNkCgYBSE8Od2I5YQOEQn76B7aLT\nkOxpiSMNMX6EN3KD2NDZ0p4kT81vwUPqHwWvCMQNRNAiOc7qNOaXmaTUD7/TeaoC\ncNViyfOPXFD0DKa4Bs0nbukPvAn92y9hSqdSCIOSOUf9VnwZqD3lAr3+ZqBfeBTw\nwFLNDBqdO7MTgw4lEWDPZQKBgQCIm3c2yN0sqR0GiHPtwfz5dVsL9F2CXOewAXbD\naNznHuTFNdOPl5iVlsqt3E6HsV1d1eqNi62m+BCbIKjb4wRrIH/dCMzmrHyAlVv0\n4JiEB8U50HZV9oIlYHP5Ctti8QK03Sf16Wo2k++tD8G11izDtZtUJoDHdBSpoZJK\nzb+loQKBgQC0/VewDbcRW5FymBPJiEnrTtpHk40x14pNfRbRrNDIx/jIx/gnqwyI\n6ud7uJkK+LAHw+r2HjHtSMVE9I54zr+QG8fAe8alJQ3mSP1tlpLbMaMb7LpomKo/\nfVVz1laGP3tcpXDEp80hbrDxO7aOexUgCFKHrYTAgVCm8Eqzz8HdsQ==\n-----END RSA PRIVATE KEY-----" + } + }, + "url": "https://identity.ap-hyderabad-1.oraclecloud.com/20160918/users/ocid1.user.oc1..aaaaaaaax4esyzwnhlxdb6h5dfopzkuw5kujtffz3y4yvczqnjwlpbnk7q7q", + "method": "GET" + }, + "ociWithBody": { + "auth": { + "type": "oci", + "oci": { + "tenancy": "ocid1.tenancy.oc1..aaaaaaaa5h4h55rwzjwrufcfaqp4h5gxcqv65ozk7rytrvcltwujfj45g25a", + "user": "ocid1.user.oc1..aaaaaaaax4esyzwnhlxdb6h5dfopzkuw5kujtffz3y4yvczqnjwlpbnk7q7q", + "fingerprint": "42:34:8b:df:0a:9c:b0:c8:b6:ac:7a:c2:ce:7e:45:85", + "privatekey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAvOxs9ybstMcalgCEbNIHeNh7oqVBLAC0+bhi7KEr2xYFSQws\nu4MdPXqSCTQu1/4fsMoCbRLZMRmC8YonjjhdRyVFZrAkE5Q9WfF3N40s/GVK8Myb\nWxBS5hZnc4td/d+OoGLE5aKNwaFNoTn7iDCU1JfG+d4l7Z5twzHESs3XnzNSeRms\n/MAFk3zdjlpOm9pbafFqFoim6Be8dRXtWU0xN5VRHD3txisQJlcLVNMm6IK9UWUg\nJzmn8d5ASwvU75/nJKc8DLoQ0cVeYaIhXWGME6ApXyb62zKmccPpkRzRZSJYPeUo\nLK2JC/w/os1NAi0mTGb/KqOzd7BJf1RO0SQYHwIDAQABAoIBAGRS4KJ2hyiQuquB\nZtEqz1D7FNyQCQXG8lPR+KZUCCf1j6T01CRXQgXNuxMsJNwKVOXawGMPqUqLk5+U\nj+DUSaRoRGx9iQrko5fJxFc5nvQBDkTFQpKfHoaUyybukWEpgGHnUUvogIKcl87Q\nLaRqt/4VA1LZByciBIOtP+HBYYlmGlg6UDeOFTku9nFbraz8FlzqLAy7xSUB3aDs\n1JeDwR9PBQsRG72GQkJR/EmPbD2Y/r+zmZP25/97pouO/ji9DJwDcVc1qFpn4JKf\nLABBUEwfWEIWa4cefbDi0dCzVKxrS/RU9EYB6HtMklcy3qyxbdcLmFY8qgsxjG8K\nw2iD2iECgYEA+AtmXff5lnSZq3CJhcxBpi+MuCByWH7gB2/G50XSduRmW+lzI6iv\n+r8ADEI87+KBIju++qNvj886N9H1sm+k6j3nEE51i2nwwKB79Xzu0J23+XMSCV0D\nYb6bvNogGCOhKslQNC51ZvgsZlTu6F8/EkPlboEL8E2h4eYwx3L6UbcCgYEAwvub\nB+rhUk4+TwXVnfNZEcmQiz90yU7CI/TjDrHVq+ZJIKRHILB+7Mt7Mpi+7Ie2Cx/f\nA1Alv8I44wmLhZX63Du9hwoz3t4Y0+AiT/RMp3EkmxKIkPFZNKsmY659dNzROMG9\nEVYEmBYode17mrSc4UJi8I4iHCv1A83BC3DhzNkCgYBSE8Od2I5YQOEQn76B7aLT\nkOxpiSMNMX6EN3KD2NDZ0p4kT81vwUPqHwWvCMQNRNAiOc7qNOaXmaTUD7/TeaoC\ncNViyfOPXFD0DKa4Bs0nbukPvAn92y9hSqdSCIOSOUf9VnwZqD3lAr3+ZqBfeBTw\nwFLNDBqdO7MTgw4lEWDPZQKBgQCIm3c2yN0sqR0GiHPtwfz5dVsL9F2CXOewAXbD\naNznHuTFNdOPl5iVlsqt3E6HsV1d1eqNi62m+BCbIKjb4wRrIH/dCMzmrHyAlVv0\n4JiEB8U50HZV9oIlYHP5Ctti8QK03Sf16Wo2k++tD8G11izDtZtUJoDHdBSpoZJK\nzb+loQKBgQC0/VewDbcRW5FymBPJiEnrTtpHk40x14pNfRbRrNDIx/jIx/gnqwyI\n6ud7uJkK+LAHw+r2HjHtSMVE9I54zr+QG8fAe8alJQ3mSP1tlpLbMaMb7LpomKo/\nfVVz1laGP3tcpXDEp80hbrDxO7aOexUgCFKHrYTAgVCm8Eqzz8HdsQ==\n-----END RSA PRIVATE KEY-----" + } + }, + "body": { + "mode": "raw", + "raw": "{\"compartmentId\": \"ocid1.tenancy.oc1..aaaaaaaa5h4h55rwzjwrufcfaqp4h5gxcqv65ozk7rytrvcltwujfj45g25a\",\"targets\": [\"192.0.2.0\"],\"protocol\": \"ICMP\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://healthchecks.ap-hyderabad-1.oraclecloud.com/20160918/users/ocid1.user.oc1..aaaaaaaax4esyzwnhlxdb6h5dfopzkuw5kujtffz3y4yvczqnjwlpbnk7q7q", + "protocol": "https", + "host": [ + "healthchecks.ap-hyderabad-1.oraclecloud", + "com" + ], + "path": [ + "20180501", + "pingProbeResults" + ] + }, + "method": "POST" } } diff --git a/test/unit/auth-handlers.test.js b/test/unit/auth-handlers.test.js index ff983960c..7217a9d34 100644 --- a/test/unit/auth-handlers.test.js +++ b/test/unit/auth-handlers.test.js @@ -2613,4 +2613,98 @@ describe('Auth Handler:', function () { }); }); }); + + describe('oci', function () { + it('should have correct structure of auth header - without body', function (done) { + var request = new Request(_.cloneDeep(rawRequests.ociWithoutBody)), + auth = request.auth, + authInterface = createAuthInterface(auth), + handler = AuthLoader.getHandler(auth.type), + headers; + + handler.sign(authInterface, request, function () { + headers = request.getHeaders({ + ignoreCase: true + }); + expect(Object.keys(headers)).to.eql(['authorization', 'x-date']); + expect(headers).to.have.property('authorization'); + expect(headers.authorization).to.include('Signature version="1"'); + expect(headers.authorization).to.include('x-date (request-target) host'); + expect(headers.authorization).to.include('algorithm="rsa-sha256"'); + done(); + }); + }); + it('should have correct structure of auth header - with body', function (done) { + var request = new Request(_.cloneDeep(rawRequests.ociWithBody)), + auth = request.auth, + authInterface = createAuthInterface(auth), + handler = AuthLoader.getHandler(auth.type), + headers; + + handler.sign(authInterface, request, function () { + headers = request.getHeaders({ + ignoreCase: true + }); + expect(Object.keys(headers).sort()) + .to.eql(['authorization', 'x-date', 'content-length', 'x-content-sha256', 'content-type'].sort()); + expect(headers.authorization).to.include('Signature version="1"'); + expect(headers.authorization) + .to.include('x-date (request-target) host content-type content-length x-content-sha256'); + expect(headers.authorization).to.include('algorithm="rsa-sha256"'); + done(); + }); + }); + it('should have correct structure of auth header - with body but force disabled', function (done) { + var rawRequest = _.merge( + _.cloneDeep(rawRequests.ociWithBody), + {auth: {oci: {forceDisableBodyHashing: true}}} + ), + request = new Request(rawRequest), + auth = request.auth, + authInterface = createAuthInterface(auth), + handler = AuthLoader.getHandler(auth.type), + headers; + + handler.sign(authInterface, request, function () { + headers = request.getHeaders({ + ignoreCase: true + }); + expect(Object.keys(headers)).to.eql(['authorization', 'x-date']); + expect(headers).to.have.property('authorization'); + expect(headers.authorization).to.include('Signature version="1"'); + expect(headers.authorization).to.include('x-date (request-target) host'); + expect(headers.authorization).to.include('algorithm="rsa-sha256"'); + done(); + }); + }); + it('should not work when content type is not JSON', function (done) { + var rawRequest = _.merge( + _.cloneDeep(rawRequests.ociWithBody), + {header: [{key: 'content-type', value: 'foobar'}]} + ), + request = new Request(rawRequest), + auth = request.auth, + authInterface = createAuthInterface(auth), + handler = AuthLoader.getHandler(auth.type); + + handler.sign(authInterface, request, function (res) { + expect(res).to.be.an.instanceof(Error); + done(); + }); + }); + it('should not allow sign to be reached if required parameters arent there', function (done) { + var rawRequest = _.cloneDeep(rawRequests.ociWithBody); + + rawRequest.auth.oci.privatekey = undefined; + var request = new Request(rawRequest), + auth = request.auth, + authInterface = createAuthInterface(auth), + handler = AuthLoader.getHandler(auth.type); + + handler.pre(authInterface, function (a, b) { + expect(b).to.be.eql(false); + done(); + }); + }); + }); });