azureazure-iot-hubazure-rest-apiazure-iot-centralazure-sas

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


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.


Solution

  • 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
    }