AWS Signature authorization (Solved)

0
Hi Mendix Team, is there anybody out there how already find a good way to do a rest call with aws signature authorization?
asked
3 answers
6

I you mean AWS Signature V4, then I created this last year. This was tough, and looking back it's not a very pretty solution. Therefore, I would recommend checking out if the Java SDK's for Amazon services can be used.

Having said that, what I did was:

  • In a microflow that calls the REST API,
  • Create a list of headers I want to add,
  • Have a microflow that gathers all input parameters,
  • This microflow passes all information into a Java action,
  • The Java action returns a list of headers (including some calculated headers),
  • The microflow returns the list of headers,
  • I extract all headers from the list,
  • I add the headers to the Call REST activity call.

 

 

// This file was generated by Mendix Modeler.
//
// WARNING: Only the following code will be retained when actions are regenerated:
// - the import list
// - the code between BEGIN USER CODE and END USER CODE
// - the code between BEGIN EXTRA CODE and END EXTRA CODE
// Other code you write will be lost the next time you deploy the project.
// Special characters, e.g., é, ö, à, etc. are supported in comments.

package awsintegration.actions;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.request.GetRequest;
import com.mashape.unirest.request.HttpRequestWithBody;
import com.mashape.unirest.request.HttpRequest;
import com.mendix.core.Core;
import com.mendix.core.CoreException;
import com.mendix.logging.ILogNode;
import com.mendix.systemwideinterfaces.core.IContext;
import com.mendix.webui.CustomJavaAction;
import awsintegration.helpers.AWSSignerV4;
import com.mendix.systemwideinterfaces.core.IMendixObject;
import system.proxies.HttpHeader;

/**
 * This Java action creates a signature and generates a number of headers for use in AWS services, according to the signature v4 algorithm. This Java action has been adapted from the source of the IOT connector module by Mendix. Documentation on signing algorithm: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
 * 
 * Usage:
 * When all inputs have been provided, this action returns a list of httpHeaders. You must provide all custom headers you want to add to the request in the custom Headers list (but see below). All headers must be extracted from the list, and added to the http request: the signature is calculated based on all headers, so if only a subset is sent, the signature will not be valid.
 * 
 * This Java action returns all headers provided in the customHeaders input and adds the following headers (note that these are case sensitive!):
 *  * Authorization
 *  * host
 *  * x-amz-date
 *  * x-amz-content-sha256 (if input is set to true)
 * 
 * These headers should NOT be added to the custom header list.
 */
public class GetAWS4Signature extends CustomJavaAction<java.util.List<IMendixObject>>
{
	private java.lang.String httpMethod;
	private java.lang.String endpoint;
	private java.lang.String body;
	private java.lang.String awsID;
	private java.lang.String awsSecret;
	private java.lang.String region;
	private java.lang.String serviceName;
	private java.lang.Boolean addPayloadHeader;
	private java.util.List<IMendixObject> __customHeaders;
	private java.util.List<system.proxies.HttpHeader> customHeaders;
	private java.lang.Boolean sendUnsignedS3Request;

	public GetAWS4Signature(IContext context, java.lang.String httpMethod, java.lang.String endpoint, java.lang.String body, java.lang.String awsID, java.lang.String awsSecret, java.lang.String region, java.lang.String serviceName, java.lang.Boolean addPayloadHeader, java.util.List<IMendixObject> customHeaders, java.lang.Boolean sendUnsignedS3Request)
	{
		super(context);
		this.httpMethod = httpMethod;
		this.endpoint = endpoint;
		this.body = body;
		this.awsID = awsID;
		this.awsSecret = awsSecret;
		this.region = region;
		this.serviceName = serviceName;
		this.addPayloadHeader = addPayloadHeader;
		this.__customHeaders = customHeaders;
		this.sendUnsignedS3Request = sendUnsignedS3Request;
	}

	@java.lang.Override
	public java.util.List<IMendixObject> executeAction() throws Exception
	{
		this.customHeaders = new java.util.ArrayList<system.proxies.HttpHeader>();
		if (__customHeaders != null)
			for (IMendixObject __customHeadersElement : __customHeaders)
				this.customHeaders.add(system.proxies.HttpHeader.initialize(getContext(), __customHeadersElement));

		// BEGIN USER CODE
		ILogNode logger = Core.getLogger("AWS Signer V4");    
		logger.debug("Build request");
		AWSSignerV4 signer = new AWSSignerV4();
		if (httpMethod.toLowerCase().equals("get") || httpMethod.toLowerCase().equals("head")) {
			GetRequest getRequest = getGetRequest(httpMethod);
			addHeadersToRequest(getRequest, customHeaders);
	        logger.debug("Request created: " + getRequest.toString());
	        getRequest = signer.sign(getRequest, awsID, awsSecret, region, serviceName, addPayloadHeader);
	        logger.debug("Request signed: " + getRequest.toString());
	        return createMendixHeaders(getRequest);
		} else {
	        HttpRequestWithBody requestWithBody = getHttpRequestWithBody(httpMethod);
	        if (body != null) {
		        requestWithBody.body(body);	        	
	        }
	        addHeadersToRequest(requestWithBody, customHeaders);
	        logger.debug("Request created: " + requestWithBody.toString());
	        requestWithBody = signer.sign(requestWithBody, awsID, awsSecret, region, serviceName, addPayloadHeader, sendUnsignedS3Request);
	        logger.debug("Request signed: " + requestWithBody.toString());
	        return createMendixHeaders(requestWithBody);
		} 
		// END USER CODE
	}

