Calculate AWS v4 Signature with client-side JavaScript
My team recently completed a challenging 5-month product build that integrates with many Amazon Web Services (AWS) features. We had the opportunity to use some new services, namely AWS Cognito, which forced us to pioneer new solutions more than a typical project. First up: calculating AWS v4 signatures client-side to integrate EvaporateJS with AWS Cognito.
Benefits of AWS Cognito
Although some parts of AWS Cognito feel incomplete (multi-factor authentication and documentation come to mind), it offers great potential by allowing users to authenticate directly to the AWS ecosystem. When Member Matt logs in, he can run client-side JavaScript to authenticate and talk directly with the AWS API. And when Admin Amber logs in, Cognito's group feature allows her to run elevated commands that Matt cannot. This lets us securely interact with AWS without intermediary servers to proxy commands.
Case in point: uploading private files to AWS S3. We want users of our product to store sensitive files in S3 without the ability to read or overwrite another user's files. We could upload files to a custom API that identifies the user and saves their files to S3, but this adds a lot of overhead. Instead, Cognito gives each user their own AWS credentials so they can read and write directly to S3, and we can write AWS policies to isolate users (see "Segment S3 uploads by Cognito user").
Uploading to S3 with EvaporateJS
To upload files to S3 we chose EvaporateJS, which immediately added value like multi-part uploads, pausing, resuming, and progress callbacks.
The following code uses EvaporateJS 1.x. The current version of EvaporateJS, 2.x, adds customAuthMethod
so the implementation is slightly different. See this EvaporateJS 2.x example by paolavness.
import { assign, curry, mapKeys, map } from 'lodash';
import Evaporate from 'evaporate';
// These cryptography functions are defined later in the articleimport { hashSha256ToHex, hmacSha256 } from './crypto';
/**
* Generate the signature for AWS to authenticate the request.
* @param {String} secretKey - The IAM secret key of the AWS user making the request
* @param {Object} response - The response from the signerUrl or awsLambda requests
* @param {String} stringToSign - The "string to sign" generated in the previous step
* of the AWS Signature v4 process.
* @returns {String} - The signature included in the Authorization header for AWS
* to authenticate the request.
*/
const signRequest = (secretKey, response, stringToSign) => {
// stringToSign is URL-encoded by EvaporateJS, but the signature must use the decoded string.
const stringToSignDecoded = decodeURIComponent(stringToSign);
// Signing is defined later in the article};
class S3Manager {
/**
* Create an instance of S3Manager.
* @constructor
* @param {String} accessKey - Uploader's AWS access key
* @param {String} secretKey - Uploader's AWS secret key
* @param {String} sessionToken - Uploader's AWS session token
*/
constructor(accessKey, secretKey, sessionToken) {
this.manager = new Evaporate({
aws_key: accessKey,
awsRegion: 'my-aws-region-identifier', // TODO: Specify this
awsSignatureVersion: '4', // this is default in EvaporateJS 2.x
bucket: 'my-s3-bucket-name', // TODO: Specify this
// Required for AWS Signature Version 4
cryptoHexEncodedHash256: hashSha256ToHex,
signResponseHandler: curry(signRequest)(secretKey),
});
this.sessionToken = sessionToken;
}
/**
* Upload a file to AWS S3.
* @param {File} file - The File object uploaded to the browser
* @param {Object} transferHeaders - Key-value pairs of headers to add to the AWS request, like
* x-amz-server-side-encryption or x-amz-meta-* headers
* @param {Object} options - EvaporateJS properties to set/override for this upload
*/
upload(file, transferHeaders = {}, options = {}) {
const authHeaders = { 'x-amz-security-token': this.sessionToken };
const params = assign(
{
file,
name: 'encoded/upload/path.extension', // TODO: Specify this
xAmzHeadersAtInitiate: assign({}, authHeaders, transferHeaders),
xAmzHeadersCommon: authHeaders,
},
options,
);
return this.manager.add(params);
}
}
To create an instance of S3Manager
using our logged-in cognitoUser:
const getS3Manager = () => {
return new Promise((resolve, reject) =>
cognitoUser.getSession((sessionErr, tokens) => {
if (sessionErr) {
return reject(sessionErr);
}
// getSession() will populate AWS.config.credentials
AWS.config.credentials.get(credentialsErr => {
if (credentialsErr) {
return reject(credentialsErr);
}
const {
accessKeyId,
secretAccessKey,
sessionToken,
} = AWS.config.credentials;
const s3Manager = new S3Manager(
accessKeyId,
secretAccessKey,
sessionToken,
);
return resolve(s3Manager);
});
}),
);
};
Signing the AWS Request
Using third-party libraries to interact with AWS requires us to cryptographically sign our own requests. The AWS SDK takes care of this but other libraries do not.
Signature Version 4 is the latest method for signing AWS requests. Of the four steps, EvaporateJS handles all except the third.
3. You use your AWS secret access key to derive a signing key, and then use that signing key and the string to sign to create a signature.
The EvaporateJS docs explain how to use AWS Lambda to sign requests. This is comparable to the Custom API proxy described earlier, in that we need to use our AWS Secret Key when calling AWS but we can't reveal it to the user. But with Cognito, that's no longer an issue! How can we use the Cognito user's credentials to sign the EvaporateJS request?
There are many popular client-side cryptography libraries, and I played with the v4 signature in several. Here are examples of the signRequest
function from the code above:
These examples calculate the v4 signature of the stringToSign
variable. To integrate with EvaporateJS, be sure to instead sign the stringToSignDecoded
variable in the signRequest
function.
Still More Cryptography
Finally, EvaporateJS uses other cryptographic operations, some required (like cryptoHexEncodedHash256
) and some just encouraged (like cryptoMd5Method
). Here are implementations of those functions, compared side-by-side for accuracy:
I hope these examples help you dive into exciting libraries like AWS Cognito and EvaporateJS!