Skip to main content

AWS Signature with RestAssured

 

AWS Signature authentication support is not something that is available in RestAssured as out-of-box feature. At least I wasn't able to find any good documentation for the same. So, how we can implement AWS Signature Headers with Rest Assured?

Signing AWS requests with Signature Version 4...

This explains step by step how to create a signature and add it to an HTTP request to AWS: https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html


Below given source code (AWSV4Auth.java) helps to calculate signature based on given data and return the headers. Once you get headers suing aWSV4Auth.getHeaders(), you just need to add these headers in your Rest API call (Sample for the same is given below: RATest.java)

AWSV4Auth.java:

public class AWSV4Auth {


    private AWSV4Auth() {

    }


    public static class Builder {


        private String accessKeyID;

        private String secretAccessKey;

        private String regionName;

        private String serviceName;

        private String httpMethodName;

        private String canonicalURI;

        private TreeMap<String, String> queryParametes;

        private TreeMap<String, String> awsHeaders;

        private String payload;

        private boolean debug = false;


        public Builder(String accessKeyID, String secretAccessKey) {

            this.accessKeyID = accessKeyID;

            this.secretAccessKey = secretAccessKey;

        }


        public Builder regionName(String regionName) {

            this.regionName = regionName;

            return this;

        }


        public Builder serviceName(String serviceName) {

            this.serviceName = serviceName;

            return this;

        }


        public Builder httpMethodName(String httpMethodName) {

            this.httpMethodName = httpMethodName;

            return this;

        }


        public Builder canonicalURI(String canonicalURI) {

            this.canonicalURI = canonicalURI;

            return this;

        }


        public Builder queryParametes(TreeMap<String, String> queryParametes) {

            this.queryParametes = queryParametes;

            return this;

        }


        public Builder awsHeaders(TreeMap<String, String> awsHeaders) {

            this.awsHeaders = awsHeaders;

            return this;

        }


        public Builder payload(String payload) {

            this.payload = payload;

            return this;

        }


        public Builder debug() {

            this.debug = true;

            return this;

        }


        public AWSV4Auth build() {

            return new AWSV4Auth(this);

        }

    }


    private String accessKeyID;

    private String secretAccessKey;

    private String regionName;

    private String serviceName;

    private String httpMethodName;

    private String canonicalURI;

    private TreeMap<String, String> queryParametes;

    private TreeMap<String, String> awsHeaders;

    private String payload;

    private boolean debug = false;


    /* Other variables */

    private final String HMACAlgorithm = "AWS4-HMAC-SHA256";

    private final String aws4Request = "aws4_request";

    private String strSignedHeader;

    private String xAmzDate;

    private String currentDate;


    private AWSV4Auth(Builder builder) {

        accessKeyID = builder.accessKeyID;

        secretAccessKey = builder.secretAccessKey;

        regionName = builder.regionName;

        serviceName = builder.serviceName;

        httpMethodName = builder.httpMethodName;

        canonicalURI = builder.canonicalURI;

        queryParametes = builder.queryParametes;

        awsHeaders = builder.awsHeaders;

        payload = builder.payload;

        debug = builder.debug;


        /* Get current timestamp value.(UTC) */

        xAmzDate = getTimeStamp();

        currentDate = getDate();

    }


    /**

     * Task 1: Create a Canonical Request for Signature Version 4.

     *

     * @return

     */