	/**
	 * Returns a string representation of this action
	 */
	@java.lang.Override
	public java.lang.String toString()
	{
		return "GetAWS4Signature";
	}

	// BEGIN EXTRA CODE
	private List<IMendixObject> createMendixHeaders(HttpRequest request) throws CoreException {
		IContext context = Core.createSystemContext();
		ArrayList<IMendixObject> result = new ArrayList<IMendixObject>();
		Map<String, List<String>> myheaders = request.getHeaders();
		for (Map.Entry<String,List<String>> entry : myheaders.entrySet()) {
			HttpHeader httpHeader = new HttpHeader(context);
			httpHeader.setKey(entry.getKey());
			httpHeader.setValue(entry.getValue().get(0));
			result.add(httpHeader.getMendixObject());
		}
		return result;
	}
	
	private void addHeadersToRequest(HttpRequest request, List<system.proxies.HttpHeader> customHeaders) throws CoreException {
		for (HttpHeader httpHeader : customHeaders) {
			request.header(httpHeader.getKey(), httpHeader.getValue());
		}
	}
	
	private HttpRequestWithBody getHttpRequestWithBody(String httpMethod) {
		if (httpMethod.toLowerCase().equals("post")) {
			return Unirest.post(endpoint);
		}
		if (httpMethod.toLowerCase().equals("put")) {
			return Unirest.put(endpoint);
		}
		if (httpMethod.toLowerCase().equals("delete")) {
			return Unirest.delete(endpoint);
		}
		if (httpMethod.toLowerCase().equals("patch")) {
			return Unirest.patch(endpoint);
		}
		throw new RuntimeException("Method " + httpMethod +  " currently not supported as HttpRequestWithBody");
	}
	
	private GetRequest getGetRequest(String httpMethod) {
		if (httpMethod.toLowerCase().equals("get")) {
			return Unirest.get(endpoint);
		}
		if (httpMethod.toLowerCase().equals("head")) {
			return Unirest.head(endpoint);
		}
		throw new RuntimeException("Method " + httpMethod +  " currently not supported as GetRequest");
	}
	// END EXTRA CODE
}
package awsintegration.helpers;

import com.amazonaws.util.IOUtils;
import com.mashape.unirest.request.GetRequest;
import com.mashape.unirest.request.HttpRequest;
import com.mashape.unirest.request.HttpRequestWithBody;
import com.mendix.core.Core;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;

import com.mendix.logging.ILogNode;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;

/**
 * Created by ako on 4/20/2016.
 * Edited by Rom van Arendonk on 4/01/2020
 * <p>
 * AWS docs: http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
 * Original code: http://www.javaquery.com/2016/01/aws-version-4-signing-process-complete.html
 * More info: https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/main/java/com/amazonaws/auth/AWS4Signer.java
 */
public class AWSSignerV4 {


    public GetRequest sign(GetRequest request, String awsAccessKeyId, String awsSecretAccessKey, String region, String serviceName, Boolean addPayloadHeader) throws MalformedURLException, UnsupportedEncodingException {
        logger.debug("sign without body");
        URL url = new URL(request.getUrl());
        this.accessKeyID = awsAccessKeyId;
        this.secretAccessKey = awsSecretAccessKey;
        this.regionName = region;
        this.serviceName = serviceName;
        this.httpMethodName = request.getHttpMethod().name();
        this.canonicalURI = url.getPath();
        this.urlQueryString = new URL(request.getUrl()).getQuery();
        this.queryParameters = null;
       
        this.payload = (request.getBody() != null ? request.getBody().toString() : "");
        if (addPayloadHeader) {
        	request.header("x-amz-content-sha256", generateHex(payload));
        }
        
        this.awsHeaders = createAWSHeaders(request);
        this.awsHeaders.put("host", url.getHost());

        xAmzDate = getTimeStamp();
        currentDate = getDate();
        
        //Actual signing happens in this getHeaders() method!
        Map<String, String> headers = getHeaders();
        
        //These headers are generated by this code
        //Existing headers will be overwritten
        request.header("Authorization", headers.get("Authorization"));
        request.header("host", url.getHost());
        request.header("x-amz-date", headers.get("x-amz-date"));
        return request;
    }

