Device registration and management for the Azure IoT Hub

Posted on Sunday, October 18, 2015

Recently Microsoft announced the public preview of the Azure IoT Hub.
The IoT Hub is a great solution to establish bi-directional communication between devices and the cloud. Furthermore, it has an authentication model per device, allowing you to manage per device credentials.

In this post we will look into the IoT Hub and create a solution where a device can register itself with the IoT Hub using a Registration Key and where you can remotely update the credentials on a device.
For the registration part the device will use a REST API and for updating the credentials the IoT Hub functionality of cloud-to-device and device-to-cloud messaging in combination with the FeedbackReceiver will be used.
The solution will contain:

  • A Universal Windows Platform App (that can run on any Windows 10 device and in my case will run on a Raspberry Pi 2 running Windows IoT Core)
  • A MVC 5 website (I wanted to go with an ASP.NET 5 website but the IoT Hub nuget packages aren't supporting this at this time)
  • A portable class library to share models between the UWP app and the MVC website.

The finished solution can be found on GitHub

First things first, obviously you need to create a IoT Hub using the Azure Portal.
I'm Dutch, so I'm cheap, so I'm sticking with the Free tier.

You can only have one free IoT Hub per subscription.


For the solution you also need a Storage account so create one of those as well.

When the IoT Hub and Storage account are created, you can create the solution in Visual Studio which will look something like this.

Make sure you add the Microsoft.Azure.Devices NuGet package to the MVC 5 website.
And add the Microsoft.Azure.Devices.Client NuGet package to the UWP app.

Device registration

The first thing you need to build is the generation of the Registration Keys.
We're using Table Storage for this so you need a TableEntity and some wrapper for the Storage operations.

public class RegistrationKeyEntity : TableEntity
{
    public string RegistrationKey
    {
        get { return RowKey; }
        set { RowKey = value; }
    }
    public DateTime ValidUntil { get; set; }
    public DateTime? UsedOn { get; set; }

    public RegistrationKeyEntity()
    {
        PartitionKey = "RegistrationKey";
    }
}
public class RegistrationKeyStorage
{
    private readonly CloudTable _registrationKeysTable;

    public RegistrationKeyStorage(string connectionString)
    {
        var account = CloudStorageAccount.Parse(connectionString);
        var tableClient = account.CreateCloudTableClient();
        _registrationKeysTable = tableClient.GetTableReference("RegistrationKeys");
        _registrationKeysTable.CreateIfNotExists();
    }

    public async Task<RegistrationKeyEntity> CreateRegistrationKey()
    {
        var registrationKeyEntity = new RegistrationKeyEntity();
        registrationKeyEntity.RegistrationKey = Guid.NewGuid().ToString();
        registrationKeyEntity.ValidUntil = DateTime.UtcNow.AddHours(2);

        var operation = TableOperation.InsertOrReplace(registrationKeyEntity);
        await _registrationKeysTable.ExecuteAsync(operation);
        return registrationKeyEntity;
    }
}

With this in place you can create a simple Action method for generating the Registration Key and showing the new key to the user.

public async Task<ActionResult> CreateRegistrationKey()
{
    var registrationKeyStorage = new RegistrationKeyStorage(ConfigurationManager.AppSettings["StorageConnectionString"]);
    var registrationKey = await registrationKeyStorage.CreateRegistrationKey();
    return View(registrationKey);
}

Now for the REST API, the API will check whether the Registration Key is still valid and if it hasn't been used already by some other device.
If it is still valid, it will register the new device with the IoT Hub using the RegistryManager.
And finally returning the hostname (the IoT Hub endpoint) and the Primary Shared Access Key for this newly created device.

public class DeviceRegistrationController : ApiController
{
    // POST api/values
    public async Task<IHttpActionResult> Post(DeviceRegistrationRequest request)
    {
        var registrationKeyStorage = new RegistrationKeyStorage(ConfigurationManager.AppSettings["StorageConnectionString"]);
        var registrationKey = registrationKeyStorage.GetRegistrationKey(request.RegistrationKey);
        if (registrationKey != null
            && registrationKey.ValidUntil > DateTime.UtcNow
            && !registrationKey.UsedOn.HasValue)
        {
            registrationKey.UsedOn = DateTime.UtcNow;
            await registrationKeyStorage.UpdateRegistrationKey(registrationKey);

            var registryManager = RegistryManager.CreateFromConnectionString(ConfigurationManager.AppSettings["IoTHubConnectionString"]);
            try
            {
                var device = await registryManager.AddDeviceAsync(new Device(request.DeviceId));

                var iotHubConnectionStringBuilder = IotHubConnectionStringBuilder.Create(ConfigurationManager.AppSettings["IoTHubConnectionString"]);
                return Ok(new DeviceRegistrationResponse
                {
                    HostName = iotHubConnectionStringBuilder.HostName,
                    AccessKey = device.Authentication.SymmetricKey.PrimaryKey
                });
            }
            catch (DeviceAlreadyExistsException)
            {
            }
        }
        return BadRequest();
    }
}

Since we're using the RegistryManager to manage the devices for the IoT Hub, the IoTHubConnectionString should give enough permissions to do this.
The IoT Hub has some nice permissions you can give to an access policy. To use the RegistryManager you should at least use a policy that has registry read and write permissions.
Since I'm being lazy today, I'm just going to use the primary connectionstring of the iothubowner policy.

And now for the code for the device.
In the MainPage.xaml just add some textboxes and a button, so a user can enter the Registration Key, the name of the device and the url to the REST API.

<TextBox x:Name="RegistrationUrlTextBox" PlaceholderText="Registration Url" Margin="10" Width="250" />
<TextBox x:Name="RegistrationKeyTextBox" PlaceholderText="Registration key" Margin="10" Width="250"/>
<TextBox x:Name="DeviceIdTextBox" PlaceholderText="Device id" Margin="10" Width="250"/>
<Button x:Name="RegisterButton" Content="Register" Margin="10" HorizontalAlignment="Center" Click="RegisterButton_Click"/>

In the button click, add the code to call the REST API, save the credentials and initialize the device IoT Hub client.

private async void RegisterButton_Click(object sender, RoutedEventArgs e)
{
    var deviceId = DeviceIdTextBox.Text;
    var dataToSend = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(
        new DeviceRegistrationRequest
        {
            RegistrationKey = RegistrationKeyTextBox.Text,
            DeviceId = deviceId
        }));

    var webRequest = (HttpWebRequest)WebRequest.Create(RegistrationUrlTextBox.Text);
    webRequest.ContentType = "application/json";
    webRequest.Method = "POST";
    var requestStream = await webRequest.GetRequestStreamAsync();
    requestStream.Write(dataToSend, 0, dataToSend.Length);

    try
    {
        var response = await webRequest.GetResponseAsync() as HttpWebResponse;
        if (response != null
            && response.StatusCode == HttpStatusCode.OK)
        {
            using (var streamReader = new StreamReader(response.GetResponseStream(), true))
            {
                var responseText = streamReader.ReadToEnd();
                var deviceRegistrationResponse = JsonConvert.DeserializeObject<DeviceRegistrationResponse>(responseText);

                var connectionString = SaveConnectionString(deviceRegistrationResponse.HostName, deviceId, deviceRegistrationResponse.AccessKey);
                InitializeDeviceClient(connectionString);
            }
        }
    }
    catch (Exception)
    {
    }
}

