Search code examples
node.jsazureazure-functionsnode-crypto

Encoding/Encrypting the Azure Log Analytics Authorization Header in node.js


I've been trying to get the Log Collector API working in a node.js Azure Function but am stuck on the 403/Forbidden error which indicates I'm not forming the authorization header correctly. The complete code is in a github repository here:

https://github.com/sportsmgmt-labs/Azure-Log-Analytics-Node-Function

The Data Collector API documentation is here:

https://learn.microsoft.com/en-us/azure/log-analytics/log-analytics-data-collector-api

The authorization header should be formatted as follows:

Authorization: SharedKey {WorkspaceID}:{Signature}

Where the signature is encoded/encrypted like this:

Base64(HMAC-SHA256(UTF8(StringToSign)))

Here is my code that is creating the authorization header:

var contentLength = Buffer.byteLength(req.body['log-entry'], 'utf8');

var authorization = 'POST\n' + contentLength + '\napplication/json\nx-ms-date:' + processingDate + '\n/api/logs';

// encode string using Base64(HMAC-SHA256(UTF8(StringToSign)))
authorization = crypto.createHmac('sha256', sharedKey).update(authorization.toString('utf8')).digest('base64');

authorization = 'Authorization: SharedKey ' + workspaceId + ':' + authorization;

The response from the server is:

{"Error":"InvalidAuthorization","Message":"An invalid scheme was specified in the Authorization header"}

Could someone please help me understand what I'm doing wrong? Thanks!

Edit: Here is the Python code to do this:

def build_signature(customer_id, shared_key, date, content_length, method, content_type, resource):
    x_headers = 'x-ms-date:' + date
    string_to_hash = method + "\n" + str(content_length) + "\n" + content_type + "\n" + x_headers + "\n" + resource
    bytes_to_hash = bytes(string_to_hash).encode('utf-8')  
    decoded_key = base64.b64decode(shared_key)
    encoded_hash = base64.b64encode(hmac.new(decoded_key, bytes_to_hash, digestmod=hashlib.sha256).digest())
    authorization = "SharedKey {}:{}".format(customer_id,encoded_hash)
    return authorization

...and the C# code:

    static void Main()
    {
        // Create a hash for the API signature
        var datestring = DateTime.UtcNow.ToString("r");
        string stringToHash = "POST\n" + json.Length + "\napplication/json\n" + "x-ms-date:" + datestring + "\n/api/logs";
        string hashedString = BuildSignature(stringToHash, sharedKey);
        string signature = "SharedKey " + customerId + ":" + hashedString;

        PostData(signature, datestring, json);
    }

    // Build the API signature
    public static string BuildSignature(string message, string secret)
    {
        var encoding = new System.Text.ASCIIEncoding();
        byte[] keyByte = Convert.FromBase64String(secret);
        byte[] messageBytes = encoding.GetBytes(message);
        using (var hmacsha256 = new HMACSHA256(keyByte))
        {
            byte[] hash = hmacsha256.ComputeHash(messageBytes);
            return Convert.ToBase64String(hash);
        }
    }

Solution

  • You need to decode the shared key first. Please try changing the following lines of code:

    authorization = crypto.createHmac('sha256', sharedKey).update(authorization.toString('utf8')).digest('base64');
    authorization = 'Authorization: SharedKey ' + workspaceId + ':' + authorization;
    

    to:

    authorization = crypto.createHmac('sha256', new Buffer(sharedKey, 'base64')).update(authorization, 'utf-8').digest('base64');
    var signature = 'SharedKey ' + workspaceId + ':' + authorization;
    

    Then the request header will look like:

    headers: {
        'content-type': 'application/json',
        'Authorization': signature,
        'Log-Type': <log_type>,
        'x-ms-date': processingDate
    },
    

    Node.js code sample

    var request = require('request');
    var crypto = require('crypto');
    
    // Azure Log Analysis credentials
    var workspaceId = 'xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
    var sharedKey = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
    
    var apiVersion = '2016-04-01';
    var processingDate = new Date().toUTCString();
    
    var jsonData = [{
       "slot_ID": 12345,
        "ID": "5cdad72f-c848-4df0-8aaa-ffe033e75d57",
        "availability_Value": 100,
        "performance_Value": 6.954,
        "measurement_Name": "last_one_hour",
        "duration": 3600,
        "warning_Threshold": 0,
        "critical_Threshold": 0,
        "IsActive": "true"
    },
    {   
        "slot_ID": 67890,
        "ID": "b6bee458-fb65-492e-996d-61c4d7fbb942",
        "availability_Value": 100,
        "performance_Value": 3.379,
        "measurement_Name": "last_one_hour",
        "duration": 3600,
        "warning_Threshold": 0,
        "critical_Threshold": 0,
        "IsActive": "false"
    }]
    
    var body = JSON.stringify(jsonData);    
    
    var contentLength = Buffer.byteLength(body, 'utf8');
    
    var stringToSign = 'POST\n' + contentLength + '\napplication/json\nx-ms-date:' + processingDate + '\n/api/logs';
    var signature = crypto.createHmac('sha256', new Buffer(sharedKey, 'base64')).update(stringToSign, 'utf-8').digest('base64');
    var authorization = 'SharedKey ' + workspaceId + ':' + signature;
    
    var headers = {
        "content-type": "application/json", 
        "Authorization": authorization,
        "Log-Type": 'WebMonitorTest',
        "x-ms-date": processingDate
    };
    
    var url = 'https://' + workspaceId + '.ods.opinsights.azure.com/api/logs?api-version=' + apiVersion;
    
    request.post({url: url, headers: headers, body: body}, function (error, response, body) {
      console.log('error:', error); 
      console.log('statusCode:', response && response.statusCode); 
      console.log('body:', body); 
    });