@studyportals/aws4sign
Version:
443 lines (339 loc) • 11.4 kB
JavaScript
module.exports = class AWS4Sign{
/**
* Constructor for the class
*
* @param {Object} CryptoJS
*/
constructor(CryptoJS){
this.CryptoJS = CryptoJS;
}
/**
* Configuring the object
*
* @param {Object} options
* @param {Object} options.awsCredentials this has to be the AWS.config.credentials object obtained after logging in with Cognito Federated Identities
* @param {String} options.method the request http method: GET, PUT, POST, PATCH, DELETE
* @param {String} options.url the full url of the Api Gateway endpoint <i>https://xyz123.execute-api.<region>.amazonaws.com/<deploy_instance>/<endpoint></i>
* @param {Object} options.params a simple plain javascript object containing the parameters of the request, if the method is of type GET the params will be converted in querystring format
* @param {String} options.region the AWS region of the service you are making the request for
* @param {String} options.service the AWS standard service name,
* for a the whole list visit http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-aws-service-namespaces
*/
configure( options ){
this.method = (typeof options.method === 'string') ? options.method.toUpperCase() : 'GET';
if( typeof(options.url) !== 'string' || options.url.length === 0 ){
throw new Error('URL must be defined');
}
this.CanonicalURI = this.getCanonicalURI( options.url );
if( typeof(options.service) !== 'string' || options.service.length === 0 ){
throw new Error('Service must be defined');
}
this.service = options.service;
if( typeof(options.region) !== 'string' || options.region.length === 0 ){
throw new Error('Region must be defined');
}
this.region = options.region;
this.awsCredentials = options.awsCredentials;
this.QueryStringObject = {};
if( options.url.indexOf('?') !== -1 ){
this.buildQueryStringObject( options.url.substr(options.url.indexOf('?') + 1) );
}
if( this.method === 'GET' && typeof options.params === 'object' ){
this.buildQueryStringObject( options.params );
}
if( typeof options.params === 'object' && this.method !== 'GET' ){
this.params = options.params;
}
this.CanonicalQueryString = this.getCanonicalQueryString(this.QueryStringObject);
this.CleanQueryString = this.getCleanQueryString();
this.initHeaders( options );
this.normalizedHeaders = this.normalizeHeaders( this.headers );
this.setCleanURL( options );
}
initHeaders(options){
this.headers = {
'Host': this.getHost(options.url),
'X-Amz-Date': this.getISO8601Date()
};
if( typeof options.params === 'object'
&& this.method.toUpperCase() !== 'GET'
&& Object.keys(options.params).length > 0 ){
this.headers['Content-Type'] = 'application/json';
}
}
setCleanURL(options){
if( options.url.indexOf('?') !== -1 ){
this.cleanURL = options.url.substr( 0, options.url.indexOf('?') );
} else {
this.cleanURL = options.url;
}
if( this.CleanQueryString.length > 0 ){
this.cleanURL += '?' + this.CleanQueryString;
}
}
buildQueryStringObject(params){
if( typeof params === "string" ){
params.split('&').forEach((param) => {
let pair = param.split('=');
this.QueryStringObject[pair[0]] = encodeURIComponent( pair[1] );
});
} else if( typeof params === 'object' ){
for( let key in params ){
if(params.hasOwnProperty(key)){
this.QueryStringObject[key] = encodeURIComponent( params[key] );
}
}
}
}
/**
* Based on the type of <i>elem</i> parameters:
* If the type is String, extracts, if any, the querystring part of the url
* and normalize it according to RFC 3986.
* If the type is Object and the method is GET, creates a querystring representation
* and normalize it according to RFC 3986.
*
* @param {String|Object} elem
* @returns {string}
*/
getCanonicalQueryString(elem){
let cqs = '';
if(typeof elem === 'string') {
let regex = '?';
let begin = elem.indexOf(regex);
if (begin !== -1) {
cqs = elem.substring(begin + regex.length).trim();
if (cqs.indexOf('&') !== -1) {
let qsArray = cqs.split("&").sort();
cqs = decodeURI( qsArray.join("&").trim() );
cqs = encodeURI( cqs );
}
}
} else if(typeof elem === 'object' && this.method === 'GET'){
let qsArray = [];
Object.keys(elem).forEach(function(key, index){
elem[key] = decodeURIComponent( elem[key] );
qsArray[index] = (key + '=' + encodeURIComponent(elem[key]) );
elem[key] = encodeURIComponent( elem[key] );
});
cqs = qsArray.sort().join("&").trim();
}
return cqs;
}
/**
* Extracts the uri and returns the normalized paths according to RFC 3986.
*
* @param {String} url
* @returns {string}
*/
getCanonicalURI(url){
let regex = '//';
let begin = url.indexOf(regex);
if(begin !== -1){
url = url.substring(begin + regex.length);
regex = '/';
let endRegex = '?';
begin = url.indexOf(regex);
let end = -1;
if(begin !== -1){
url = url.substring(begin);
end = url.indexOf(endRegex);
}
if(end !== -1){
url = url.substring(0, end);
}
}
url = url.trim();
return encodeURI(url);
}
/**
* Extracts the host information and saves it as object variable.
*
* @param url
*/
getHost(url){
let regex = '//';
let begin = url.indexOf(regex);
if(begin !== -1){
url = url.substring(begin + regex.length);
let endRegex = '/';
let end = url.indexOf(endRegex);
if(end !== -1){
url = url.substring(0, end);
}
}
return url;
}
/**
* Returns a string representing the current date in YYYYMMDD format.
*
* @returns {string}
*/
getDate(){
const date = new Date();
let year = date.getFullYear();
let month = date.getMonth()+1;
month = (month<10)?('0'+month):month;
let day = date.getUTCDate();
day = (day<10)?('0'+day):day;
return '' + year + month + day;
}
/**
* Returns a string representing the current time in ISO8601 format
* YYYYMMDD'T'HHMMSS'Z'
*
* @returns {string}
*/
getISO8601Date(){
const date = new Date();
let hour = date.getUTCHours();
hour = (hour<10)?('0'+hour):hour;
let minutes = date.getMinutes();
minutes = (minutes<10)?('0'+minutes):minutes;
let seconds = date.getSeconds();
seconds = (seconds<10)?('0'+seconds):seconds;
return this.getDate() + 'T' + hour + minutes + seconds + 'Z';
}
/**
* Extracts the headers from passed object, set them to the request and normalize them for the canonical request.
* The normalized values are saved in the <i>headers</i> variable.
*
* @param reqHeaders
*/
normalizeHeaders(reqHeaders){
let objectHeaders = [];
if(typeof reqHeaders !== 'undefined' && reqHeaders !== null){
Object.keys(reqHeaders).forEach(function(key) {
let newKey = key.trim().replace(' ',' ').toLowerCase();
let newVal = reqHeaders[key].trim();
while(newVal.indexOf(' ') !== -1){
newVal = newVal.replace(' ',' ');
}
objectHeaders[newKey] = newVal;
});
}
return objectHeaders;
}
/**
* Returns the headers of the request in canonical form.
*
* @returns {string}
*/
getSignedHeaders(){
return Object.keys(this.normalizedHeaders).sort().join(';');
}
/**
* Returns the headers entries of the request in canonical form.
*
* @returns {string}
*/
getCanonicalHeaders(){
let canonicalHeadersEntry = '';
let objectHeaders = this.normalizedHeaders;
Object.keys(objectHeaders).sort().forEach(function(key) {
canonicalHeadersEntry += key + ':' + objectHeaders[key] + '\n';
});
return canonicalHeadersEntry;
}
/**
* Prepare the request adding the Authorization and the X-Amz-Security-Token
* headers.
* The X-Amz-Security-Token is needed because we are accessing AWS resources
* with Cognito Federated Identities.
*/
prepareRequest(){
//create the Authorization header
let authorization = 'AWS4-HMAC-SHA256 '
+ 'Credential=' + this.awsCredentials.accessKeyId + '/' + this.getDate() + '/' + this.region + '/' + this.service + '/aws4_request, '
+ 'SignedHeaders=' + this.getSignedHeaders() + ', '
+ 'Signature=' + this.getSignature();
this.headers['Authorization'] = authorization;
this.headers['X-Amz-Security-Token'] = this.awsCredentials.sessionToken;
}
/**
* Creates the signature to add to the request.
*
* @returns {string}
*/
getSignature(){
//create signing key
let kDate = this.CryptoJS.HmacSHA256(this.getDate(),"AWS4" + this.awsCredentials.secretAccessKey);
let kRegion = this.CryptoJS.HmacSHA256(this.region,kDate);
let kService = this.CryptoJS.HmacSHA256(this.service,kRegion);
let signignKey = this.CryptoJS.HmacSHA256("aws4_request",kService);
//create the string to sign
let stringToSign = 'AWS4-HMAC-SHA256\n'
+ this.normalizedHeaders['x-amz-date'] + '\n'
+ this.getDate() + '/' + this.region + '/' + this.service + '/aws4_request\n'
+ this.getCanonicalRequestDigest();
//create the signature
return this.CryptoJS.HmacSHA256(stringToSign,signignKey).toString();
}
/**
* Returns the SHA-256 hashed canonical request represented as a string of
* lowercase hexademical characters.
*
* @returns {string}
*/
getCanonicalRequestDigest(){
return this.CryptoJS.SHA256(this.getCanonicalRequest())
.toString()
.toLowerCase();
}
/**
* Method to get the request in canonical form once that all fields have been set.
*
* @returns {string} canonical request
*/
getCanonicalRequest(){
return this.method + '\n' +
this.CanonicalURI + '\n' +
this.CanonicalQueryString + '\n' +
this.getCanonicalHeaders() + '\n' +
this.getSignedHeaders() + '\n' +
this.getHashedPayload();
}
/**
* Returns the hashed payload of the request as a lowercase hexadecimal string.
*
* @returns {string}
*/
getHashedPayload(){
let payload = '';
if(this.method === 'GET' ){
payload = this.CryptoJS.SHA256('');
} else {
let encrypt = (typeof this.params !== 'undefined') ? JSON.stringify(this.params) : '';
payload = this.CryptoJS.SHA256( encrypt );
}
return payload.toString().toLowerCase();
}
/**
* Utility method to convert a simple javascript object into query string.
* @returns {string}
*/
getCleanQueryString(){
let str = [];
for(let p in this.QueryStringObject){
if (this.QueryStringObject.hasOwnProperty(p)) {
str.push( p + "=" + this.QueryStringObject[p] );
}
}
return str.join("&");
}
/**
* Signs the request and returns an object containing the headers for the request, method, url and data
* @returns {object}
*/
getXmlHttpRequestOptions(){
this.prepareRequest();
let options = {
url: this.cleanURL,
method: this.method,
headers: this.headers
};
if( this.method !== 'GET' && typeof this.params !== 'undefined' ){
options.data = JSON.stringify(this.params);
}
return options;
}
}