For this sample we're saving the connection string into the LocalSettings of the application. In a real world application, you probably want to encrypt it or even save it somewhere else.

private static string SaveConnectionString(string hostName, string deviceId, string accessKey)
{
    var iotHubConnectionStringBuilder = IotHubConnectionStringBuilder.Create(
        hostName,
        AuthenticationMethodFactory.CreateAuthenticationWithRegistrySymmetricKey(
            deviceId,
            accessKey));
    var settings = ApplicationData.Current.LocalSettings;
    settings.Values["IoTHubConnectionString"] = iotHubConnectionStringBuilder.ToString();
    return iotHubConnectionStringBuilder.ToString();
}

And once the connectionString has been saved, a DeviceClient can be created so the device can start receiving cloud-to-device messages.

private async Task InitializeDeviceClient(string connectionString)
{
    _deviceClient = DeviceClient.CreateFromConnectionString(connectionString, TransportType.Http1);

    Message receivedMessage;
    string messageData;
    while (true)
    {
        receivedMessage = await _deviceClient.ReceiveAsync();
        if (receivedMessage != null)
        {
            messageData = Encoding.ASCII.GetString(receivedMessage.GetBytes());
            await _deviceClient.CompleteAsync(receivedMessage);
        }
    }
}

Device management

To be able to manage the devices you obviously need to have a list of all the devices registered in the IoT Hub.
This can be accomplished by calling registryManager.GetDevicesAsync. Using this method a simple Action method can be created that returns all devices.

