AWS authentication in X++ for Vendor Central & others

  • October 22, 2024

The authentication step to AWS is rather complicated. A client had asked me to code the authentication step and they would handle the rest of the integration. What I found is the process can be complicated with a lot of nuances.

While you could code this in C# and add the dll reference, I prefer to code directly in X++ as it reduces complexity for future changes. I could not find anywhere else on the internet with samples in X++, so I figure I would post it here.

There are options to download wrappers, code packages, etc that already do this integration. I tried a few of those and ran into issues with dependencies and many had bugs and wouldn’t authenticate. Rather than try to find a good fit and fix code, I found it easier to just write my own, especially since it was limited to a few endpoints for this client.

I didn’t get the chance to fully clean up the code. Once we had a working proof of concept, the client’s developer was able to take it over. This code is shared with their permission. It isn’t perfect, but may save you time getting started.

A few items to consider cleaning up and notes:

Postman is very useful for confirming your credentials are working before you start tinkering with your code – it has an option for AWS.

Make parameters for values such as vendor central role ARN, access Key, secret Key, AWS region, etc

Standard URL encoding does NOT work, you must use Amazon’s flavor of this, which includes the hex characters being capital case. Special callout for case sensitivity here as it usually doesn’t matter in X++. I didn’t code a method to perform this encoding, you will need that

The parameters in canonicalQueryString and canonicalHeaders need to be in alphabetical order (I hardcoded for this specific example) – and you should parameterize this better instead of hardcoding.

using System.Net.Http;

public class AmznIntegrationHelper
{
    public static void main(Args _args)
    {
        AmznIntegrationHelper AmznIntegrationHelper = new AmznIntegrationHelper();
        str accessToken = AmznIntegrationHelper.getLWAAccessToken();

        str accessKey, secretAccessKey, sessionToken;

        [accessKey, secretAccessKey, sessionToken] = AmznIntegrationHelper.getAWSSTSRequest();

        str xml = AmznIntegrationHelper.getPurchaseOrders(accessToken, accessKey, secretAccessKey, sessionToken);
    }

    public str getLWAAccessToken()
    {
        str endpoint = 'https://api.amazon.com/auth/o2/token';
        str refreshToken = 'Atzr|IwEBIDq9<redacted>';
        str clientId = 'amzn1.application-oa2-client.<redacted>';
        str clientSecret = 'amzn1.oa2-cs.v1.<redacted>';

        var httpClient = new HttpClient();
        var request = System.Net.WebRequest::Create(endpoint);
        request.set_Method('POST');
        request.set_ContentType('application/x-www-form-urlencoded');

        System.IO.Stream requestStream = request.GetRequestStream();
        System.IO.StreamWriter writer = new System.IO.StreamWriter(requestStream);
        writer.Write(strFmt('grant_type=refresh_token&refresh_token=%1&client_id=%2&client_secret=%3', refreshToken, clientId, clientSecret));
        writer.Flush();
        writer.Close();
        writer.Dispose();

        System.Net.WebResponse response = request.GetResponse();
        System.IO.Stream dataStream = response.GetResponseStream();
        System.IO.StreamReader streamRead  = new System.IO.StreamReader(dataStream);
        str jsonString = streamRead.ReadToEnd();
        dataStream.Close();
        response.Close();

        XmlDocument xmlDoc;//this was a custom tool = JsonHelper::jsonToXmlDoc(jsonString);

        XmlNode root = xmlDoc.firstChild();
        return root.selectSingleNode('access_token').innerText();
    }

