A lightweight, environment-agnostic implementation of the AWS Signature v4 Signing Process. Even works with React Native!
You can install via Yarn or npm
yarn add agnostic-aws-signature
npm install agnostic-aws-signature
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
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 });
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);
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';
Here is a list of all of the functions exposed by this library
value {String}
Value to be hashed
Returns the SHA256 Hash of the value
.
const hashedString = hash('test'); // Returns '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
secret {String}
Secret Key used in the HMAC-SHA256 Algorithmvalue {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'
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/'
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'
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'
});
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',
});
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 requestheaders {Object}
An Object containing any number of headers that belong in the final requestpayload {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
datetime {String}
The current datetime in an ISO8601 format.region {String}
The region where the AWS Service you want to request existsservice {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'
datetime {String}
The current datetime in an ISO8601 format.credentialScope {String}
The credential scope built previouslyhashedCanonicalRequest {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'
secretKey {String}
A valid AWS Secret Keydatetime {String}
The current datetime in an ISO8601 format.region {String}
The region where the AWS Service you want to request existsservice {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'
accessKey {String}
A valid AWS Access KeycredentialScope {String}
The credential scope built previouslyheaders {Object}
An Object containing any number of headers that belong in the final requestsignature {String}
The hex representation of a HMAC-SHA256 hash withsigningKey
andstringToSign
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'
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'
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' ]
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.
It's easy to get up and running locally! Just clone the repo, install the node modules and away you go! 🚀
> git clone [email protected]:RBrNx/agnostic-aws-signature.git
> cd agnostic-aws-signature
> yarn install # Alternatively use `npm install`
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