Send telemetry messages to Azure IoT Central device using HTTP POST request

1.2k Views Asked by At

I'm trying to send telemetry to a device in Azure IoT Central with an HTTP POST request.

Similar Rest API is available for Azure IoT Hub - https://learn.microsoft.com/en-us/rest/api/iothub/device/send-device-event

I was able to extract the IoT Hub resource URL behind the Azure IoT Central using this website - https://dpsgen.z8.web.core.windows.net/

It takes Scope Id, Device Id and Device Primary Key that we get from Azure IoT Central. It gives you the IoT Hub connection string,

HostName=iotc-<<unique-iot-hub-id>>.azure-devices.net;DeviceId=<<device-id>>;SharedAccessKey=<<device-primary-key>>

Using the above IoT Hub host-name, I tried IoT Hub send device event Rest API. It is failing with an Unauthorized error.

I am using SAS token generated from the below path within the Azure IoT Central application

Azure IoT Central -> Permissions -> API tokens -> "App Administrator" Role

Any help will be useful.

2

There are 2 best solutions below

0
On BEST ANSWER

Have a look at my answer where is described in details how to generate a connection info for sending a telemetry data to the Azure IoT Central App using a REST Post request.

The following is an updated azure function to generate a requested device connection info:

#r "Newtonsoft.Json"

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;


public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
    int retryCounter = 10;
    int pollingTimeInSeconds = 3;

    string deviceId = req.Query["deviceid"];
    string mid = req.Query["modelid"];
               
    log.LogInformation($"DeviceId = {deviceId}, ModelId = {mid}");

    if (!Regex.IsMatch(deviceId, @"^[a-z0-9\-]+$"))
        throw new Exception($"Invalid format: DeviceID must be alphanumeric, lowercase, and may contain hyphens");

    string iotcScopeId = System.Environment.GetEnvironmentVariable("AzureIoTC_scopeId");      
    string iotcSasToken = System.Environment.GetEnvironmentVariable("AzureIoTC_sasToken");    
            
    if(string.IsNullOrEmpty(iotcScopeId) || string.IsNullOrEmpty(iotcSasToken))
        throw new ArgumentNullException($"Missing the scopeId and/or sasToken of the IoT Central App");

    string deviceKey = SharedAccessSignatureBuilder.ComputeSignature(iotcSasToken, deviceId);
 
    string address = $"https://global.azure-devices-provisioning.net/{iotcScopeId}/registrations/{deviceId}/register?api-version=2021-06-01";
    string sas = SharedAccessSignatureBuilder.GetSASToken($"{iotcScopeId}/registrations/{deviceId}", deviceKey, "registration");
 
    log.LogInformation($"sas_token: {sas}");

    using (HttpClient client = new HttpClient())
    {
        client.DefaultRequestHeaders.Add("Authorization", sas);
        client.DefaultRequestHeaders.Add("accept", "application/json");
        string jsontext = string.IsNullOrEmpty(mid) ? null : $"{{ \"modelId\": \"{mid}\" }}";
        var response = await client.PutAsync(address, new StringContent(JsonConvert.SerializeObject(new { registrationId = deviceId, payload = jsontext }), Encoding.UTF8, "application/json"));

        var atype = new { errorCode = "", message = "", operationId = "", status = "", registrationState = new JObject() };
        do
        {
            dynamic operationStatus = JsonConvert.DeserializeAnonymousType(await response.Content.ReadAsStringAsync(), atype);
            if (!string.IsNullOrEmpty(operationStatus.errorCode))
            {
                throw new Exception($"{operationStatus.errorCode} - {operationStatus.message}");
            }
            response.EnsureSuccessStatusCode();
            if (operationStatus.status == "assigning")
            {
                Task.Delay(TimeSpan.FromSeconds(pollingTimeInSeconds)).Wait();
                address = $"https://global.azure-devices-provisioning.net/{iotcScopeId}/registrations/{deviceId}/operations/{operationStatus.operationId}?api-version=2021-06-01";
                response = await client.GetAsync(address);
            }
            else if (operationStatus.status == "assigned")
            {
                log.LogInformation($"{JsonConvert.SerializeObject(operationStatus, Formatting.Indented)}");
                string assignedHub = operationStatus.registrationState.assignedHub;
                string cstr = $"HostName={assignedHub};DeviceId={operationStatus.registrationState.deviceId};SharedAccessKey={deviceKey}"; // + (string.IsNullOrEmpty(mid) ? "" : $";modelId={mid.Replace(";","#")}"); 
                string requestUrl = $"https://{assignedHub}/devices/{operationStatus.registrationState.deviceId}/messages/events?api-version=2021-04-12";
                string deviceSasToken = SharedAccessSignatureBuilder.GetSASToken($"{assignedHub}/{operationStatus.registrationState.deviceId}", deviceKey);

                log.LogInformation($"IoTC DeviceConnectionString:\n\t{cstr}");
                return new OkObjectResult(JObject.FromObject(new { iotHub = assignedHub, iotFireUrl = requestUrl, deviceSasToken = deviceSasToken, deviceConnectionString = cstr }));
            }
            else
            {
                throw new Exception($"{operationStatus.registrationState.status}: {operationStatus.registrationState.errorCode} - {operationStatus.registrationState.errorMessage}");
            }
        } while (--retryCounter > 0);

        throw new Exception("Registration device status retry timeout exprired, try again.");
    } 
}