    public container getAWSSTSRequest()
    {
        str vendCentralRoleARN = 'arn:aws:iam::<redacted>:role/VendorCentralAPI';
        str baseEndpoint = strFmt('https://sts.amazonaws.com/');
        str uriQueryStr = strFmt('Action=AssumeRole&DurationSeconds=3600&RoleArn=%1&RoleSessionName=Test&Version=2011-06-15', vendCentralRoleARN);
        str endPoint = baseEndpoint + '?' + uriQueryStr;
        
        str accessKey = '<redacted>';
        str secretKey = '<redacted>';
        str AWSRegion = 'us-east-1';
        str serviceName = 'sts';

        utcdatetime now = DateTimeUtil::utcNow();

        str amzDate = int2Str(DateTimeUtil::year(now)) + strRFix(int2Str(DateTimeUtil::month(now)), 2, '0') + strRFix(int2Str(DateTimeUtil::day(now)), 2, '0');//  '20230407';
        str amzDateTime = amzDate + 'T' + strRFix(int2Str(DateTimeUtil::hour(now)), 2, '0') + strRFix(int2Str(DateTimeUtil::minute(now)), 2, '0') + strRFix(int2Str(DateTimeUtil::second(now)), 2, '0') + 'Z';// '20230407T220157Z';
        str terminator = 'aws4_request';

        //better details here https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
        //Note URL encoding must have the hex values capital

        //https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
        //Step 1
        str hashedPayload = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';//empty str hashed
        str serviceUri = 'sts.amazonaws.com';

        str httpMethod = 'GET';
        str canonicalUri = '/';
        str canonicalQueryString = 'Action=AssumeRole&DurationSeconds=3600&RoleArn=arn%3Aaws%3Aiam%3A%3A<redacted>%3Arole%2FVendorCentralAPI&RoleSessionName=Test&Version=2011-06-15';
        str canonicalHeaders = strFmt('host:%1\nx-amz-content-sha256:%2\nx-amz-date:%3\n', serviceUri, hashedPayload, amzDateTime);
        str signedHeaders = 'host;x-amz-content-sha256;x-amz-date';

        str canonicalRequest = strFmt('%1\n%2\n%3\n%4\n%5\n%6', httpMethod, canonicalUri, canonicalQueryString, canonicalHeaders, signedHeaders, hashedPayload);
      
        //Step 2
        System.Byte[] canonicalRequestBytes = System.Text.Encoding::get_UTF8().GetBytes(canonicalRequest);

        var sha256 = System.Security.Cryptography.SHA256::Create();
        var canonicalRequestHashedBytes = sha256.ComputeHash(canonicalRequestBytes);
        str canonicalRequestHexEncoded = System.BitConverter::ToString(canonicalRequestHashedBytes);
        canonicalRequestHexEncoded = strRem(canonicalRequestHexEncoded, '-');
        str canonicalRequestHashed = strLwr(canonicalRequestHexEncoded);

        //Step 3
        str algo = 'AWS4-HMAC-SHA256';
        str requestDateTime = amzDateTime;
        str credentialScope = strFmt('%1/%2/%3/%4', amzDate, AWSRegion, serviceName, terminator);

        str stringToSign = strFmt('%1\n%2\n%3\n%4', algo, requestDateTime, credentialScope, canonicalRequestHashed);
      
        //Step 4
        var keyBytes = System.Text.Encoding::get_UTF8().GetBytes('AWS4' + secretKey);

        var kDate = this.getHMACHash(keyBytes, amzDate);
        var kRegion = this.getHMACHash(kDate, AWSRegion);
        var kService = this.getHMACHash(kRegion, serviceName);
        var kSigning = this.getHMACHash(kService, terminator);
        var signatureBytes = this.getHMACHash(kSigning, stringToSign);

        str hashedHexEncoded = System.BitConverter::ToString(signatureBytes);
        hashedHexEncoded = strRem(hashedHexEncoded, '-');
        str signature = strLwr(hashedHexEncoded);

        //Step 5
        str authStr = strFmt('AWS4-HMAC-SHA256 Credential=%1/%2/%3/%4/%5, SignedHeaders=%6, Signature=%7', accessKey, amzDate, AWSRegion, serviceName, terminator, signedHeaders, signature);

        var headers = new System.Net.WebHeaderCollection();
        headers.Add('X-Amz-Date', amzDateTime);
        headers.Add('x-amz-content-sha256', hashedPayload);
        headers.Add('Authorization', authStr);

        //Make the http call
        var httpClient = new HttpClient();
        var request = System.Net.WebRequest::Create(endpoint);
        request.set_Method(httpMethod);
        request.set_Headers(headers);

        System.Net.WebResponse response;
        System.Net.WebException webEx;
        try
        {
            response = request.GetResponse();
        }
        catch (webEx)
        {
            System.IO.Stream dataStream = webEx.get_Response().GetResponseStream();
            System.IO.StreamReader streamRead  = new System.IO.StreamReader(dataStream);
            error(streamRead.ReadToEnd());
        }

        str xmlString;
        if(response)
        {
            System.IO.Stream dataStream = response.GetResponseStream();
            System.IO.StreamReader streamRead  = new System.IO.StreamReader(dataStream);
            xmlString = streamRead.ReadToEnd();
            dataStream.Close();
            response.Close();
        }

        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.loadXml(xmlString);

        XmlNode root = xmlDoc.documentElement();
        XmlNode AssumeRoleResult = root.firstChild();
        XmlNodeList AssumedRoleUser = AssumeRoleResult.childNodes();
        AssumedRoleUser.nextNode();//AssumedRoleUser
        XmlNode Credentials = AssumedRoleUser.nextNode();
        XmlNodeList credentialDetails = Credentials.childNodes();
        XmlNode accessKeyId = credentialDetails.nextNode();
        XmlNode secretAccessKey = credentialDetails.nextNode();
        XmlNode sessionTokenId = credentialDetails.nextNode();

        return [accessKeyId.innerText(), secretAccessKey.innerText(), sessionTokenId.innerText()];
    }

