UNPKG

agnostic-aws-signature

Version:

Environment Agnostic implementation of the AWS Signature v4 Signing Process. Even works with React Native!

407 lines (282 loc) 15.4 kB
<p align="center"> <img height="90" src="https://user-images.githubusercontent.com/1332314/89441622-57902080-d745-11ea-9dee-e030a435eefd.png" alt="Library's logo"> </p> <h3 align="center"> Agnostic AWS Signature </h3> <p align="center"> A lightweight, environment-agnostic implementation of the AWS Signature v4 Signing Process. Even works with React Native! </p> <p align="center"> <a href="https://www.npmjs.com/package/agnostic-aws-signature"> <img src="https://img.shields.io/npm/v/agnostic-aws-signature?style=flat-square" alt="Current npm package version." /> </a> <a href="https://www.npmjs.com/package/agnostic-aws-signature"> <img src="https://img.shields.io/bundlephobia/min/agnostic-aws-signature?style=flat-square" alt="Bundle size" /> </a> <a href="https://github.com/RBrNx/agnostic-aws-signature/blob/master/LICENSE"> <img src="https://raw.githubusercontent.com/RBrNx/agnostic-aws-signature/master/shields/coverage.svg" alt="Test coverage" /> </a> <a href="https://github.com/charpeni/react-native-url-polyfill/blob/master/LICENSE"> <img src="https://img.shields.io/npm/l/agnostic-aws-signature?style=flat-square" alt="Agnostic AWS Signature is released under the ISC license." /> </a> </p> <hr /> <br /> ## Contents - [Installation](#installation) - [Usage Guide](#usage-guide) - [Signing A Request](#signing-a-request) - [Rolling your own implementation](#rolling-your-own-implementation) - [Usage with React Native](#usage-with-react-native) - [API Reference](#api-reference) - [Contributing](#contributing) - [Local Development](#local-development) - [Code Quality](#code-quality) <br /> ## Installation You can install via Yarn or npm ```bash yarn add agnostic-aws-signature ``` ```bash npm install agnostic-aws-signature ``` <br /> ## Usage Guide Just need to import it, easy peasy! ```javascript import createAwsClient from 'agnostic-aws-signature'; // Either import the default export import { createAwsClient } from 'agnostic-aws-signature'; // Or import the named export ``` <br /> #### Signing a Request The key to communicating with an AWS Resource that requires use of the AWS Signature v4 process, is to send a set of Signed Headers along with our request. ```javascript import { Auth } from 'aws-amplify'; import createAwsClient from 'agnostic-aws-signature'; // createAwsClient requires a valid AWS AccessKey, SecretKey and SessionToken // I recommened getting them from Amplify using Auth.currentCredentials(); const { accessKeyId, secretAccessKey, sessionToken } = await Auth.currentCredentials(); const awsClient = createAwsClient(accessKeyId, secretAccessKey, sessionToken, { region: 'eu-west-2', // Your AWS resource region endpoint: API_URL, // Your AWS resource url }); const body = { firstName: 'Conor', role: 'Developer', }, // Sign our Request to allow User access to AWS resource const signedRequest = awsClient.signRequest({ method: 'POST', // Method of your request headers: { // Whatever headers you need to send to the resource accept: '*/*', 'content-type': 'application/json', }, body, // Whatever body you need to send to the resource }); // Use the newly signed headers const response = await fetch(signedRequest.url, { headers: signedRequest.headers, body }); ``` <br /> #### Rolling your own implementation `agnostic-aws-sgignature` exposes all of the helper functions it uses to produce the signed headers, so if you are following along with the [AWS Docs](https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html) or simply want to make some modifications to the process you can roll your own implementation quite easily. ```javascript import { Auth } from 'aws-amplify'; import { buildCanonicalRequest, calculateSigningKey, ... } from 'agnostic-aws-signature'; // Use any of the helper functions as you see fit const canonicalRequest = buildCanonicalRequest(requestMethod, requestPath, queryParams, headers, body); ``` <br /> #### Usage with React Native Since this library opts to use CryptoJS instead of relying on the Crypto module exposed by Node.js, it should work out of the box with React Native. Unfortunately the React Native team opted to roll their own `URL` [implementation](https://github.com/facebook/react-native/issues/16434) which does not support things like `new URL('https://www.google.com')`. To get around this (and to therefore use `agnostic-aws-signature` with RN) I would recommend you install [react-native-url-polyfill](https://github.com/charpeni/react-native-url-polyfill) and use either Option 1 or Option 2. ```javascript import 'react-native-url-polyfill/auto'; ``` <br/> ## API Reference Here is a list of all of the functions exposed by this library #### hash(_value_) => String - `value {String}` Value to be hashed Returns the SHA256 Hash of the `value`. ```javascript const hashedString = hash('test'); // Returns '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' ``` <br /> #### hmac(_secret, value_) => CryptoJS Binary - `secret {String}` Secret Key used in the HMAC-SHA256 Algorithm - `value {String}` Value to be hashed Returns the HMAC-SHA256 Hash of the `secret` and `value` in Binary Form. Use the `toString()` method to convert to the Hex representation. ```javascript const hmacBinary = hmac('secretkey', 'test'); // Returns '{ sigBytes: 32, words: [-1682038133, 846640694, -339234647, 161088799, -24662870, -1503298019, -322824905, -477709546] }' const hmacString = hmacBinary.toString(); // Returns '9bbe228b3276b636ebc7b0a9f665fae1fe87acaaa6657e1decc21537e386bb16' ``` <br /> #### buildCanonicalUri(_uri_) => String - `uri {String}` URI to be encoded Returns the encoded URI as outlined in Step 2 of the AWS Docs - [Create a Canonical Request](https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html) ```javascript const canonicalUri = buildCanonicalUri('/documents and settings/'); // Returns '/documents%20and%20settings/' ``` <br /> #### buildCanonicalQueryString(_queryParams_) => String - `queryParams {Object}` An Object containing any number of query parameters that will be converted to a query string Returns the Query String as outlined in Step 3 of the AWS Docs - [Create a Canonical Request](https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html) ```javascript const queryString = buildCanonicalQueryString({ Action: 'ListUsers', Version: '2010-05-08' }); // Returns 'Action=ListUsers&Version=2010-05-08' ``` <br /> #### buildCanonicalHeaders(_headers_) => String - `headers {Object}` An Object containing any number of headers that will be sent in the final request Returns a string of Header Keys and Values as outlined in Step 4 of the AWS Docs - [Create a Canonical Request](https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html) ```javascript const headers = buildCanonicalHeaders({ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', // Returns 'content-type:application/x-www-form-urlencoded; charset=utf-8\n HOST: 'iam.amazonaws.com', // host:iam.amazonaws.com\n 'x-amz-date': '20200830T123600Z', // x-amz-date:20200830T123600Z\n' }); ``` <br /> #### buildCanonicalSignedHeaders(_headers_) => String - `headers {Object}` An Object containing any number of headers that will be sent in the final request Returns a string of semi-colon delimited Header Keys as outlined in Step 5 of the AWS Docs - [Create a Canonical Request](https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html) ```javascript const signedHeaders = buildCanonicalSignedHeaders({ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', // Returns 'content-type;host;x-amz-date\n' HOST: 'iam.amazonaws.com', 'x-amz-date': '20200830T123600Z', }); ``` <br /> #### buildCanonicalRequest(_method, path, queryParams, headers, payload_) => String - `method {String}` The request method (GET, POST, etc) - `path {String}` The request path (everything after the endpoint i.e '/api/route') - `queryParams {Object}` An Object containing any number of query parameters that belong in the final request - `headers {Object}` An Object containing any number of headers that belong in the final request - `payload {Object|String}` The body of the request Returns a string of the final canonical request that is pieced together from all of the parameters. Outlined in Step 7 of the AWS Docs - [Create a Canonical Request](https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html) ```javascript const queryParams = { Action: 'ListUsers', Version: '2010-05-08' }; const headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', HOST: 'iam.amazonaws.com', 'x-amz-date': '20200830T123600Z', }; const canonicalRequest = buildCanonicalRequest('GET', '/api/users', queryParams, headers, ''); // Returns 'GET\n // /api/users\n // Action=ListUsers&Version=2010-05-08\n // content-type:application/x-www-form-urlencoded; charset=utf-8\n // host:iam.amazonaws.com\n // x-amz-date:20200830T123600Z\n // \n // content-type;host;x-amz-date\n // e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ``` <br /> #### buildCredentialScope(_datetime, region, service_) => String - `datetime {String}` The current datetime in an ISO8601 format. - `region {String}` The region where the AWS Service you want to request exists - `service {String}` An Object containing any number of query parameters that belong in the final request Returns the credential scope for a request as outlined in Step 3 of the AWS Docs - [Create a String to Sign](https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html) ```javascript const credentialScope = buildCredentialScope('20200830T123600Z', 'eu-west-2', 'execute-api'); // Returns '20200830/eu-west-2/execute-api/aws4_request\n' ``` <br /> #### buildStringToSign(_datetime, credentialScope, hashedCanonicalRequest_) => String - `datetime {String}` The current datetime in an ISO8601 format. - `credentialScope {String}` The credential scope built previously - `hashedCanonicalRequest {String}` A SHA256 hash of the canonicalRequest built previously Returns the 'string to sign' for a request as outlined in the AWS Docs - [Create a String to Sign](https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html) ```javascript const queryParams = { Action: 'ListUsers', Version: '2010-05-08' }; const headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', HOST: 'iam.amazonaws.com', 'x-amz-date': '20200830T123600Z', }; const canonicalRequest = buildCanonicalRequest('GET', '/api/users', queryParams, headers, ''); const credentialScope = buildCredentialScope('20200830T123600Z', 'eu-west-2', 'execute-api'); const hashedCanonicalRequest = hash(canonicalRequest); const stringToSign = buildStringToSign('20200830T123600Z', credentialScope, hashedCanonicalRequest); // Returns 'AWS4-HMAC-SHA256\n // 20200830T123600Z\n // 20200830/eu-west-2/execute-api/aws4_request\n // f122ea64ffc8fda0b9ffcbc71f07f7d2c23e19f2ad2db26fec414ff0a0a595b7' ``` <br /> #### calculateSigningKey(_secretKey, datetime, region, service_) => String - `secretKey {String}` A valid AWS Secret Key - `datetime {String}` The current datetime in an ISO8601 format. - `region {String}` The region where the AWS Service you want to request exists - `service {String}` An Object containing any number of query parameters that belong in the final request Returns the signing key for a request as outlined in Step 1 of the AWS Docs - [Calculate the Signature](https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html) ```javascript const signingKey = calculateSigningKey(mySecretKey, '20200830T123600Z', 'eu-west-2', 'execute-api'); // Returns 'AWS4-HMAC-SHA256\n // 20200830T123600Z\n // 20200830/eu-west-2/execute-api/aws4_request\n // f122ea64ffc8fda0b9ffcbc71f07f7d2c23e19f2ad2db26fec414ff0a0a595b7' ``` <br /> #### buildAuthorizationHeader(_accessKey, credentialScope, headers, signature_) => String - `accessKey {String}` A valid AWS Access Key - `credentialScope {String}` The credential scope built previously - `headers {Object}` An Object containing any number of headers that belong in the final request - `signature {String}` The hex representation of a HMAC-SHA256 hash with `signingKey` and `stringToSign` as parameters (See Step 2 of [Calculate the Signature](https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html)) Returns the Authorization header for a request as outlined in Step 1 of the AWS Docs - [Add Signature to the Request](https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html) ```javascript const credentialScope = buildCredentialScope('20200830T123600Z', 'eu-west-2', 'execute-api'); const headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', HOST: 'iam.amazonaws.com', 'x-amz-date': '20200830T123600Z', }; const signingKey = calculateSigningKey(mySecretKey, '20200830T123600Z', 'eu-west-2', 'execute-api'); const signature = hmac(signingKey, stringToSign).toString(); const authorizationHeader = buildAuthorizationHeader(myAccessKey, credentialScope, headers, signature); // Returns 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20200830/eu-west-2/execute-api/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7' ``` <br /> #### extractHostname(_url_) => String - `url {String}` The URL we want to extract the hostname from Returns the hostname of the provided URL. ```javascript const hostname = extractHostname('https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html'); // Returns 'docs.aws.amazon.com' ``` <br /> #### getHeaderKeys(_headers_) => [String] - `headers {Object}` An Object containing any number of headers that will be sent in the final request Returns an array containing each header key lowercased ```javascript const headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', HOST: 'iam.amazonaws.com', 'x-amz-date': '20200830T123600Z', }; const headerKeys = getHeaderKeys(headers); // Returns [ 'content-type', 'host', 'x-amz-date' ] ``` <br/> ## Contributing I am more than happy to accept any contributions anyone would like to make, whether that's raising an issue, suggesting an improvement or developing a new feature. #### Local Development It's easy to get up and running locally! Just clone the repo, install the node modules and away you go! 🚀 ```bash > git clone git@github.com:RBrNx/agnostic-aws-signature.git > cd agnostic-aws-signature > yarn install # Alternatively use `npm install` ``` <br/> #### Code Quality To help keep the code styling consistent across the repo, I am using ESLint and Prettier, along with Git Hooks to ensure that any pull requests will meet the code quality standards. While some of the hooks are specifically for code styling, there is a `pre-push` hook implemented that will run all of the Unit Tests before any commits are pushed. If any of the Unit Tests fail, or the overall Test Coverage drops below 95%, the push will fail