public sealed class SharedAccessSignatureBuilder
{
    public static string GetHostNameNamespaceFromConnectionString(string connectionString)
    {
        return GetPartsFromConnectionString(connectionString)["HostName"].Split('.').FirstOrDefault();
    }
    public static string GetSASTokenFromConnectionString(string connectionString, uint hours = 24)
    {
        var parts = GetPartsFromConnectionString(connectionString);
        if (parts.ContainsKey("HostName") && parts.ContainsKey("SharedAccessKey"))
            return GetSASToken(parts["HostName"], parts["SharedAccessKey"], parts.Keys.Contains("SharedAccessKeyName") ? parts["SharedAccessKeyName"] : null, hours);
        else
            return string.Empty;
    }
    public static string GetSASToken(string resourceUri, string key, string keyName = null, uint hours = 24)
    {
        try
        {
            var expiry = GetExpiry(hours);
            string stringToSign = System.Web.HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
            var signature = SharedAccessSignatureBuilder.ComputeSignature(key, stringToSign);
            var sasToken = keyName == null ?
                String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry) :
                String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry, keyName);
            return sasToken;
        }
        catch
        {
            return string.Empty;
        }
    }

    #region Helpers
    public static string ComputeSignature(string key, string stringToSign)
    {
        using (HMACSHA256 hmac = new HMACSHA256(Convert.FromBase64String(key)))
        {
            return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
        }
    }

    public static Dictionary<string, string> GetPartsFromConnectionString(string connectionString)
    {
        return connectionString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Split(new[] { '=' }, 2)).ToDictionary(x => x[0].Trim(), x => x[1].Trim(), StringComparer.OrdinalIgnoreCase);
    }

    // default expiring = 24 hours
    private static string GetExpiry(uint hours = 24)
    {
        TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
        return Convert.ToString((ulong)sinceEpoch.TotalSeconds + 3600 * hours);
    }

    public static DateTime GetDateTimeUtcFromExpiry(ulong expiry)
    {
        return (new DateTime(1970, 1, 1)).AddSeconds(expiry);
    }
    public static bool IsValidExpiry(ulong expiry, ulong toleranceInSeconds = 0)
    {
        return GetDateTimeUtcFromExpiry(expiry) - TimeSpan.FromSeconds(toleranceInSeconds) > DateTime.UtcNow;
    }

    public static string CreateSHA256Key(string secret)
    {
        using (var provider = new SHA256CryptoServiceProvider())
        {
            byte[] keyArray = provider.ComputeHash(UTF8Encoding.UTF8.GetBytes(secret));
            provider.Clear();
            return Convert.ToBase64String(keyArray);
        }
    }

    public static string CreateRNGKey(int keySize = 32)
    {
        byte[] keyArray = new byte[keySize];
        using (var provider = new RNGCryptoServiceProvider())
        {
            provider.GetNonZeroBytes(keyArray);
        }
        return Convert.ToBase64String(keyArray);
    }
    #endregion
}
0
On

The IoT Central API token is to manage the application functionality and cannot be used by the device. Select the device in IoT Central and click on the "Connect" menu at the top, use the primary key shown for that device.

As a side note, https although supported for devices is not well suited for IoT due to its polling nature and does not support device twin desired or reported properties. https://learn.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-d2c-guidance

IoT Central provides built-in high availability, the underlying IoTHub name can change, so manually getting the IoTHub name is not recommended. Always make call DPS to retrieve IoTHub name, the first time and periodically or error conditions.