    public str getPurchaseOrders(str _accessToken, str _accessKey, str _secretKey, str _sessionToken)
    {
        str baseEndpoint = strFmt('https://sellingpartnerapi-na.amazon.com/');
       // str uriQueryStr = strFmt('Action=AssumeRole&DurationSeconds=3600&RoleArn=%1&RoleSessionName=Test&Version=2011-06-15', vendCentralRoleARN);
        str path = 'vendor/orders/v1/purchaseOrders';
        str endPoint = baseEndpoint + path;// + '?' + uriQueryStr;
        
        str accessKey = _accessKey;
        str secretKey = _secretKey;
        str AWSRegion = 'us-east-1';
        str serviceName = 'execute-api';

        utcdatetime now = DateTimeUtil::utcNow();

        str amzDate = int2Str(DateTimeUtil::year(now)) + strRFix(int2Str(DateTimeUtil::month(now)), 2, '0') + strRFix(int2Str(DateTimeUtil::day(now)), 2, '0');//  '20230407';
        str amzDateTime = amzDate + 'T' + strRFix(int2Str(DateTimeUtil::hour(now)), 2, '0') + strRFix(int2Str(DateTimeUtil::minute(now)), 2, '0') + strRFix(int2Str(DateTimeUtil::second(now)), 2, '0') + 'Z';// '20230407T220157Z';
        str terminator = 'aws4_request';

        //better details here https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
        //Note URL encoding must have the hex values capital

        //https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
        //Step 1
        str hashedPayload = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';//empty str hashed
        str serviceUri = 'sellingpartnerapi-na.amazon.com';

        str httpMethod = 'GET';
        str canonicalUri = '/' + path;
        str canonicalQueryString = '';
        str canonicalHeaders = strFmt('host:%1\nx-amz-access-token:%2\nx-amz-content-sha256:%3\nx-amz-date:%4\nx-amz-security-token:%5\n', serviceUri, _accessToken, hashedPayload, amzDateTime, _sessionToken);
        str signedHeaders = 'host;x-amz-access-token;x-amz-content-sha256;x-amz-date;x-amz-security-token';

        str canonicalRequest = strFmt('%1\n%2\n%3\n%4\n%5\n%6', httpMethod, canonicalUri, canonicalQueryString, canonicalHeaders, signedHeaders, hashedPayload);
      
        //Step 2
        System.Byte[] canonicalRequestBytes = System.Text.Encoding::get_UTF8().GetBytes(canonicalRequest);

        var sha256 = System.Security.Cryptography.SHA256::Create();
        var canonicalRequestHashedBytes = sha256.ComputeHash(canonicalRequestBytes);
        str canonicalRequestHexEncoded = System.BitConverter::ToString(canonicalRequestHashedBytes);
        canonicalRequestHexEncoded = strRem(canonicalRequestHexEncoded, '-');
        str canonicalRequestHashed = strLwr(canonicalRequestHexEncoded);

        //Step 3
        str algo = 'AWS4-HMAC-SHA256';
        str requestDateTime = amzDateTime;
        str credentialScope = strFmt('%1/%2/%3/%4', amzDate, AWSRegion, serviceName, terminator);

        str stringToSign = strFmt('%1\n%2\n%3\n%4', algo, requestDateTime, credentialScope, canonicalRequestHashed);
      
        //Step 4
        var keyBytes = System.Text.Encoding::get_UTF8().GetBytes('AWS4' + secretKey);

        var kDate = this.getHMACHash(keyBytes, amzDate);
        var kRegion = this.getHMACHash(kDate, AWSRegion);
        var kService = this.getHMACHash(kRegion, serviceName);
        var kSigning = this.getHMACHash(kService, terminator);
        var signatureBytes = this.getHMACHash(kSigning, stringToSign);

        str hashedHexEncoded = System.BitConverter::ToString(signatureBytes);
        hashedHexEncoded = strRem(hashedHexEncoded, '-');
        str signature = strLwr(hashedHexEncoded);

        //Step 5
        str authStr = strFmt('AWS4-HMAC-SHA256 Credential=%1/%2/%3/%4/%5, SignedHeaders=%6, Signature=%7', accessKey, amzDate, AWSRegion, serviceName, terminator, signedHeaders, signature);

        var headers = new System.Net.WebHeaderCollection();
        headers.Add('X-Amz-Date', amzDateTime);
        headers.Add('x-amz-content-sha256', hashedPayload);
        headers.Add('x-amz-access-token', _accessToken);
        headers.Add('x-amz-security-token', _sessionToken);
        headers.Add('Authorization', authStr);

        //Make the http call
        var httpClient = new HttpClient();
        var request = System.Net.WebRequest::Create(endpoint);
        request.set_Method(httpMethod);
        request.set_Headers(headers);

        System.Net.WebResponse response;
        System.Net.WebException webEx;
        try
        {
            response = request.GetResponse();
        }
        catch (webEx)
        {
            System.IO.Stream dataStream = webEx.get_Response().GetResponseStream();
            System.IO.StreamReader streamRead  = new System.IO.StreamReader(dataStream);
            error(streamRead.ReadToEnd());
        }

        str xmlString;
        if(response)
        {
            System.IO.Stream dataStream = response.GetResponseStream();
            System.IO.StreamReader streamRead  = new System.IO.StreamReader(dataStream);
            xmlString = streamRead.ReadToEnd();
            dataStream.Close();
            response.Close();
        }

        return xmlString;
    }

    public System.Byte[] getHMACHash(System.Byte[] key, str data)
    {
        var HMACSHA256 = new System.Security.Cryptography.HMACSHA256(key);

        System.Byte[] dataBytes = System.Text.Encoding::get_UTF8().GetBytes(data);

        return HMACSHA256.ComputeHash(dataBytes);
    }

} 

Questions?

Reach out with any feedback

Always happy to answer questions