The GetDevicesAsync requires the number of devices it should return. By specifying -1 you instruct it to simply return all devices.

public async Task<ActionResult> Index()
{
    var registryManager = RegistryManager.CreateFromConnectionString(ConfigurationManager.AppSettings["IoTHubConnectionString"]);
    var devices = await registryManager.GetDevicesAsync(-1);
    return View(devices);
}

And use this view to display the devices.

@foreach (var device in Model)
{
    <hr />
    <form class="form-horizontal">
        <div class="form-group">
            <label class="col-sm-2 control-label">Id</label>
            <div class="col-sm-10">
                <p class="form-control-static">@(device.Id)</p>
            </div>
        </div>
        <div class="form-group">
            <label class="col-sm-2 control-label">Connection State</label>
            <div class="col-sm-10">
                <p class="form-control-static">@(device.ConnectionState)</p>
            </div>
        </div>
        <div class="form-group">
            <label class="col-sm-2 control-label">ConnectionStateUpdatedTime</label>
            <div class="col-sm-10">
                <p class="form-control-static">@(device.ConnectionStateUpdatedTime)</p>
            </div>
        </div>
        <div class="form-group">
            <label class="col-sm-2 control-label">LastActivity</label>
            <div class="col-sm-10">
                <p class="form-control-static">@(device.LastActivityTime)</p>
            </div>
        </div>
        <div class="form-group">
            @(Html.ActionLink("Renew keys", "RenewKeys", "Home", new { id = device.Id}, new { @class = "btn btn-info" }))
        </div>
    </form>
}

A button has been added per device to renew the keys for that specific device. During renewal of the keys, first instruct the device to use the secondary access key. Doing this ensures that the device can still connect to the IoT Hub while the primary key is being updated.
After the device has switched to this secondary key, the primary key can safely be updated and the device can be instructed to use the newly created primary key.

public async Task<ActionResult> RenewKeys(string id)
{
    var registryManager = RegistryManager.CreateFromConnectionString(ConfigurationManager.AppSettings["IoTHubConnectionString"]);
    var device = await registryManager.GetDeviceAsync(id);
    var serviceClient = ServiceClient.CreateFromConnectionString(ConfigurationManager.AppSettings["IoTHubConnectionString"]);

    // First tell the device to use the secondary key
    var updateAccessKey = new UpdateAccessKeyRequest
    {
        AccessKey = device.Authentication.SymmetricKey.SecondaryKey
    };
    var accepted = await SendMessageAndWaitForResponse(serviceClient, id, updateAccessKey);

    if (accepted)
    {
        // Since the device is know using the secondary key,
        // we can update the primary key without disruption for the device
        updateAccessKey = new UpdateAccessKeyRequest
        {
            AccessKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()))
        };
        var originialKey = device.Authentication.SymmetricKey.PrimaryKey;
        device.Authentication.SymmetricKey.PrimaryKey = updateAccessKey.AccessKey;
        var updatedDevice = await registryManager.UpdateDeviceAsync(device, true);
        accepted = await SendMessageAndWaitForResponse(serviceClient, id, updateAccessKey);

        // If the device could not change the AccessKey, make sure we keep using the original one
        if (!accepted)
        {
            device.Authentication.SymmetricKey.PrimaryKey = originialKey;
            updatedDevice = await registryManager.UpdateDeviceAsync(device, true);
        }
    }

    return View(accepted);
}

