AmazonS3 generatePresignedUrl SignatureDoesNotMatch 403

Recently I’m playing with AWS S3 client to generate a URL for client to upload files to our S3 bucket. The code example I referenced can be found here

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.HttpMethod;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;

public class GeneratePresignedUrlAndUploadObject {
	private static String bucketName = "*** bucket name ***";
	private static String objectKey  = "*** The key to your object ***";

	public static void main(String[] args) throws IOException {
		AmazonS3 s3client = new AmazonS3Client(new ProfileCredentialsProvider());

		try {
			System.out.println("Generating pre-signed URL.");
			java.util.Date expiration = new java.util.Date();
			long milliSeconds = expiration.getTime();
			milliSeconds += 1000 * 60 * 60; // Add 1 hour.
			expiration.setTime(milliSeconds);

			GeneratePresignedUrlRequest generatePresignedUrlRequest =
				    new GeneratePresignedUrlRequest(bucketName, objectKey);
			generatePresignedUrlRequest.setMethod(HttpMethod.PUT);
			generatePresignedUrlRequest.setExpiration(expiration);

			URL url = s3client.generatePresignedUrl(generatePresignedUrlRequest); 

			UploadObject(url);

			System.out.println("Pre-Signed URL = " + url.toString());
		} catch (AmazonServiceException exception) {
			System.out.println("Caught an AmazonServiceException, " +
					"which means your request made it " +
					"to Amazon S3, but was rejected with an error response " +
			"for some reason.");
			System.out.println("Error Message: " + exception.getMessage());
			System.out.println("HTTP  Code: "    + exception.getStatusCode());
			System.out.println("AWS Error Code:" + exception.getErrorCode());
			System.out.println("Error Type:    " + exception.getErrorType());
			System.out.println("Request ID:    " + exception.getRequestId());
		} catch (AmazonClientException ace) {
			System.out.println("Caught an AmazonClientException, " +
					"which means the client encountered " +
					"an internal error while trying to communicate" +
					" with S3, " +
			"such as not being able to access the network.");
			System.out.println("Error Message: " + ace.getMessage());
		}
	}

	public static void UploadObject(URL url) throws IOException
	{
		HttpURLConnection connection=(HttpURLConnection) url.openConnection();
		connection.setDoOutput(true);
		connection.setRequestMethod("PUT");
		OutputStreamWriter out = new OutputStreamWriter(
				connection.getOutputStream());
		out.write("This text uploaded as object.");
		out.close();
		int responseCode = connection.getResponseCode();
		System.out.println("Service returned response code " + responseCode);

	}
}

The above code works perfectly, I can get signed URL for file upload and upload file using uploadObject method. However, when I used CURL command to upload the file:

$ curl -X PUT –data-binary @/Users/Harvey/Kodak.jpg “https://mybucket.s3.amazonaws.com/ec047b50585a44eebb9fadf77af65464/5b50fa3924354055bd160f86d47a138a.jpg?AWSAccessKeyId=myAwsAccessKey&Expires=1451899212&Signature=G3Q9zsrZUrP2x2Gly9”

I always get 403 “SignatureDoesNotMatch” error. What might be wrong? Forgot to set AWS S3 region? Expiry time too short? Signature isn’t URL encoded? After ruling out all those possibility, finally I found some clues from reading this stackoverflow question

The issue was the HTTP Content-Type header!!! In the code example above, there’s no ContentType set, however CURL command sends an implicit ContentType HTTP header (you can find it by adding -v to CURL command)

$ curl -v -X PUT –data-binary @/Users/Harvey/Kodak.jpg “https://mybucket.s3.amazonaws.com/ec047b50585a44eebb9fadf77af65464/5b50fa3924354055bd160f86d47a138a.jpg?AWSAccessKeyId=myAwsAccessKey&Expires=1451899212&Signature=G3Q9zsrZUrP2x2Gly9”

The default one sent by CURL is  Content-Type: application/x-www-form-urlencoded

The solution

Once the root cause is found, it’s quite easy to fix it.

1.  Set the correct contentType is the GeneratePresignedUrlRequest before generating the signed URL

generatePresignedUrlRequest.setMethod(HttpMethod.PUT);
generatePresignedUrlRequest.setExpiration(expiration);
generatePresignedUrlRequest.setContentType(contentType); //e.g. image/jpeg
2. Always specify the correct contentType in CURL command (or whatever client code you use)
$ curl --header "Content-Type: image/jpeg" -X PUT --data-binary @/Users/Harvey/Kodak.jpg "https://mybucket.s3.amazonaws.com/ec047b50585a44eebb9fadf77af65464/5b50fa3924354055bd160f86d47a138a.jpg?AWSAccessKeyId=myAwsAccessKey&Expires=1451899212&Signature=G3Q9zsrZUrP2x2Gly9"
(Visited 196 times, 3 visits today)

Leave a Reply