-
Notifications
You must be signed in to change notification settings - Fork 111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Support for OCI Authentication Protocol #1109
base: develop
Are you sure you want to change the base?
Changes from all commits
aff9762
7b21616
6d7d251
59d6bc5
a0e7fec
c30b625
bae9ea7
73ec96b
2a02c7a
b6f698f
90a1eff
5f6272c
794960b
f797baf
73357eb
9662cfb
2d1c422
e43701c
fc105ea
ecafeb7
92e55aa
d519f37
5d9aa1d
83e11f5
302bd63
f11bb0e
42dae80
0a46aea
41c7534
fc10879
dea3b35
a69f4a4
bd3d714
eae8afb
9dd566a
755d503
0c40f59
d1e7f0d
d0e4ac8
4a9e594
d3afb2f
39c85c7
3adba22
c6a4860
66a16ea
522480b
9a396a9
88f6bcf
c37cc0c
83e68e4
900e414
5a0b09f
9089e6d
0cbbc2d
464dd57
e4c5ee4
c4ba9e4
57d4e00
e57e674
25a8d66
6558fb3
1d8e1d4
a9ce626
03a212e
3471697
9a34087
8e08c42
62c854c
0225945
a788757
0085985
d2e8cea
7734c02
62083e3
3896edb
ddd7d4a
400c198
e69bfa0
6fedc6f
b089359
28cccf3
97a953c
a7b30b8
491dd93
f1e36b5
0bc859e
27d82d9
32dc9d9
a7027d4
c49a9b5
1d4c269
4a82dfc
0a70256
81578d2
38e9e66
e659a67
90681b5
034d1a7
ebb855b
369315e
915985e
2c57ea4
8e7f146
0ff9200
240a512
dd1905c
72ab1eb
4ae2bdf
704f5b9
3e79375
2d03d34
d36b556
a813cc2
860b4c6
9ef3d82
a90de54
59ac23d
44064f4
a944b88
ca3ec1d
9200d60
210a132
e7ad8ee
7103cc0
f8be798
aac783a
81c65e4
b5f829c
b733d35
652bbc9
3d21a3d
30426a0
9b7cbbe
da900db
e5d38c4
4532e6b
555cbbe
ade7087
f9fc8f0
458912e
94b7208
ae6d1c5
d87fb1f
d848664
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extra newline. |
||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we have to convert |
||
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(); | ||
}); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
postman-url-encoder
instead