In the method SendMessageAndWaitForResponse the actual IoT Hub message is created and a FeedbackReceiver is requested from the ServiceClient which will be used to validate that the device has processed the message.
For the FeedbackReceiver to work, it is mandatory to set the Ack property of the message to DeliveryAcknowledgement.Full. The message should also have a unique MessageId so in the response of the FeedbackReceiver, validation can be added that this specific message has been processed. If it has, the FeedbackReceiver should be instructed that this feedback has been completed.

private async Task<bool> SendMessageAndWaitForResponse(ServiceClient serviceClient, string deviceId, UpdateAccessKeyRequest updateAccessKey)
{
    var serviceMessage = new Message(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(updateAccessKey)));
    serviceMessage.Ack = DeliveryAcknowledgement.Full;
    serviceMessage.MessageId = Guid.NewGuid().ToString();

    var feedbackReceiver = serviceClient.GetFeedbackReceiver();
    var feedbackReceived = false;
    var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token;
    var feedbackTask = Task.Run(async () =>
    {
        while (!feedbackReceived && !cancellationToken.IsCancellationRequested)
        {
            var feedbackBatch = await feedbackReceiver.ReceiveAsync(TimeSpan.FromSeconds(0.5));
            if (feedbackBatch != null)
            {
                feedbackReceived = feedbackBatch.Records.Any(fm =>
                    fm.DeviceId == deviceId
                    && fm.OriginalMessageId == serviceMessage.MessageId);
                if (feedbackReceived)
                {
                    await feedbackReceiver.CompleteAsync(feedbackBatch);
                }
            }
        }
    }, cancellationToken);

    await Task.WhenAll(
        feedbackTask,
        serviceClient.SendAsync(deviceId, serviceMessage));

    return feedbackReceived;
}

With the cloud side done, it's time to update the device side.
Inside the InitializeDeviceClient method the while loop needs to be changed. First check if the message is an UpdateAccessKeyRequest. If so update the connectionString with the new Access key, re-initialize the deviceClient and send a confirmation back to the cloud. Do note that this confirmation is being send using the new access key, thus immediately validating that this new key works. A fallback mechanism on the device side, in case the new key doesn't work, could still be implemented.

while (true)
{
    receivedMessage = await _deviceClient.ReceiveAsync();

    if (receivedMessage != null)
    {
        messageData = Encoding.ASCII.GetString(receivedMessage.GetBytes());
        var updateAccessKeyRequest = JsonConvert.DeserializeObject<UpdateAccessKeyRequest>(messageData);
        if (updateAccessKeyRequest != null)
        {
            UpdateConnectionString(updateAccessKeyRequest.AccessKey);
            await _deviceClient.CloseAsync();
            _deviceClient = DeviceClient.CreateFromConnectionString(ApplicationData.Current.LocalSettings.Values["IoTHubConnectionString"].ToString(), TransportType.Http1);
            await _deviceClient.CompleteAsync(receivedMessage);
        }
    }
}

With the UWP app done, it can be deployed to a Windows IoT Core device using this excellent post.

That's it, a minimal version of IoT Hub device registration and management. Hope you like it and triggers you to create some great stuff using the Azure IoT Hub!

One side note: the current UWP implementation of the DeviceClient only supports HTTP1. In the device list view you can see the ConnectionState of a device. This state is (currently) not being updated in the IoT Hub when using HTTP1. Instead of a UWP app, you could also build a ConsoleApplication with the same implementation, but using AMQP. In that case the ConnectionState is being updated automatically and can be observed in the device list view.


Disclaimer: Any views or opinions expressed on this blog are my own personal ones and do not represent my employer in any way.