UNPKG

@studyportals/aws4sign

Version:

443 lines (339 loc) 11.4 kB
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.&lt;region>.amazonaws.com/&lt;deploy_instance>/&lt;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; } }