agnostic-aws-signature

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

Usage no npm install needed!

<script type="module">
  import agnosticAwsSignature from 'https://cdn.skypack.dev/agnostic-aws-signature';
</script>

README

Library's logo

Agnostic AWS Signature

A lightweight, environment-agnostic implementation of the AWS Signature v4 Signing Process. Even works with React Native!

Current npm package version. Bundle size Test coverage Agnostic AWS Signature is released under the ISC license.



Contents


Installation

You can install via Yarn or npm

yarn add agnostic-aws-signature
npm install agnostic-aws-signature

Usage Guide

Just need to import it, easy peasy!

import createAwsClient from 'agnostic-aws-signature'; // Either import the default export

import { createAwsClient } from 'agnostic-aws-signature'; // Or import the named export

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.

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 });

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 or simply want to make some modifications to the process you can roll your own implementation quite easily.

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);

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 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 and use either Option 1 or Option 2.

import 'react-native-url-polyfill/auto';

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.

const hashedString = hash('test'); // Returns '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'

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.

const hmacBinary = hmac('secretkey', 'test'); // Returns '{ sigBytes: 32, words: [-1682038133, 846640694, -339234647, 161088799, -24662870, -1503298019, -322824905, -477709546] }'
const hmacString = hmacBinary.toString(); // Returns '9bbe228b3276b636ebc7b0a9f665fae1fe87acaaa6657e1decc21537e386bb16'

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

const canonicalUri = buildCanonicalUri('/documents and settings/'); // Returns '/documents%20and%20settings/'

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

const queryString = buildCanonicalQueryString({ Action: 'ListUsers', Version: '2010-05-08' }); // Returns 'Action=ListUsers&Version=2010-05-08'

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

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'
});

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

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',
});

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

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

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

const credentialScope = buildCredentialScope('20200830T123600Z', 'eu-west-2', 'execute-api'); // Returns '20200830/eu-west-2/execute-api/aws4_request\n'

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

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'

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

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'

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)

Returns the Authorization header for a request as outlined in Step 1 of the AWS Docs - Add Signature to the Request

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'

extractHostname(url) => String

  • url {String} The URL we want to extract the hostname from

Returns the hostname of the provided URL.

const hostname = extractHostname('https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html'); // Returns 'docs.aws.amazon.com'

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

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' ]

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! 🚀

> git clone git@github.com:RBrNx/agnostic-aws-signature.git

> cd agnostic-aws-signature

> yarn install # Alternatively use `npm install`

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