    public HttpRequestWithBody sign(HttpRequestWithBody request, String awsAccessKeyId, String awsSecretAccessKey, String region, String serviceName, Boolean addPayloadHeader, Boolean sendUnsignedS3Request) throws IOException {
    	logger.debug("sign with body");
        URL url = new URL(request.getUrl());
        this.accessKeyID = awsAccessKeyId;
        this.secretAccessKey = awsSecretAccessKey;
        this.regionName = region;
        this.serviceName = serviceName;
        this.httpMethodName = request.getHttpMethod().name();
        this.canonicalURI = url.getPath();
        this.queryParameters = null;
        this.urlQueryString = new URL(request.getUrl()).getQuery();
  
        logger.debug("pre payload");
        
        
        if (request.getBody() != null && !request.getBody().toString().equals("")) {
        	logger.debug("There is a body: " + request.getBody().toString());
        	
        	this.payload = IOUtils.toString(request.getBody().getEntity().getContent());	
        } else {
        	this.payload = "";
        }        
        
        if (addPayloadHeader) {
        	logger.debug("adding payload header");
        	if (sendUnsignedS3Request) {
            	request.header("x-amz-content-sha256", "UNSIGNED-PAYLOAD"); //test for unsigned payload in S3 PutObject, should be parameterized
            } else {
            	request.header("x-amz-content-sha256", generateHex(payload));
            }
        }
        logger.debug("post payload");
        logger.debug("Payload: " + this.payload);
        this.awsHeaders = createAWSHeaders(request);
        this.awsHeaders.put("host", url.getHost());
        
        logger.debug("Set awsHeaders");
        
        xAmzDate = getTimeStamp();
        currentDate = getDate();
        logger.debug("I will start signing");

        //Actual signing happens in this getHeaders() method!
        Map<String, String> headers = getHeaders();
        
        //These headers are generated by this code
        //Existing headers will be overwritten
        request.header("Authorization", headers.get("Authorization"));
        request.header("host", url.getHost());
        request.header("x-amz-date", headers.get("x-amz-date"));
        return request;
    }

    private String accessKeyID;
    private String secretAccessKey;
    private String regionName;
    private String serviceName;
    private String httpMethodName;
    private String canonicalURI;
    private TreeMap<String, String> queryParameters;
    private TreeMap<String, String> awsHeaders;
    private String payload;
    private String urlQueryString;

    /* Other variables */
    private final String HMACAlgorithm = "AWS4-HMAC-SHA256";
    private final String aws4Request = "aws4_request";
    private String strSignedHeader;
    private String xAmzDate;
    private String currentDate;

    /**
     * Task 1: Create a Canonical Request for Signature Version 4.
     *
     * @return
     * @throws UnsupportedEncodingException 
     */
    private String prepareCanonicalRequest() throws UnsupportedEncodingException {
        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 (this.queryParameters != null && !queryParameters.isEmpty()) {
            for (Map.Entry<String, String> entrySet : queryParameters.entrySet()) {
                String key = entrySet.getKey();
                String value = entrySet.getValue();
                queryString.append(key).append("=").append(URLEncoder.encode(value, "UTF-8")).append("&");
            }
            queryString.append("\n");
        } else if (this.urlQueryString != null && !this.urlQueryString.isEmpty()) {
            try {
                String u = "http://aws.com" + canonicalURI + "?" + this.urlQueryString;
                List<NameValuePair> urlPars = URLEncodedUtils.parse(new URI(u), "UTF8");
                Map<String, String> pars = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
                for (NameValuePair par : urlPars) {
                    pars.put(par.getName(), par.getValue());
                }
                for (Map.Entry<String, String> entrySet : pars.entrySet()) {
                    String key = entrySet.getKey();
                    String value = entrySet.getValue();
                    queryString.append(key).append("=").append(URLEncoder.encode(value, "UTF-8")).append("&");
                }
                if(queryString.length() > 0){
                    queryString = queryString.deleteCharAt(queryString.length()-1);
                }
                queryString.append("\n");
            } catch (URISyntaxException e) {
                e.printStackTrace();
            }
        } 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));
      	logger.debug("##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);

       	logger.debug("##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
     * @throws UnsupportedEncodingException 
     */
    public Map<String, String> getHeaders() throws UnsupportedEncodingException {
        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));

           	logger.debug("##Signature:\n" + signature);
           	logger.debug("##Header:");
            for (Map.Entry<String, String> entrySet : header.entrySet()) {
               	logger.debug(entrySet.getKey() + " = " + entrySet.getValue());
            }
            logger.debug("================================");
            return header;
        } else {
        	logger.debug("##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 (NoSuchAlgorithmException | UnsupportedEncodingException 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());
    }
    
    private TreeMap<String, String> createAWSHeaders(HttpRequest request) {
    	logger.debug("I will add the headers");
    	TreeMap<String, String> result = new TreeMap<String, String>(Comparator.naturalOrder());
    	Map<String, List<String>> headers = request.getHeaders();
    	for (Map.Entry<String,List<String>> entry : headers.entrySet()) {
    		result.put(entry.getKey(), entry.getValue().get(0));
    	}
    	logger.debug("I have added the headers");
    	return result;
    }

    private ILogNode logger = Core.getLogger("AWS Signer V4");
}

 

answered
0

In my experience, it’s not easy to generate the signature that AWS requires. It was easier for me to import their Java SDK and make calls via Java actions.

answered
0

Hi Rom van Arendonk thank you very much for this detailed informaton.

The signature process works really good for my aws polly service!

 

answered