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”).

Without Cognito, user uploads to custom API which proxies the file to S3

With Cognito, user authenticates and uploads directly to S3

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.

To create an instance of S3Manager using our logged-in cognitoUser:

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!

6 thoughts on “Calculate AWS v4 Signature with client-side JavaScript

  1. Chard


    I got confused on this statement

    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:

    What type of cognito user’s credential are going to be used to sign the request? Is that just a jwt token that was issued by cognito, that can be verified then by an AWS Lambda when EvaporateJS tries to upload and contacts the signer_url?

    1. Drew Post author

      Gah, I’m so sorry that I overlooked your comments. I misconfigured my server and I didn’t receive the notifications that they were in the moderation queue.

      I struggled to convey the details of this setup. I think you mostly followed everything I was trying to say, but I’ll restate some points just in case.

      Traditionally a website uses a single AWS account to upload to S3, and these account credentials must be kept on the server to avoid exposing them on the frontend. But once a Cognito user signs in, they have personal AWS credentials like an access key, secret key, and session token. We can use those personal credentials to authenticate directly to S3, without exposing any secret keys in our JavaScript and without needing a server (or Lambda) to protect our secrets.

      In my approach, we set a signResponseHandler in lieu of a signerUrl. Our goal is to sign the request client-side without needing to contact a server. The signRequest() function handles this, and the content of that function varies depending on which client-side cryptography library you’re using (CryptoJS, Crypto Browserify, the AWS SDK, etc.). What cryptography library are you using to sign the request?

      The JWTs created by Cognito are great for many things, but not used here. Instead it’s cognitoUser.getSession() followed by AWS.config.credentials.get() that will yield real AWS credentials.

  2. X

    I may misunderstood your diagram, but did you mean to have Lambda function as the function signer?

    Or you really mean that any client app can just do stuff in s3 with just Cognito credentials?

    1. Drew Post author

      In the first diagram, “Private Uploads to S3 without Cognito”, the Custom API in the middle could also be a Lambda. Basically it represents some extra layer that hides the AWS credentials from the frontend. But yes, this piece is intentionally missing from the “Private Uploads to S3 with Cognito” diagram because Cognito can provide AWS credentials for the client app to use — no app server is required.

      And just to be clear, the username/password entered by the Cognito user are not the same credentials we use to communicate with AWS. A user Bob might log in with the username “[email protected]” and password “[email protected]”, but once he’s signed in he can get normal AWS credentials like an access key of “AKIDEXAMPLE” and a secret key of “wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY”.

  3. gino


    I tried your example but I get signature does not match. I double check the steps, and everything and it looks okay to me but still getting signature does not match. I’m using evaporate 2.0.8

    1. Drew Post author

      Unfortunately all of my code was written for Evaporate 1.x. Looking at the 1.x to 2.0 migration instructions, I think item #5 is most problematic for the code in my examples.

      Off the top of my head, it looks like you would modify the call to new Evaporate() to rename signResponseHandler to customAuthMethod. Then the signature of the signRequest() function would change to:

      const signRequest = (secretKey, signParams, signHeaders, stringToSign, signatureDateTime, canonicalRequest) => {

      The content of the signRequest() function would remain the same, except that after you generate the signature (final in my linked examples) with your cryptography library of choice, you would:

      return Promise.resolve(final);

      to satisfy the expectation that customAuthMethod returns a Promise.

      Are you still working on this problem? I know I took a long time to respond and you’ve probably moved on. If not, and if the suggestion above doesn’t help, then I can try to write an Evaporate 2.x example.

      Also, it may help to look at the content of the “signature does not match” errors from AWS. They show a lot of detail about how AWS is signing the request, which is very helpful when trying to identically sign it in your app.


Leave a Reply

Your email address will not be published.