    private String prepareCanonicalRequest() {

        StringBuilder canonicalURL = new StringBuilder("");


        /* Step 1.1 Start with the HTTP request method (GET, PUT, POST, etc.), followed by a newline character. */

        canonicalURL.append(httpMethodName).append("\n");


        /* Step 1.2 Add the canonical URI parameter, followed by a newline character. */

        canonicalURI = canonicalURI == null || canonicalURI.trim().isEmpty() ? "/" : canonicalURI;

        canonicalURL.append(canonicalURI).append("\n");


        /* Step 1.3 Add the canonical query string, followed by a newline character. */

        StringBuilder queryString = new StringBuilder("");

        if (queryParametes != null && !queryParametes.isEmpty()) {

            for (Map.Entry<String, String> entrySet : queryParametes.entrySet()) {

                String key = entrySet.getKey();

                String value = entrySet.getValue();

                queryString.append(key).append("=").append(encodeParameter(value)).append("&");

            }


            queryString.deleteCharAt(queryString.lastIndexOf("&"));


            queryString.append("\n");

        } else {

            queryString.append("\n");

        }

        canonicalURL.append(queryString);


        /* Step 1.4 Add the canonical headers, followed by a newline character. */

        StringBuilder signedHeaders = new StringBuilder("");

        if (awsHeaders != null && !awsHeaders.isEmpty()) {

            for (Map.Entry<String, String> entrySet : awsHeaders.entrySet()) {

                String key = entrySet.getKey();

                String value = entrySet.getValue();

                signedHeaders.append(key).append(";");

                canonicalURL.append(key).append(":").append(value).append("\n");

            }


            /* Note: Each individual header is followed by a newline character, meaning the complete list ends with a newline character. */

            canonicalURL.append("\n");

        } else {

            canonicalURL.append("\n");

        }


        /* Step 1.5 Add the signed headers, followed by a newline character. */

        strSignedHeader = signedHeaders.substring(0, signedHeaders.length() - 1); // Remove last ";"

        canonicalURL.append(strSignedHeader).append("\n");


        /* Step 1.6 Use a hash (digest) function like SHA256 to create a hashed value from the payload in the body of the HTTP or HTTPS. */

        payload = payload == null ? "" : payload;

        canonicalURL.append(generateHex(payload));


        if (debug) {

            System.out.println("##Canonical Request:\n" + canonicalURL.toString());

        }


        return canonicalURL.toString();

    }


    /**

     * Task 2: Create a String to Sign for Signature Version 4.

     *

     * @param canonicalURL

     * @return

     */

    private String prepareStringToSign(String canonicalURL) {

        String stringToSign = "";


        /* Step 2.1 Start with the algorithm designation, followed by a newline character. */

        stringToSign = HMACAlgorithm + "\n";


        /* Step 2.2 Append the request date value, followed by a newline character. */

        stringToSign += xAmzDate + "\n";


        /* Step 2.3 Append the credential scope value, followed by a newline character. */

        stringToSign += currentDate + "/" + regionName + "/" + serviceName + "/" + aws4Request + "\n";


        /* Step 2.4 Append the hash of the canonical request that you created in Task 1: Create a Canonical Request for Signature Version 4. */

        stringToSign += generateHex(canonicalURL);


        if (debug) {

            System.out.println("##String to sign:\n" + stringToSign);

        }


        return stringToSign;

    }


    /**

     * Task 3: Calculate the AWS Signature Version 4.

     *

     * @param stringToSign

     * @return

     */

    private String calculateSignature(String stringToSign) {

        try {

            /* Step 3.1 Derive your signing key */

            byte[] signatureKey = getSignatureKey(secretAccessKey, currentDate, regionName, serviceName);


            /* Step 3.2 Calculate the signature. */

            byte[] signature = HmacSHA256(signatureKey, stringToSign);


            /* Step 3.2.1 Encode signature (byte[]) to Hex */

            String strHexSignature = bytesToHex(signature);

            return strHexSignature;

        } catch (Exception ex) {

            ex.printStackTrace();

        }

        return null;

    }


    /**

     * Task 4: Add the Signing Information to the Request. We'll return Map of

     * all headers put this headers in your request.

     *

     * @return

     */

    public Map<String, String> getHeaders() {

        awsHeaders.put("x-amz-date", xAmzDate);


        /* Execute Task 1: Create a Canonical Request for Signature Version 4. */

        String canonicalURL = prepareCanonicalRequest();


        /* Execute Task 2: Create a String to Sign for Signature Version 4. */

        String stringToSign = prepareStringToSign(canonicalURL);


        /* Execute Task 3: Calculate the AWS Signature Version 4. */

        String signature = calculateSignature(stringToSign);


        if (signature != null) {

            Map<String, String> header = new HashMap<String, String>(0);

            header.put("x-amz-date", xAmzDate);

            header.put("Authorization", buildAuthorizationString(signature));


            if (debug) {

                System.out.println("##Signature:\n" + signature);

                System.out.println("##Header:");

                for (Map.Entry<String, String> entrySet : header.entrySet()) {

                    System.out.println(entrySet.getKey() + " = " + entrySet.getValue());

                }

                System.out.println("================================");

            }

            return header;

        } else {

            if (debug) {

                System.out.println("##Signature:\n" + signature);

            }

            return null;

        }

    }


    /**

     * Build string for Authorization header.

     *

     * @param strSignature

     * @return

     */

    private String buildAuthorizationString(String strSignature) {

        return HMACAlgorithm + " "

                + "Credential=" + accessKeyID + "/" + getDate() + "/" + regionName + "/" + serviceName + "/" + aws4Request + ","

                + "SignedHeaders=" + strSignedHeader + ","

                + "Signature=" + strSignature;

    }


    /**

     * Generate Hex code of String.

     *

     * @param data

     * @return

     */

    private String generateHex(String data) {

        MessageDigest messageDigest;

        try {

            messageDigest = MessageDigest.getInstance("SHA-256");

            messageDigest.update(data.getBytes("UTF-8"));

            byte[] digest = messageDigest.digest();

            return String.format("%064x", new java.math.BigInteger(1, digest));

        } catch (Exception e) {

            e.printStackTrace();

        }

        return null;

    }


    /**

     * Apply HmacSHA256 on data using given key.

     *

     * @param data

     * @param key

     * @return

     * @throws Exception

     * @reference:

     * http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-java

     */

    private byte[] HmacSHA256(byte[] key, String data) throws Exception {

        String algorithm = "HmacSHA256";

        Mac mac = Mac.getInstance(algorithm);

        mac.init(new SecretKeySpec(key, algorithm));

        return mac.doFinal(data.getBytes("UTF8"));

    }


    /**

     * Generate AWS signature key.

     *

     * @param key

     * @param date

     * @param regionName

     * @param serviceName

     * @return

     * @throws Exception

     * @reference

     * http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-java

     */

    private byte[] getSignatureKey(String key, String date, String regionName, String serviceName) throws Exception {

        byte[] kSecret = ("AWS4" + key).getBytes("UTF8");

        byte[] kDate = HmacSHA256(kSecret, date);

        byte[] kRegion = HmacSHA256(kDate, regionName);

        byte[] kService = HmacSHA256(kRegion, serviceName);

        byte[] kSigning = HmacSHA256(kService, aws4Request);

        return kSigning;

    }


    final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();


    /**

     * Convert byte array to Hex

     *

     * @param bytes

     * @return

     */

    private String bytesToHex(byte[] bytes) {

        char[] hexChars = new char[bytes.length * 2];

        for (int j = 0; j < bytes.length; j++) {

            int v = bytes[j] & 0xFF;

            hexChars[j * 2] = hexArray[v >>> 4];

            hexChars[j * 2 + 1] = hexArray[v & 0x0F];

        }

        return new String(hexChars).toLowerCase();

    }


    /**

     * Get timestamp. yyyyMMdd'T'HHmmss'Z'

     *

     * @return

     */

    private String getTimeStamp() {

        DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");

        dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//server timezone

        return dateFormat.format(new Date());

    }


    /**

     * Get date. yyyyMMdd

     *

     * @return

     */

    private String getDate() {

        DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");

        dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//server timezone

        return dateFormat.format(new Date());

    }


    /**

     * Using {@link URLEncoder#encode(java.lang.String, java.lang.String) } instead of

     * {@link URLEncoder#encode(java.lang.String) }

   

     * @param param

     * @return 

     */

     private String encodeParameter(String param){

         try {

             return URLEncoder.encode(param, "UTF-8");

         } catch (Exception e) {

             return URLEncoder.encode(param);

         }

     }

}


RATest.java


public class RATest {


@Test

private void test1() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {

Response response = (Response) postWithBodyAndHeader();

System.out.println(response.getBody().asString());

System.out.println(response.statusCode());


}


public static ResponseOptions<Response> postWithBodyAndHeader()

throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {


String url = "https://abd.com"; //add your URL here

String payload = "{\"lat_id\":\"1187605\",\"long_id\":\"dx6266588841\",\"name\":\"ClientXYZ\"}"; \\add your body part here or read from external file and pass here


TreeMap<String, String> awsHeaders = new TreeMap<String, String>();

awsHeaders.put("host", "devapi.host.com"); //add your host entry here


AWSV4Auth aWSV4Auth = new AWSV4Auth.Builder("accessKeyID", "secretAccessKey") // add your accessKeyID and secretAccessKey here

.regionName("us-east-1").serviceName("exe-api") // es -

// elastic

// search.

// use your

// service

// name

.httpMethodName("POST") // GET, PUT, POST, DELETE, etc...

.canonicalURI("/dev/place") // end point

.queryParametes(null) // query parameters if any

.awsHeaders(awsHeaders) // aws header parameters

.payload(payload) // payload if any

.debug() // turn on the debug mode

.build();


try {


RequestSpecification Request = RestAssured.given();


Map<String, String> header = aWSV4Auth.getHeaders();

for (Map.Entry<String, String> entrySet : header.entrySet()) {

String key = entrySet.getKey();

String value = entrySet.getValue();


Request.headers(key, value);

Request.header("Accept", "application/json");

Request.header("x-api-key", "apikey"); /add any extra headers here (if you need)


}


Request.body(payload);

Request.headers(header);

Request.relaxedHTTPSValidation();

return Request.post(new URI(url));

} catch (URISyntaxException e) {

e.printStackTrace();

}


finally {


}

return null;

}

}



Comments

  1. is this code available in GitHub?

    ReplyDelete
  2. It wasn't but I just pushed, see if this helps https://github.com/dgambhir01/AWS

    ReplyDelete

Post a Comment

Popular posts from this blog

How to Unzip files in Selenium (Java)?

1) Using Java (Lengthy way) : Create a utility and use it:>> import java.io.BufferedOutputStream; import org.openqa.selenium.io.Zip; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream;   public class UnzipUtil {     private static final int BUFFER_SIZE = 4096;     public void unzip (String zipFilePath, String destDirectory) throws IOException {         File destDir = new File(destDirectory);         if (!destDir.exists()) {             destDir.mkdir();         }         ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));         ZipEntry entry = zipIn.getNextEntry();         // to iterates over entries in the zip folder         while (en...

The use of Verbose attribute in testNG or POM.xml (maven-surefire-plugin)

At times, we see some weird behavior in your testNG execution and feel that the information displayed is insufficient and would like to see more details. At other times, the output on the console is too verbose and we may want to only see the errors. This is where a verbose attribute can help you- it is used to define the amount of logging to be performed on the console. The verbosity level is 0 to 10, where 10 is most detailed. Once you set it to 10, you'll see that console output will contain information regarding the tests, methods, and listeners, etc. <suite name="Suite" thread-count="5" verbose="10"> Note* You can specify -1 and this will put TestNG in debug mode. The default level is 0. Alternatively, you can set the verbose level through attribute in "maven-surefire-plugin" in pom.xml, as shown in the image. #testNG #automationTesting #verbose # #testAutomation

Encode/Decode the variable/response using Postman itself

We get a lot of use cases where we may have to implement Base64 encoding and/or decoding while building our APIs. And, if you are wondering if it is possible to encode/decode the variable/response using Postman itself or how to encode/decode the token or password in postman and save it in a variable? To Base64 encode/decode, the quickest way is to use JavaScript methods btoa, atob: atob - It turns base64-encoded ASCII data back to binary. btoa - It turns binary data to base64-encoded ASCII. Sample code : var responseBody = pm.response.json(); var parsedPwd = JSON.parse(atob(responseBody.password)); // presuming password is in the payload pm.collectionVariables.set("password", parsedPwd);