From c7aa1ac8d18dca8c975806adfbe4a378e2b0fc42 Mon Sep 17 00:00:00 2001 From: "David R. Williamson" Date: Wed, 10 Feb 2021 10:18:09 -0800 Subject: [PATCH 01/29] fix: samples readme links were out-of-date (#1784) --- iothub/device/samples/readme.md | 56 +++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/iothub/device/samples/readme.md b/iothub/device/samples/readme.md index e28b7d4056..719e22aab6 100644 --- a/iothub/device/samples/readme.md +++ b/iothub/device/samples/readme.md @@ -2,63 +2,73 @@ This folder contains simple samples showing how to use the various features of Microsoft Azure IoT Hub service, from a device running C# code. ### [Device samples][device-samples] -* [Message Sample][d-message-sample] -* [Method Sample][d-method-sample] -* [Twin Sample][d-twin-sample] -* [File Upload Sample][d-file-upload-sample] -* [Keys Rollover Sample][d-keys-rollover-sample] -* [Import Export Devices Sample][d-import-export-devices-sample] -* [Plug And Play Device Sample][d-pnp-sample] -* [Xamarin Sample][d-xamarin-sample] + +- [Reconnection sample][d-message-sample] + - This sample illustrates how to write a device application to handle connection issues, connection-related exceptions, and how to manage the lifetime of the `DeviceClient` + - Includes sending messages and symmetric key failover +- [Method sample][d-method-sample] +- [Receive message sample][d-receive-message-sample] +- [Twin sample][d-twin-sample] +- [File upload sample][d-file-upload-sample] +- [Import/export devices sample][d-import-export-devices-sample] +- [Connect with X509 certificate sample][d-x509-cert-sample] +- [Plug and Play device samples][d-pnp-sample] +- [Xamarin sample][d-xamarin-sample] ### [Module samples][module-samples] -* [Message Sample][m-message-sample] -* [Twin Sample][m-twin-sample] + +- [Message sample][m-message-sample] +- [Twin sample][m-twin-sample] ### Prerequisites + In order to run the device samples on Linux or Windows, you will first need the following prerequisites: -* [Setup your IoT hub][lnk-setup-iot-hub] -* [Provision your device and get its credentials][lnk-manage-iot-device] + +- [Setup your IoT hub][lnk-setup-iot-hub] +- [Provision your device and get its credentials][lnk-manage-iot-device] ### Setup environment The following prerequisite is the minimum requirement to build and run the samples. -Visual Studio is **not required** to run the samples. +- Install the latest .NET Core from -- Install the latest .NET Core from https://dot.net +> Visual Studio is **not required** to run the samples. ### Get and run the samples + You need to clone the repository or download the sample (the one you want to try) project's folder on your device. -#### Build and run the samples: +#### Build and run the samples + 1. Preparing the sample application: 1. Set the following environment variables on the terminal from which you want to run the application. - * IOTHUB_DEVICE_CONNECTION_STRING + - IOTHUB_DEVICE_CONNECTION_STRING -2. Building the sample application: +1. Building the sample application: To build the sample application using dotnet, from terminal navigate to the sample folder (where the .csproj file lives). Then execute the following command and check for build errors: - ``` + ```console dotnet build ``` -3. Running the sample application: +1. Running the sample application: - To run the sample application using dotnet, execute the following command. + To run the sample application using dotnet, execute the following command. - ``` + ```console dotnet run ``` [device-samples]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device -[d-message-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/MessageSample +[d-message-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/DeviceReconnectionSample +[d-receive-message-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/MessageReceiveSample [d-method-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/MethodSample [d-twin-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/TwinSample [d-file-upload-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/FileUploadSample -[d-keys-rollover-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/KeysRolloverSample +[d-x509-cert-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/X509DeviceCertWithChainSample [d-import-export-devices-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/ImportExportDevicesSample [d-pnp-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/PnpDeviceSamples [d-xamarin-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/XamarinSample From 91d7fe2da963ab06fc7bcacc8b225a2151966d5c Mon Sep 17 00:00:00 2001 From: "David R. Williamson" Date: Wed, 10 Feb 2021 10:48:56 -0800 Subject: [PATCH 02/29] fix: samples arguments and remove 1 more dead link (#1785) --- iothub/device/samples/readme.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/iothub/device/samples/readme.md b/iothub/device/samples/readme.md index 719e22aab6..3908583189 100644 --- a/iothub/device/samples/readme.md +++ b/iothub/device/samples/readme.md @@ -15,10 +15,11 @@ This folder contains simple samples showing how to use the various features of M - [Plug and Play device samples][d-pnp-sample] - [Xamarin sample][d-xamarin-sample] -### [Module samples][module-samples] +### Module sample - [Message sample][m-message-sample] -- [Twin sample][m-twin-sample] + - This sample illustrates how to write an IoT Hub module to handle connection issues, connection-related exceptions, and how to manage the lifetime of the `ModuleClient` + - Includes sending messages and symmetric key failover ### Prerequisites @@ -41,12 +42,7 @@ You need to clone the repository or download the sample (the one you want to try #### Build and run the samples -1. Preparing the sample application: - 1. Set the following environment variables on the terminal from which you want to run the application. - - - IOTHUB_DEVICE_CONNECTION_STRING - -1. Building the sample application: +1. Building the sample application To build the sample application using dotnet, from terminal navigate to the sample folder (where the .csproj file lives). Then execute the following command and check for build errors: @@ -54,9 +50,16 @@ You need to clone the repository or download the sample (the one you want to try dotnet build ``` +1. Preparing the sample application: + 1. Many of these samples take parameters. To see the parameters required, type: + + ```console + dotnet run --help + ``` + 1. Running the sample application: - To run the sample application using dotnet, execute the following command. + To run the sample application using dotnet, execute the following command with any required parameters discovered in the previous step. ```console dotnet run @@ -73,9 +76,7 @@ You need to clone the repository or download the sample (the one you want to try [d-pnp-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/PnpDeviceSamples [d-xamarin-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/XamarinSample -[module-samples]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/module -[m-message-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/module/MessageSample -[m-twin-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/module/TwinSample +[m-message-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/module/ModuleSample [lnk-setup-iot-hub]: https://aka.ms/howtocreateazureiothub [lnk-manage-iot-device]: https://github.com/Azure/azure-iot-device-ecosystem/blob/master/setup_iothub.md#create-new-device-in-the-iot-hub-device-identity-registry \ No newline at end of file From c371f5788eb000c81548e7f1ca1689b731ecd0d8 Mon Sep 17 00:00:00 2001 From: Abhipsa Misra Date: Tue, 16 Feb 2021 14:46:02 -0800 Subject: [PATCH 03/29] fix(readme): Update the location of ConsoleEventListener in our readme --- tools/CaptureLogs/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/CaptureLogs/readme.md b/tools/CaptureLogs/readme.md index 4b64525af5..794c2f4d24 100644 --- a/tools/CaptureLogs/readme.md +++ b/tools/CaptureLogs/readme.md @@ -9,7 +9,7 @@ On Linux and OSX LTTNG and perfcollect can be used to collect traces. For more i ## Console logging Logging can be added to console. Note that this method will substantially slow down execution. - 1. Add `e2e\test\CommonConsoleEventListener.cs` to your project. + 1. Add `e2e\test\Helpers\ConsoleEventListener.cs` to your project. 2. Instantiate the listener. Add one or more filters (e.g. `Microsoft-Azure-` or `DotNetty-`): ```C# From 5a753da7c4e5b6f31e60940397fe019234498be6 Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Fri, 19 Feb 2021 10:19:58 -0800 Subject: [PATCH 04/29] doc(service-client) - Updating readme (#1799) --- readme.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/readme.md b/readme.md index 1a89f2e50f..b4b0f7ea56 100644 --- a/readme.md +++ b/readme.md @@ -103,17 +103,17 @@ For details on OS support see the following resources: :heavy_check_mark: feature available :heavy_multiplication_x: feature planned but not supported :heavy_minus_sign: no support planned -| Features | Support | Transport protocol used underneath | Description | -|---------------------------------------------------------------------------------------------------------------|--------------------- |-------------------------|--------------------------------------------------------------------------------------------------------------------------| -| [Identity registry (CRUD)](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry) | :heavy_check_mark: | HTTP | Use your backend app to perform CRUD operation for individual device or in bulk. | -| [Cloud-to-device messaging](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d) | :heavy_check_mark: | AMQP | Use your backend app to send cloud-to-device messages in AMQP and AMQP-WS, and set up cloud-to-device message receivers. | -| [Direct Methods operations](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-direct-methods) | :heavy_check_mark: | HTTP | Use your backend app to invoke direct method on device. | -| [Device Twins operations](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-device-twins) | :heavy_check_mark: | HTTP | Use your backend app to perform twin operations. | -| [Query](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-query-language) | :heavy_check_mark: | HTTP | Use your backend app to perform query for information. | -| [Jobs](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-jobs) | :heavy_check_mark: | HTTP | Use your backend app to perform job operation. | -| [File Upload](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-file-upload) | :heavy_check_mark: | AMQP | Set up your backend app to receive file upload notifications. -| [Digital Twin Client](https://docs.microsoft.com/en-us/azure/iot-pnp/overview-iot-plug-and-play) | :heavy_check_mark: | HTTP | Set up your backend app to perform operations on plug and play devices. | -| [IoT Hub Statistics](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-metrics) | :heavy_check_mark: | HTTP | Get IoT Hub identity registry statistics; such as total device count for device statistics, and connected device count for service statistics. | +| Features | Support | Transport protocol used underneath | Client to use | Description | +|---------------------------------------------------------------------------------------------------------------|--------------------- |-------------------------| -------|--------------------------------------------------------------------------------------------------------------------------| +| [Identity registry (CRUD)](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-identity-registry) | :heavy_check_mark: | HTTP | RegistryManager | Use your backend app to perform CRUD operation for individual device or in bulk. || +| [Query](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-query-language) | :heavy_check_mark: | HTTP | RegistryManager | Use your backend app to query for information on device twins, module twins, jobs and message routing. | +| [Import/Export jobs](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-jobs) | :heavy_check_mark: | HTTP | RegistryManager | Use your backend app to import or export device identities in bulk. | +| [Scheduled jobs](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-jobs) | :heavy_check_mark: | HTTP | JobsClient | Use your backend app to schedule jobs to update desired properties, update tags and invoke direct methods. +| [Cloud-to-device messaging](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d) | :heavy_check_mark: | AMQP | ServiceClient | Use your backend app to send cloud-to-device messages in AMQP and AMQP-WS, and set up notifications for cloud-to-device message delivery. | +| [Direct Methods operations](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-direct-methods) | :heavy_check_mark: | HTTP | ServiceClient | Use your backend app to invoke direct method on device. | +| [File Upload Notifications](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-file-upload) | :heavy_check_mark: | AMQP | ServiceClient | Use your backend app to receive file upload notifications. +| [IoT Hub Statistics](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-metrics) | :heavy_check_mark: | HTTP | ServiceClient | Use your backend app to get IoT hub identity registry statistics such as total device count for device statistics, and connected device count for service statistics. +| [Digital Twin Operations](https://docs.microsoft.com/en-us/azure/iot-pnp/overview-iot-plug-and-play) | :heavy_check_mark: | HTTP | DigitalTwinClient or RegistryManager | Use your backend app to perform operations on plug and play devices. The operations include get twins, update twins and invoke commands. DigitalTwinClient is the preferred client to use. ### Provisioning Device SDK From c7d4a5fbccbc6b5f53723d0f1f870e0629fd27c3 Mon Sep 17 00:00:00 2001 From: Abhipsa Misra Date: Mon, 22 Feb 2021 13:15:07 -0800 Subject: [PATCH 05/29] fix(doc): Update amqp transport exception doc to have detailed description for quota exceeded error mapping --- iothub/device/devdoc/amqpTransportExceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iothub/device/devdoc/amqpTransportExceptions.md b/iothub/device/devdoc/amqpTransportExceptions.md index 7016dfa8ac..71c6562895 100644 --- a/iothub/device/devdoc/amqpTransportExceptions.md +++ b/iothub/device/devdoc/amqpTransportExceptions.md @@ -41,7 +41,7 @@ Below is the behavior of the SDK on receiving an exception over Amqp transport p | amqp:not-implemented | NotSupportedException | InnerException: AmqpException.Error.Condition = AmqpSymbol.NotImplemented | Inspect the exception details, collect logs and contact service | | amqp:precondition-failed | IotHubException | InnerException: AmqpException.Error.Condition = AmqpSymbol.PreconditionFailed | Inspect the exception details, collect logs and contact service | | amqp:resource-deleted | IotHubException | InnerException: AmqpException.Error.Condition = AmqpSymbol.ResourceDeleted | Inspect the exception details, collect logs and contact service | -| amqp:resource-limit-exceeded | IotHubException | InnerException: AmqpException.Error.Condition = AmqpSymbol.ResourceLimitExceeded | Inspect the exception details, collect logs and contact service | +| amqp:resource-limit-exceeded | DeviceMaximumQueueDepthExceededException | The correct exception type for this error code is `QuotaExceededException` but it was incorrectly mapped to `DeviceMaximumQueueDepthExceededException`. In order to avoid a breaking change, we now return the correct exception details as an inner exception within the `DeviceMaximumQueueDepthExceededException` thrown. | Upgrade or increase the number of units on your IoT Hub or wait until the next UTC day for the daily quota to refresh and then retry the operation. | | amqp:unauthorized-access | UnauthorizedException | InnerException: AmqpException.Error.Condition = AmqpSymbol.UnauthorizedAccess | Inspect your credentials | | com.microsoft:message-lock-lost | DeviceMessageLockLostException | The device client attempted to complete/reject/abandon a received cloud-to-device message, but the lock token was expired (took > 1min after receiving the message to complete/reject/abandon it) | Call `ReceiveAsync()` again to retrieve an updated lock token, and then complete/reject/abandon the message. De-duplication logic wil need to be implemented at the application level | | amqp:transaction :unknown-id | IotHubException | InnerException: AmqpException.Error.Condition = AmqpSymbol.TransactionUnknownId | Inspect the exception details, collect logs and contact service | From 2726affeda91d08cf09af676cb346397d9076025 Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Thu, 25 Feb 2021 10:06:18 -0800 Subject: [PATCH 06/29] feature(device-client): Make the DeviceClient and ModuleClient extensible (#1802) --- iothub/device/src/DeviceClient.cs | 22 ++++++++++++++++++++-- iothub/device/src/ModuleClient.cs | 22 ++++++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/iothub/device/src/DeviceClient.cs b/iothub/device/src/DeviceClient.cs index 43966a49c6..57e91e77a2 100644 --- a/iothub/device/src/DeviceClient.cs +++ b/iothub/device/src/DeviceClient.cs @@ -15,7 +15,7 @@ namespace Microsoft.Azure.Devices.Client /// Contains methods that a device can use to send messages to and receive from the service. /// /// - public sealed class DeviceClient : IDisposable + public class DeviceClient : IDisposable { /// /// Default operation timeout. @@ -608,7 +608,25 @@ public void SetConnectionStatusChangesHandler(ConnectionStatusChangesHandler sta /// /// Releases the unmanaged resources used by the DeviceClient and optionally disposes of the managed resources. /// - public void Dispose() => InternalClient?.Dispose(); + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the unmanaged resources used by the DeviceClient and allows for any derived class to override and + /// provide custom implementation. + /// + /// Setting to true will release both managed and unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + InternalClient?.Dispose(); + InternalClient = null; + } + } /// /// Set a callback that will be called whenever the client receives a state update diff --git a/iothub/device/src/ModuleClient.cs b/iothub/device/src/ModuleClient.cs index 901a84d57e..aee3b0a706 100644 --- a/iothub/device/src/ModuleClient.cs +++ b/iothub/device/src/ModuleClient.cs @@ -21,7 +21,7 @@ namespace Microsoft.Azure.Devices.Client /// /// Contains methods that a module can use to send messages to and receive from the service and interact with module twins. /// - public sealed class ModuleClient : IDisposable + public class ModuleClient : IDisposable { private const string ModuleMethodUriFormat = "/twins/{0}/modules/{1}/methods?" + ClientApiVersionHelper.ApiVersionQueryStringLatest; private const string DeviceMethodUriFormat = "/twins/{0}/methods?" + ClientApiVersionHelper.ApiVersionQueryStringLatest; @@ -433,7 +433,25 @@ public void SetConnectionStatusChangesHandler(ConnectionStatusChangesHandler sta /// /// Releases the unmanaged resources used by the ModuleClient and optionally disposes of the managed resources. /// - public void Dispose() => InternalClient?.Dispose(); + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the unmanaged resources used by the ModuleClient and allows for any derived class to override and + /// provide custom implementation. + /// + /// Setting to true will release both managed and unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + InternalClient?.Dispose(); + InternalClient = null; + } + } /// /// Set a callback that will be called whenever the client receives a state update From c95dc1f9376496a4a6271237827be746d533161d Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Fri, 26 Feb 2021 13:14:11 -0800 Subject: [PATCH 07/29] doc(service-client): Added extra comments to clarify true and false in dispose (#1805) --- iothub/device/src/DeviceClient.cs | 3 ++- iothub/device/src/ModuleClient.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/iothub/device/src/DeviceClient.cs b/iothub/device/src/DeviceClient.cs index 57e91e77a2..9ab69f8f3e 100644 --- a/iothub/device/src/DeviceClient.cs +++ b/iothub/device/src/DeviceClient.cs @@ -618,7 +618,8 @@ public void Dispose() /// Releases the unmanaged resources used by the DeviceClient and allows for any derived class to override and /// provide custom implementation. /// - /// Setting to true will release both managed and unmanaged resources. + /// Setting to true will release both managed and unmanaged resources. Setting to + /// false will only release the unmanaged resources. protected virtual void Dispose(bool disposing) { if (disposing) diff --git a/iothub/device/src/ModuleClient.cs b/iothub/device/src/ModuleClient.cs index aee3b0a706..8970dd1f0d 100644 --- a/iothub/device/src/ModuleClient.cs +++ b/iothub/device/src/ModuleClient.cs @@ -443,7 +443,8 @@ public void Dispose() /// Releases the unmanaged resources used by the ModuleClient and allows for any derived class to override and /// provide custom implementation. /// - /// Setting to true will release both managed and unmanaged resources. + /// Setting to true will release both managed and unmanaged resources. Setting to + /// false will only release the unmanaged resources. protected virtual void Dispose(bool disposing) { if (disposing) From 5fcd166c4dc936ae75779a5283042e21fb74a73c Mon Sep 17 00:00:00 2001 From: bikamani <41314966+bikamani@users.noreply.github.com> Date: Fri, 26 Feb 2021 17:24:31 -0800 Subject: [PATCH 08/29] feature,fix (device-client) Handle Twin failures using Amqp (#1796) --- e2e/test/iothub/twin/TwinE2ETests.cs | 6 +- .../Transport/Amqp/AmqpTransportHandler.cs | 46 +++++----- .../Transport/AmqpIoT/AmqpIoTReceivingLink.cs | 87 ++++++++++--------- 3 files changed, 79 insertions(+), 60 deletions(-) diff --git a/e2e/test/iothub/twin/TwinE2ETests.cs b/e2e/test/iothub/twin/TwinE2ETests.cs index b4a660bc21..622534243f 100644 --- a/e2e/test/iothub/twin/TwinE2ETests.cs +++ b/e2e/test/iothub/twin/TwinE2ETests.cs @@ -10,7 +10,6 @@ using Microsoft.Azure.Devices.Shared; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Devices.E2ETests.Twins { @@ -367,6 +366,10 @@ await Twin_ClientHandlesRejectionInvalidPropertyNameAsync( .ConfigureAwait(false); } + // These tests worked earlier for Amqp and AmqpWs since it was catching a wrong exception + // To Do: Fix Update reported properties method behavior (breaking change) to wait for response + // and we should be able to enable these tests then. + [Ignore] [LoggedTestMethod] public async Task Twin_ClientHandlesRejectionInvalidPropertyName_Amqp() { @@ -375,6 +378,7 @@ await Twin_ClientHandlesRejectionInvalidPropertyNameAsync( .ConfigureAwait(false); } + [Ignore] [LoggedTestMethod] public async Task Twin_ClientHandlesRejectionInvalidPropertyName_AmqpWs() { diff --git a/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs b/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs index 0dae99b25f..8b662b27ed 100644 --- a/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs +++ b/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs @@ -324,7 +324,8 @@ public override async Task EnableTwinPatchAsync(CancellationToken cancellationTo try { cancellationToken.ThrowIfCancellationRequested(); - await _amqpUnit.SendTwinMessageAsync(AmqpTwinMessageType.Put, Guid.NewGuid().ToString(), null, _operationTimeout).ConfigureAwait(false); + string correlationId = AmqpTwinMessageType.Put + Guid.NewGuid().ToString(); + await _amqpUnit.SendTwinMessageAsync(AmqpTwinMessageType.Put, correlationId, null, _operationTimeout).ConfigureAwait(false); } finally { @@ -355,7 +356,7 @@ public override async Task SendTwinGetAsync(CancellationToken cancellation try { await EnableTwinPatchAsync(cancellationToken).ConfigureAwait(false); - Twin twin = await RoundTripTwinMessage(AmqpTwinMessageType.Get, null, cancellationToken).ConfigureAwait(false); + Twin twin = await RoundTripTwinMessageAsync(AmqpTwinMessageType.Get, null, cancellationToken).ConfigureAwait(false); if (twin == null) { throw new InvalidOperationException("Service rejected the message"); @@ -375,7 +376,7 @@ public override async Task SendTwinPatchAsync(TwinCollection reportedProperties, try { await EnableTwinPatchAsync(cancellationToken).ConfigureAwait(false); - Twin twin = await RoundTripTwinMessage(AmqpTwinMessageType.Patch, reportedProperties, cancellationToken).ConfigureAwait(false); + await RoundTripTwinMessageAsync(AmqpTwinMessageType.Patch, reportedProperties, cancellationToken).ConfigureAwait(false); } finally { @@ -383,11 +384,11 @@ public override async Task SendTwinPatchAsync(TwinCollection reportedProperties, } } - private async Task RoundTripTwinMessage(AmqpTwinMessageType amqpTwinMessageType, TwinCollection reportedProperties, CancellationToken cancellationToken) + private async Task RoundTripTwinMessageAsync(AmqpTwinMessageType amqpTwinMessageType, TwinCollection reportedProperties, CancellationToken cancellationToken) { - Logging.Enter(this, cancellationToken, $"{nameof(RoundTripTwinMessage)}"); + Logging.Enter(this, cancellationToken, $"{nameof(RoundTripTwinMessageAsync)}"); - string correlationId = Guid.NewGuid().ToString(); + string correlationId = amqpTwinMessageType + Guid.NewGuid().ToString(); Twin response = null; try @@ -405,10 +406,6 @@ private async Task RoundTripTwinMessage(AmqpTwinMessageType amqpTwinMessag // Consider that the task may have faulted or been canceled. // We re-await the task so that any exceptions/cancellation is rethrown. response = await receivingTask.ConfigureAwait(false); - if (response == null) - { - throw new InvalidOperationException("Service response is null"); - } } else { @@ -419,7 +416,7 @@ private async Task RoundTripTwinMessage(AmqpTwinMessageType amqpTwinMessag finally { _twinResponseCompletions.TryRemove(correlationId, out _); - Logging.Exit(this, cancellationToken, $"{nameof(RoundTripTwinMessage)}"); + Logging.Exit(this, cancellationToken, $"{nameof(RoundTripTwinMessageAsync)}"); } return response; @@ -522,19 +519,28 @@ private async Task DisposeMessageAsync(string lockToken, AmqpIoTDisposeActions o private void TwinMessageListener(Twin twin, string correlationId, TwinCollection twinCollection) { - if (correlationId != null) + if (correlationId == null) { - // It is a GET, just complete the task. - TaskCompletionSource task; - if (_twinResponseCompletions.TryRemove(correlationId, out task)) - { - task.SetResult(twin); - } + // This is desired property updates, so call the callback with TwinCollection. + _onDesiredStatePatchListener(twinCollection); } else { - // It is a PATCH, just call the callback with the TwinCollection - _onDesiredStatePatchListener(twinCollection); + if (correlationId.StartsWith(AmqpTwinMessageType.Get.ToString(), StringComparison.OrdinalIgnoreCase) || + correlationId.StartsWith(AmqpTwinMessageType.Patch.ToString(), StringComparison.OrdinalIgnoreCase)) + { + // For Get and Patch, complete the task. + TaskCompletionSource task; + if (_twinResponseCompletions.TryRemove(correlationId, out task)) + { + task.SetResult(twin); + } + else + { + // This can happen if we received a message from service with correlation Id that was not set by SDK or does not exist in dictionary. + Logging.Info("Could not remove correlation id to complete the task awaiter for a twin operation.", nameof(TwinMessageListener)); + } + } } } diff --git a/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs b/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs index 0ed8dc63f3..cde5458672 100644 --- a/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs +++ b/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs @@ -23,7 +23,7 @@ internal class AmqpIoTReceivingLink private Action _onEventsReceived; private Action _onDeviceMessageReceived; private Action _onMethodReceived; - private Action _onDesiredPropertyReceived; + private Action _onTwinMessageReceived; public AmqpIoTReceivingLink(ReceivingAmqpLink receivingAmqpLink) { @@ -257,89 +257,98 @@ private void DisposeDelivery(AmqpMessage amqpMessage, bool settled, Accepted acc internal void RegisterTwinListener(Action onDesiredPropertyReceived) { - _onDesiredPropertyReceived = onDesiredPropertyReceived; - _receivingAmqpLink.RegisterMessageListener(OnDesiredPropertyReceived); + _onTwinMessageReceived = onDesiredPropertyReceived; + _receivingAmqpLink.RegisterMessageListener(OnTwinChangesReceived); } - private void OnDesiredPropertyReceived(AmqpMessage amqpMessage) + private void OnTwinChangesReceived(AmqpMessage amqpMessage) { if (Logging.IsEnabled) { - Logging.Enter(this, amqpMessage, $"{nameof(OnDesiredPropertyReceived)}"); + Logging.Enter(this, amqpMessage, $"{nameof(OnTwinChangesReceived)}"); } try { _receivingAmqpLink.DisposeDelivery(amqpMessage, true, AmqpIoTConstants.AcceptedOutcome); string correlationId = amqpMessage.Properties?.CorrelationId?.ToString(); - - if (!VerifyResponseMessage(amqpMessage)) - { - _onDesiredPropertyReceived.Invoke(null, correlationId, null); - } + int status = GetStatus(amqpMessage); Twin twin = null; TwinCollection twinProperties = null; - if (correlationId != null) + if (status >= 400) { - if (amqpMessage.BodyStream != null) + // Handle failures + _onTwinMessageReceived.Invoke(null, correlationId, null); + if (correlationId.StartsWith(AmqpTwinMessageType.Get.ToString(), StringComparison.OrdinalIgnoreCase)) { - // This a result of a GET TWIN so return (set) the full twin - using (StreamReader reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8)) + string error = null; + using (var reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8)) { - string body = reader.ReadToEnd(); - var properties = JsonConvert.DeserializeObject(body); - twin = new Twin(properties); - } - } - else - { - // This is a desired property ack from the service - twin = new Twin(); + error = reader.ReadToEnd(); + }; + // Retry for Http status code request timeout, Too many requests and server errors + throw new IotHubException(error, status >= 500 || status == 429 || status == 408); } } else { - // No correlationId, this is a PATCH sent by the sevice so return (set) the TwinCollection - - using (StreamReader reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8)) + if (correlationId == null) { + // Here we are getting desired property update notifications and want to handle it first + using var reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8); string patch = reader.ReadToEnd(); twinProperties = JsonConvert.DeserializeObject(patch); } + else if (correlationId.StartsWith(AmqpTwinMessageType.Get.ToString(), StringComparison.OrdinalIgnoreCase)) + { + // This a response of a GET TWIN so return (set) the full twin + using var reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8); + string body = reader.ReadToEnd(); + var properties = JsonConvert.DeserializeObject(body); + twin = new Twin(properties); + } + else if (correlationId.StartsWith(AmqpTwinMessageType.Patch.ToString(), StringComparison.OrdinalIgnoreCase)) + { + // This can be used to coorelate success response with updating reported properties + // However currently we do not have it as request response style implementation + Logging.Info("Updated twin reported properties successfully", nameof(OnTwinChangesReceived)); + } + else if (correlationId.StartsWith(AmqpTwinMessageType.Put.ToString(), StringComparison.OrdinalIgnoreCase)) + { + // This is an acknowledgement received from service for subscribing to desired property updates + Logging.Info("Subscribed for twin successfully", nameof(OnTwinChangesReceived)); + } + else + { + // This shouldn't happen + Logging.Info("Received a correlation Id for Twin operation that does not match Get, Patch or Put request", nameof(OnTwinChangesReceived)); + } + _onTwinMessageReceived.Invoke(twin, correlationId, twinProperties); } - _onDesiredPropertyReceived.Invoke(twin, correlationId, twinProperties); } finally { if (Logging.IsEnabled) { - Logging.Exit(this, amqpMessage, $"{nameof(OnDesiredPropertyReceived)}"); + Logging.Exit(this, amqpMessage, $"{nameof(OnTwinChangesReceived)}"); } } } #endregion Twin handling - internal static bool VerifyResponseMessage(AmqpMessage response) + internal static int GetStatus(AmqpMessage response) { - bool retVal = true; if (response != null) { if (response.MessageAnnotations.Map.TryGetValue(AmqpIoTConstants.ResponseStatusName, out int status)) { - if (status >= 400) - { - retVal = false; - } + return status; } } - else - { - retVal = false; - } - return retVal; + return -1; } } } From a6f7409484389c29879b46de8ec960703dcfcf90 Mon Sep 17 00:00:00 2001 From: Abhipsa Misra Date: Tue, 2 Mar 2021 13:05:03 -0800 Subject: [PATCH 09/29] fix(iot-service): Update xml comments for ServiceClient --- iothub/service/src/AmqpServiceClient.cs | 30 +---- iothub/service/src/ServiceClient.cs | 145 ++++++++------------- iothub/service/tests/ServiceClientTests.cs | 17 --- 3 files changed, 60 insertions(+), 132 deletions(-) diff --git a/iothub/service/src/AmqpServiceClient.cs b/iothub/service/src/AmqpServiceClient.cs index b22017be5d..0282b17f07 100644 --- a/iothub/service/src/AmqpServiceClient.cs +++ b/iothub/service/src/AmqpServiceClient.cs @@ -159,12 +159,6 @@ public async override Task SendAsync(string deviceId, Message message, TimeSpan? } } - // This call is executed over HTTP. - public override Task PurgeMessageQueueAsync(string deviceId) - { - return PurgeMessageQueueAsync(deviceId, CancellationToken.None); - } - // This call is executed over HTTP. public override Task PurgeMessageQueueAsync(string deviceId, CancellationToken cancellationToken) { @@ -202,12 +196,6 @@ public override FileNotificationReceiver GetFileNotificationRe return _fileNotificationReceiver; } - // This call is executed over HTTP. - public override Task GetServiceStatisticsAsync() - { - return GetServiceStatisticsAsync(CancellationToken.None); - } - // This call is executed over HTTP. public override Task GetServiceStatisticsAsync(CancellationToken cancellationToken) { @@ -233,12 +221,6 @@ public override Task GetServiceStatisticsAsync(CancellationTo } } - // This call is executed over HTTP. - public override Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod) - { - return InvokeDeviceMethodAsync(deviceId, cloudToDeviceMethod, CancellationToken.None); - } - // This call is executed over HTTP. public override Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod, @@ -277,12 +259,6 @@ private Task InvokeDeviceMethodAsync(Uri uri, } } - // This call is executed over HTTP. - public override Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod) - { - return InvokeDeviceMethodAsync(deviceId, moduleId, cloudToDeviceMethod, CancellationToken.None); - } - // This call is executed over HTTP. public override Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken) { @@ -300,7 +276,7 @@ public override Task InvokeDeviceMethodAsync(string d } // This call is executed over AMQP. - public override async Task SendAsync(string deviceId, string moduleId, Message message) + public override async Task SendAsync(string deviceId, string moduleId, Message message, TimeSpan? timeout = null) { Logging.Enter(this, $"Sending message with Id [{message?.MessageId}] for device {deviceId}, module {moduleId}", nameof(SendAsync)); @@ -329,6 +305,8 @@ public override async Task SendAsync(string deviceId, string moduleId, Message m message.ResetBody(); } + timeout ??= OperationTimeout; + using AmqpMessage amqpMessage = MessageConverter.MessageToAmqpMessage(message); amqpMessage.Properties.To = "/devices/" + WebUtility.UrlEncode(deviceId) + "/modules/" + WebUtility.UrlEncode(moduleId) + "/messages/deviceBound"; try @@ -339,7 +317,7 @@ public override async Task SendAsync(string deviceId, string moduleId, Message m amqpMessage, IotHubConnection.GetNextDeliveryTag(ref _sendingDeliveryTag), AmqpConstants.NullBinary, - OperationTimeout) + timeout.Value) .ConfigureAwait(false); Logging.Info(this, $"Outcome was: {outcome?.DescriptorName}", nameof(SendAsync)); diff --git a/iothub/service/src/ServiceClient.cs b/iothub/service/src/ServiceClient.cs index 6f4f28d503..ebadb0e2d8 100644 --- a/iothub/service/src/ServiceClient.cs +++ b/iothub/service/src/ServiceClient.cs @@ -29,7 +29,8 @@ public enum TransportType #pragma warning restore CA1707 // Identifiers should not contain underscores /// - /// Contains methods that services can use to send messages to devices + /// Contains methods that services can use to send messages to devices/modules, + /// invoke a direct method on a device/module and deliver notifications for file upload and cloud-to-device operations. /// public abstract class ServiceClient : IDisposable { @@ -42,11 +43,11 @@ internal ServiceClient() } /// - /// Create ServiceClient from the specified connection string + /// Create an instance of ServiceClient from the specified IoT Hub connection string. /// - /// Connection string for the IoT Hub - /// The options that allow configuration of the service client instance during initialization. - /// + /// Connection string for the IoT Hub. + /// The that allow configuration of the service client instance during initialization. + /// An instance of ServiceClient. public static ServiceClient CreateFromConnectionString(string connectionString, ServiceClientOptions options = default) { return CreateFromConnectionString(connectionString, TransportType.Amqp, options); @@ -66,25 +67,25 @@ public void Dispose() protected virtual void Dispose(bool disposing) { } /// - /// Create ServiceClient from the specified connection string using specified Transport Type + /// Create an instance of ServiceClient from the specified IoT Hub connection string using specified Transport Type. /// - /// Connection string for the IoT Hub - /// Specifies whether Amqp or Amqp_WebSocket_Only transport is used - /// The options that allow configuration of the service client instance during initialization. - /// + /// Connection string for the IoT Hub. + /// The used (Amqp or Amqp_WebSocket_Only). + /// The that allow configuration of the service client instance during initialization. + /// An instance of ServiceClient. public static ServiceClient CreateFromConnectionString(string connectionString, TransportType transportType, ServiceClientOptions options = default) { return CreateFromConnectionString(connectionString, transportType, new ServiceClientTransportSettings(), options); } /// - /// Create ServiceClient from the specified connection string using specified Transport Type + /// Create an instance of ServiceClient from the specified IoT Hub connection string using specified Transport Type and transport settings. /// - /// Connection string for the IoT Hub - /// Specifies whether Amqp or Amqp_WebSocket_Only transport is used - /// Specifies the AMQP and HTTP proxy settings for Service Client - /// The options that allow configuration of the service client instance during initialization. - /// + /// Connection string for the IoT Hub. + /// The used (Amqp or Amqp_WebSocket_Only). + /// Specifies the AMQP and HTTP proxy settings for Service Client. + /// The that allow configuration of the service client instance during initialization. + /// An instance of ServiceClient. public static ServiceClient CreateFromConnectionString(string connectionString, TransportType transportType, ServiceClientTransportSettings transportSettings, ServiceClientOptions options = default) { if (transportSettings == null) @@ -99,111 +100,77 @@ public static ServiceClient CreateFromConnectionString(string connectionString, } /// - /// Open the ServiceClient instance + /// Open the ServiceClient instance. /// - /// public abstract Task OpenAsync(); /// - /// Close the ServiceClient instance + /// Close the ServiceClient instance. /// - /// public abstract Task CloseAsync(); /// - /// Send a one-way notification to the specified device + /// Send a cloud-to-device message to the specified device. /// - /// The device identifier for the target device - /// The message containing the notification - /// The operation timeout override. If not used uses OperationTimeout default - /// + /// The device identifier for the target device. + /// The cloud-to-device message. + /// The operation timeout, which defaults to 1 minute if unspecified. public abstract Task SendAsync(string deviceId, Message message, TimeSpan? timeout = null); /// - /// Removes all messages from a device's queue. + /// Removes all cloud-to-device messages from a device's queue. /// - /// - /// - public abstract Task PurgeMessageQueueAsync(string deviceId); + /// The device identifier for the target device. + /// A cancellation token to cancel the operation. + public abstract Task PurgeMessageQueueAsync(string deviceId, CancellationToken cancellationToken = default); /// - /// Removes all messages from a device's queue. + /// Get the which can deliver acknowledgments for messages sent to a device/module from IoT Hub. + /// For more information see . /// - /// - /// - /// - public abstract Task PurgeMessageQueueAsync(string deviceId, CancellationToken cancellationToken); - - /// - /// Get the FeedbackReceiver - /// - /// An instance of the FeedbackReceiver + /// An instance of . public abstract FeedbackReceiver GetFeedbackReceiver(); /// - /// Get the FeedbackReceiver + /// Get the which can deliver notifications for file upload operations. + /// For more information see . /// - /// An instance of the FeedbackReceiver + /// An instance of . public abstract FileNotificationReceiver GetFileNotificationReceiver(); /// - /// Gets service statistics for the Iot Hub. - /// - /// returns ServiceStatistics object containing current service statistics - public abstract Task GetServiceStatisticsAsync(); - - /// - /// Gets service statistics for the Iot Hub. - /// - /// - /// The token which allows the the operation to be cancelled. - /// - /// returns ServiceStatistics object containing current service statistics - public abstract Task GetServiceStatisticsAsync(CancellationToken cancellationToken); - - /// - /// Interactively invokes a method on device - /// - /// Device Id - /// Device method parameters (passthrough to device) - /// Method result - public abstract Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod); - - /// - /// Interactively invokes a method on device + /// Gets service statistics for the IoT Hub. /// - /// Device Id - /// Device method parameters (passthrough to device) - /// Cancellation Token - /// Method result - public abstract Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken); + /// A cancellation token to cancel the operation. + /// The service statistics that can be retrieved from IoT Hub, eg. the number of devices connected to the hub. + public abstract Task GetServiceStatisticsAsync(CancellationToken cancellationToken = default); /// - /// Interactively invokes a method on device + /// Interactively invokes a method on a device. /// - /// Device Id - /// Module Id - /// Device method parameters (passthrough to device) - /// Method result - public abstract Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod); + /// The device identifier for the target device. + /// Parameters to execute a direct method on the device. + /// A cancellation token to cancel the operation. + /// The . + public abstract Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken = default); /// - /// Interactively invokes a method on device + /// Interactively invokes a method on a module. /// - /// Device Id - /// Module Id - /// Device method parameters (passthrough to device) - /// Cancellation Token - /// Method result - public abstract Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken); + /// The device identifier for the target device. + /// The module identifier for the target module. + /// Parameters to execute a direct method on the module. + /// A cancellation token to cancel the operation. + /// The . + public abstract Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken = default); /// - /// Send a one-way notification to the specified device module + /// Send a cloud-to-device message to the specified module. /// - /// The device identifier for the target device - /// The module identifier for the target device module - /// The message containing the notification - /// - public abstract Task SendAsync(string deviceId, string moduleId, Message message); + /// The device identifier for the target device. + /// The module identifier for the target module. + /// The cloud-to-module message. + /// The operation timeout, which defaults to 1 minute if unspecified. + public abstract Task SendAsync(string deviceId, string moduleId, Message message, TimeSpan? timeout = null); } } diff --git a/iothub/service/tests/ServiceClientTests.cs b/iothub/service/tests/ServiceClientTests.cs index 4ab46f7ffe..4ba3b86fd1 100644 --- a/iothub/service/tests/ServiceClientTests.cs +++ b/iothub/service/tests/ServiceClientTests.cs @@ -19,23 +19,6 @@ namespace Microsoft.Azure.Devices.Api.Test [TestCategory("Unit")] public class ServiceClientTests { - [TestMethod] - public async Task PurgeMessageQueueTest() - { - // Arrange Moq - Tuple, AmqpServiceClient, PurgeMessageQueueResult> setupParameters = this.SetupPurgeMessageQueueTests(); - Mock restOpMock = setupParameters.Item1; - AmqpServiceClient serviceClient = setupParameters.Item2; - PurgeMessageQueueResult expectedResult = setupParameters.Item3; - - // Execute method under test - PurgeMessageQueueResult result = await serviceClient.PurgeMessageQueueAsync("TestDevice").ConfigureAwait(false); - - // Verify expected result - Assert.AreSame(expectedResult, result); - restOpMock.VerifyAll(); - } - [TestMethod] public async Task PurgeMessageQueueWithCancellationTokenTest() { From 347232f292e6cadbfaaaec5627483b8fbfc7d3f9 Mon Sep 17 00:00:00 2001 From: Abhipsa Misra Date: Thu, 4 Feb 2021 18:08:52 -0800 Subject: [PATCH 10/29] fix(iot-device): Update MqttTransportHandler to not use SemaphoreSlim.WaitAsync(TimeSpan, CancellationToken) --- .../Transport/Mqtt/MqttTransportHandler.cs | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs b/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs index bf1e8971e7..a2b0ef6332 100644 --- a/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs +++ b/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs @@ -281,10 +281,8 @@ public override async Task ReceiveAsync(CancellationToken cancellationT await SubscribeCloudToDeviceMessagesAsync().ConfigureAwait(true); } - bool hasMessage = await ReceiveMessageArrivalAsync(cancellationToken).ConfigureAwait(true); - Message message = ProcessMessage(hasMessage); - - return message; + await WaitUntilC2dMessageArrivesAsync(cancellationToken).ConfigureAwait(false); + return ProcessMessage(); } finally { @@ -321,10 +319,9 @@ public override async Task ReceiveAsync(TimeoutHelper timeoutHelper) TimeSpan timeout = timeoutHelper.GetRemainingTime(); using var cts = new CancellationTokenSource(timeout); - bool hasMessage = await ReceiveMessageArrivalAsync(cts.Token).ConfigureAwait(true); - Message message = ProcessMessage(hasMessage); - return message; + await WaitUntilC2dMessageArrivesAsync(cts.Token).ConfigureAwait(false); + return ProcessMessage(); } finally { @@ -333,28 +330,25 @@ public override async Task ReceiveAsync(TimeoutHelper timeoutHelper) } } - private Message ProcessMessage(bool hasMessage) + private Message ProcessMessage() { Message message = null; try { if (Logging.IsEnabled) - Logging.Enter(this, message, $"hasMessage={hasMessage}", nameof(ProcessMessage)); + Logging.Enter(this, message, $"Will begin processing received C2D message", nameof(ProcessMessage)); - if (hasMessage) + lock (_syncRoot) { - lock (_syncRoot) + if (_messageQueue.TryDequeue(out message)) { - if (_messageQueue.TryDequeue(out message)) + if (_qos == QualityOfService.AtLeastOnce) { - if (_qos == QualityOfService.AtLeastOnce) - { - _completionQueue.Enqueue(message.LockToken); - } - - message.LockToken = _generationId + message.LockToken; + _completionQueue.Enqueue(message.LockToken); } + + message.LockToken = _generationId + message.LockToken; } } @@ -363,11 +357,11 @@ private Message ProcessMessage(bool hasMessage) finally { if (Logging.IsEnabled) - Logging.Exit(this, message, $"hasMessage={hasMessage}", nameof(ProcessMessage)); + Logging.Exit(this, message, $"Processed received C2D message with Id={message?.MessageId}", nameof(ProcessMessage)); } } - private async Task ReceiveMessageArrivalAsync(CancellationToken cancellationToken) + private async Task WaitUntilC2dMessageArrivesAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); CancellationToken disconnectToken = _disconnectAwaitersCancellationSource.Token; @@ -375,8 +369,8 @@ private async Task ReceiveMessageArrivalAsync(CancellationToken cancellati using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, disconnectToken); - // -1 millisecond represents for SemaphoreSlim to wait indefinitely until either of the linked cancellation tokens have been canceled. - return await _receivingSemaphore.WaitAsync(TimeSpan.FromMilliseconds(-1), linkedCts.Token).ConfigureAwait(true); + // Wait until either of the linked cancellation tokens have been canceled. + await _receivingSemaphore.WaitAsync(linkedCts.Token).ConfigureAwait(false); } public override async Task CompleteAsync(string lockToken, CancellationToken cancellationToken) @@ -554,7 +548,7 @@ private async Task HandleIncomingMessagesAsync() if (Logging.IsEnabled) Logging.Enter(this, "Process C2D message via callback", nameof(HandleIncomingMessagesAsync)); - Message message = ProcessMessage(true); + Message message = ProcessMessage(); await (_deviceMessageReceivedListener?.Invoke(message) ?? TaskHelpers.CompletedTask).ConfigureAwait(false); if (Logging.IsEnabled) From f3c9f8bc52edc0456f00bbf8422b91cbffd4b7ea Mon Sep 17 00:00:00 2001 From: Abhipsa Misra Date: Fri, 5 Feb 2021 23:33:01 -0800 Subject: [PATCH 11/29] fix(iot-device): Update dotnetty task calls to use ConfigureAwait(true) --- .../src/Transport/Mqtt/MqttIotHubAdapter.cs | 8 +-- .../Transport/Mqtt/MqttTransportHandler.cs | 49 +++++++++++-------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs b/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs index 71a25e9a72..b3d443359d 100644 --- a/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs +++ b/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs @@ -29,6 +29,8 @@ namespace Microsoft.Azure.Devices.Client.Transport.Mqtt // EventLoop. To limit I/O to the EventLoopGroup and keep Netty semantics, we are going to ensure that the // task continuations are executed by this scheduler using ConfigureAwait(true). // + // All await calls that happen within dotnetty's pipeline should be ConfigureAwait(true). + // internal sealed class MqttIotHubAdapter : ChannelHandlerAdapter { [Flags] @@ -475,7 +477,7 @@ private static async void PingServerAsync(object ctx) Logging.Info(context, $"Idle time was {idleTime}, so ping request was sent.", nameof(PingServerAsync)); // Wait to capture the ping response semaphore, which is released when a PINGRESP packet is received. - bool receivedPingResponse = await s_pingResponseSemaphore.WaitAsync(s_pingResponseTimeout).ConfigureAwait(false); + bool receivedPingResponse = await s_pingResponseSemaphore.WaitAsync(s_pingResponseTimeout).ConfigureAwait(true); if (!receivedPingResponse) { if (Logging.IsEnabled) @@ -1162,7 +1164,7 @@ public async Task ComposePublishPacketAsync(IChannelHandlerContex int length = (int)streamLength; IByteBuffer buffer = context.Channel.Allocator.Buffer(length, length); - await buffer.WriteBytesAsync(payloadStream, length).ConfigureAwait(false); + await buffer.WriteBytesAsync(payloadStream, length).ConfigureAwait(true); Contract.Assert(buffer.ReadableBytes == length); packet.Payload = buffer; @@ -1361,7 +1363,7 @@ public static async Task WriteMessageAsync(IChannelHandlerContext context, objec { try { - await context.WriteAndFlushAsync(message).ConfigureAwait(false); + await context.WriteAndFlushAsync(message).ConfigureAwait(true); } catch (Exception ex) { diff --git a/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs b/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs index a2b0ef6332..bce74b53a1 100644 --- a/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs +++ b/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs @@ -35,6 +35,13 @@ namespace Microsoft.Azure.Devices.Client.Transport.Mqtt { + // + // Note on ConfigureAwait: dotNetty is using a custom TaskScheduler that binds Tasks to the corresponding + // EventLoop. To limit I/O to the EventLoopGroup and keep Netty semantics, we are going to ensure that the + // task continuations are executed by this scheduler using ConfigureAwait(true). + // + // All await calls that happen within dotnetty's pipeline should be ConfigureAwait(true). + // internal sealed class MqttTransportHandler : TransportHandler, IMqttIotHubEventHandler { private const int ProtocolGatewayPort = 8883; @@ -214,7 +221,7 @@ public override async Task OpenAsync(CancellationToken cancellationToken) EnsureValidState(throwIfNotOpen: false); - await OpenInternalAsync(cancellationToken).ConfigureAwait(true); + await OpenInternalAsync(cancellationToken).ConfigureAwait(false); } finally { @@ -235,7 +242,7 @@ public override async Task SendEventAsync(Message message, CancellationToken can EnsureValidState(); Debug.Assert(_channel != null); - await _channel.WriteAndFlushAsync(message).ConfigureAwait(false); + await _channel.WriteAndFlushAsync(message).ConfigureAwait(true); } finally { @@ -249,7 +256,7 @@ public override async Task SendEventAsync(IEnumerable messages, Cancell foreach (Message message in messages) { cancellationToken.ThrowIfCancellationRequested(); - await SendEventAsync(message, cancellationToken).ConfigureAwait(true); + await SendEventAsync(message, cancellationToken).ConfigureAwait(false); } } @@ -278,7 +285,7 @@ public override async Task ReceiveAsync(CancellationToken cancellationT if (State != TransportState.Receiving) { - await SubscribeCloudToDeviceMessagesAsync().ConfigureAwait(true); + await SubscribeCloudToDeviceMessagesAsync().ConfigureAwait(false); } await WaitUntilC2dMessageArrivesAsync(cancellationToken).ConfigureAwait(false); @@ -314,7 +321,7 @@ public override async Task ReceiveAsync(TimeoutHelper timeoutHelper) if (State != TransportState.Receiving) { - await SubscribeCloudToDeviceMessagesAsync().ConfigureAwait(true); + await SubscribeCloudToDeviceMessagesAsync().ConfigureAwait(false); } TimeSpan timeout = timeoutHelper.GetRemainingTime(); @@ -515,7 +522,7 @@ private async Task HandleIncomingTwinPatchAsync(Message message) using var reader = new StreamReader(message.GetBodyStream(), System.Text.Encoding.UTF8); string patch = reader.ReadToEnd(); TwinCollection props = JsonConvert.DeserializeObject(patch); - await Task.Run(() => _onDesiredStatePatchListener(props)).ConfigureAwait(true); + await Task.Run(() => _onDesiredStatePatchListener(props)).ConfigureAwait(false); } } finally @@ -531,7 +538,7 @@ private async Task HandleIncomingMethodPostAsync(Message message) string[] tokens = Regex.Split(message.MqttTopicName, "/", RegexOptions.Compiled, s_regexTimeoutMilliseconds); using var mr = new MethodRequestInternal(tokens[3], tokens[4].Substring(6), message.GetBodyStream(), CancellationToken.None); - await Task.Run(() => _methodListener(mr)).ConfigureAwait(true); + await Task.Run(() => _methodListener(mr)).ConfigureAwait(false); } finally { @@ -576,15 +583,15 @@ public async void OnMessageReceived(Message message) } else if (topic.StartsWith(TwinPatchTopicPrefix, StringComparison.OrdinalIgnoreCase)) { - await HandleIncomingTwinPatchAsync(message).ConfigureAwait(true); + await HandleIncomingTwinPatchAsync(message).ConfigureAwait(false); } else if (topic.StartsWith(MethodPostTopicPrefix, StringComparison.OrdinalIgnoreCase)) { - await HandleIncomingMethodPostAsync(message).ConfigureAwait(true); + await HandleIncomingMethodPostAsync(message).ConfigureAwait(false); } else if (topic.StartsWith(_receiveEventMessagePrefix, StringComparison.OrdinalIgnoreCase)) { - await HandleIncomingEventMessageAsync(message).ConfigureAwait(true); + await HandleIncomingEventMessageAsync(message).ConfigureAwait(false); } else if (topic.StartsWith(_deviceboundMessagePrefix, StringComparison.OrdinalIgnoreCase)) { @@ -639,7 +646,7 @@ private async Task HandleIncomingEventMessageAsync(Message message) } } message.LockToken = _generationId + message.LockToken; - await (_moduleMessageReceivedListener?.Invoke(inputName, message) ?? TaskHelpers.CompletedTask).ConfigureAwait(true); + await (_moduleMessageReceivedListener?.Invoke(inputName, message) ?? TaskHelpers.CompletedTask).ConfigureAwait(false); } finally { @@ -860,7 +867,7 @@ public override async Task SendMethodResponseAsync(MethodResponseInternal method MqttTopicName = MethodResponseTopic.FormatInvariant(methodResponse.Status, methodResponse.RequestId) }; - await SendEventAsync(message, cancellationToken).ConfigureAwait(true); + await SendEventAsync(message, cancellationToken).ConfigureAwait(false); } public override async Task EnableTwinPatchAsync(CancellationToken cancellationToken) @@ -911,7 +918,7 @@ public override async Task SendTwinGetAsync(CancellationToken cancellation // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_017: `SendTwinGetAsync` shall wait for a response from the service with a matching $rid value // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_019: If the response is failed, `SendTwinGetAsync` shall return that failure to the caller. // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_020: If the response doesn't arrive within `MqttTransportHandler.TwinTimeout`, `SendTwinGetAsync` shall fail with a timeout error - using Message response = await SendTwinRequestAsync(request, rid, cancellationToken).ConfigureAwait(true); + using Message response = await SendTwinRequestAsync(request, rid, cancellationToken).ConfigureAwait(false); // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_021: If the response contains a success code, `SendTwinGetAsync` shall return success to the caller // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_018: When a response is received, `SendTwinGetAsync` shall return the Twin object to the caller @@ -956,7 +963,7 @@ public override async Task SendTwinPatchAsync(TwinCollection reportedProperties, // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_028: If the response is failed, `SendTwinPatchAsync` shall return that failure to the caller. // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_029: If the response doesn't arrive within `MqttTransportHandler.TwinTimeout`, `SendTwinPatchAsync` shall fail with a timeout error. // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_030: If the response contains a success code, `SendTwinPatchAsync` shall return success to the caller. - await SendTwinRequestAsync(request, rid, cancellationToken).ConfigureAwait(true); + await SendTwinRequestAsync(request, rid, cancellationToken).ConfigureAwait(false); } private async Task OpenInternalAsync(CancellationToken cancellationToken) @@ -977,7 +984,7 @@ private async Task OpenInternalAsync(CancellationToken cancellationToken) #if NET451 _serverAddresses = Dns.GetHostEntry(_hostName).AddressList; #else - _serverAddresses = await Dns.GetHostAddressesAsync(_hostName).ConfigureAwait(true); + _serverAddresses = await Dns.GetHostAddressesAsync(_hostName).ConfigureAwait(false); #endif } @@ -1022,7 +1029,7 @@ private async Task OpenInternalAsync(CancellationToken cancellationToken) }); } - await _connectCompletion.Task.ConfigureAwait(true); + await _connectCompletion.Task.ConfigureAwait(false); // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_031: `OpenAsync` shall subscribe using the '$iothub/twin/res/#' topic filter await SubscribeTwinResponsesAsync().ConfigureAwait(true); @@ -1072,7 +1079,7 @@ await _channel return; } } - await _subscribeCompletionSource.Task.ConfigureAwait(true); + await _subscribeCompletionSource.Task.ConfigureAwait(false); } private Task SubscribeTwinResponsesAsync() @@ -1139,9 +1146,9 @@ private async Task SendTwinRequestAsync(Message request, string rid, Ca { _twinResponseEvent += onTwinResponse; - await SendEventAsync(request, cancellationToken).ConfigureAwait(true); + await SendEventAsync(request, cancellationToken).ConfigureAwait(false); - await responseReceived.WaitAsync(TwinTimeout, cancellationToken).ConfigureAwait(true); + await responseReceived.WaitAsync(TwinTimeout, cancellationToken).ConfigureAwait(false); if (responseException != null) { @@ -1287,7 +1294,7 @@ private Func> CreateWebSocketChannelFactory(Iot #endif using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - await websocket.ConnectAsync(websocketUri, cts.Token).ConfigureAwait(true); + await websocket.ConnectAsync(websocketUri, cts.Token).ConfigureAwait(false); var clientWebSocketChannel = new ClientWebSocketChannel(null, websocket); clientWebSocketChannel @@ -1301,7 +1308,7 @@ private Func> CreateWebSocketChannelFactory(Iot new LoggingHandler(LogLevel.DEBUG), _mqttIotHubAdapterFactory.Create(this, iotHubConnectionString, settings, productInfo, options)); - await s_eventLoopGroup.Value.RegisterAsync(clientWebSocketChannel).ConfigureAwait(false); + await s_eventLoopGroup.Value.RegisterAsync(clientWebSocketChannel).ConfigureAwait(true); return clientWebSocketChannel; }; From a0af7b7e03fadfc5f6d9c76ee7380eda4a92b7b2 Mon Sep 17 00:00:00 2001 From: Abhipsa Misra Date: Wed, 24 Feb 2021 18:23:54 -0800 Subject: [PATCH 12/29] fix(iot-device): Fix MqttTransportHandler to not await on user supplied C2D callback --- .../device/src/Transport/Mqtt/MqttIotHubAdapter.cs | 2 +- .../src/Transport/Mqtt/MqttTransportHandler.cs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs b/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs index b3d443359d..9f2b2ea51c 100644 --- a/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs +++ b/iothub/device/src/Transport/Mqtt/MqttIotHubAdapter.cs @@ -29,7 +29,7 @@ namespace Microsoft.Azure.Devices.Client.Transport.Mqtt // EventLoop. To limit I/O to the EventLoopGroup and keep Netty semantics, we are going to ensure that the // task continuations are executed by this scheduler using ConfigureAwait(true). // - // All await calls that happen within dotnetty's pipeline should be ConfigureAwait(true). + // All awaited calls that happen within dotnetty's pipeline should be ConfigureAwait(true). // internal sealed class MqttIotHubAdapter : ChannelHandlerAdapter { diff --git a/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs b/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs index bce74b53a1..caaf8959c4 100644 --- a/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs +++ b/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs @@ -40,7 +40,7 @@ namespace Microsoft.Azure.Devices.Client.Transport.Mqtt // EventLoop. To limit I/O to the EventLoopGroup and keep Netty semantics, we are going to ensure that the // task continuations are executed by this scheduler using ConfigureAwait(true). // - // All await calls that happen within dotnetty's pipeline should be ConfigureAwait(true). + // All awaited calls that happen within dotnetty's pipeline should be ConfigureAwait(true). // internal sealed class MqttTransportHandler : TransportHandler, IMqttIotHubEventHandler { @@ -344,7 +344,7 @@ private Message ProcessMessage() try { if (Logging.IsEnabled) - Logging.Enter(this, message, $"Will begin processing received C2D message", nameof(ProcessMessage)); + Logging.Enter(this, message, $"Will begin processing received C2D message, queue size={_messageQueue.Count}", nameof(ProcessMessage)); lock (_syncRoot) { @@ -413,7 +413,7 @@ public override async Task CompleteAsync(string lockToken, CancellationToken can lockToken.Length != actualLockToken.Length + s_generationPrefixLength) { throw new IotHubException( - $"Client must send PUBACK packets in the order in which the corresponding PUBLISH packets were received (QoS 1 messages) per [MQTT-4.6.0-2]. Expected lock token: '{actualLockToken}'; actual lock token: '{lockToken}'.", + $"Client must send PUBACK packets in the order in which the corresponding PUBLISH packets were received (QoS 1 messages) per [MQTT-4.6.0-2]. Expected lock token to end with: '{actualLockToken}'; actual lock token: '{lockToken}'.", isTransient: false); } @@ -556,7 +556,11 @@ private async Task HandleIncomingMessagesAsync() Logging.Enter(this, "Process C2D message via callback", nameof(HandleIncomingMessagesAsync)); Message message = ProcessMessage(); - await (_deviceMessageReceivedListener?.Invoke(message) ?? TaskHelpers.CompletedTask).ConfigureAwait(false); + + // We are intentionally not awaiting _deviceMessageReceivedListener callback. + // This is a user-supplied callback that isn't required to be awaited by us. We can simply invoke it and continue. + _ = _deviceMessageReceivedListener?.Invoke(message); + await TaskHelpers.CompletedTask.ConfigureAwait(false); if (Logging.IsEnabled) Logging.Exit(this, "Process C2D message via callback", nameof(HandleIncomingMessagesAsync)); @@ -759,6 +763,8 @@ public override async Task EnableReceiveMessageAsync(CancellationToken cancellat public override async Task EnsurePendingMessagesAreDeliveredAsync(CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + // If the device connects with a CleanSession flag set to false, we will need to deliver the messages // that were sent before the client had subscribed to the C2D message receive topic. if (_retainMessagesAcrossSessions) From 747f2ac2895d06e5c43f76a14a56b61d01be1b7e Mon Sep 17 00:00:00 2001 From: bikamani <41314966+bikamani@users.noreply.github.com> Date: Fri, 5 Mar 2021 15:38:52 -0800 Subject: [PATCH 13/29] IoTHub Exception for Get and Patch Twin failures (#1815) --- e2e/test/iothub/twin/TwinE2ETests.cs | 10 +++------- .../src/Transport/Amqp/AmqpConnectionHolder.cs | 2 +- .../src/Transport/Amqp/AmqpConnectionPool.cs | 3 ++- .../src/Transport/Amqp/AmqpTransportHandler.cs | 17 +++++++++++++++-- iothub/device/src/Transport/Amqp/AmqpUnit.cs | 8 ++++---- .../src/Transport/Amqp/AmqpUnitManager.cs | 3 ++- .../src/Transport/Amqp/IAmqpUnitManager.cs | 3 ++- .../Transport/AmqpIoT/AmqpIoTReceivingLink.cs | 14 ++++++++------ 8 files changed, 37 insertions(+), 23 deletions(-) diff --git a/e2e/test/iothub/twin/TwinE2ETests.cs b/e2e/test/iothub/twin/TwinE2ETests.cs index 622534243f..bdea63b787 100644 --- a/e2e/test/iothub/twin/TwinE2ETests.cs +++ b/e2e/test/iothub/twin/TwinE2ETests.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Client; +using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.E2ETests.Helpers; using Microsoft.Azure.Devices.Shared; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -366,10 +367,6 @@ await Twin_ClientHandlesRejectionInvalidPropertyNameAsync( .ConfigureAwait(false); } - // These tests worked earlier for Amqp and AmqpWs since it was catching a wrong exception - // To Do: Fix Update reported properties method behavior (breaking change) to wait for response - // and we should be able to enable these tests then. - [Ignore] [LoggedTestMethod] public async Task Twin_ClientHandlesRejectionInvalidPropertyName_Amqp() { @@ -378,7 +375,6 @@ await Twin_ClientHandlesRejectionInvalidPropertyNameAsync( .ConfigureAwait(false); } - [Ignore] [LoggedTestMethod] public async Task Twin_ClientHandlesRejectionInvalidPropertyName_AmqpWs() { @@ -713,12 +709,12 @@ await deviceClient }) .ConfigureAwait(false); } - catch (Exception) + catch (IotHubException) { exceptionThrown = true; } - Assert.IsTrue(exceptionThrown, "Exception was expected, but not thrown."); + Assert.IsTrue(exceptionThrown, "IotHubException was expected for updating reported property with an invalid property name, but was not thrown."); Twin serviceTwin = await registryManager.GetTwinAsync(testDevice.Id).ConfigureAwait(false); Assert.IsFalse(serviceTwin.Properties.Reported.Contains(propName1)); diff --git a/iothub/device/src/Transport/Amqp/AmqpConnectionHolder.cs b/iothub/device/src/Transport/Amqp/AmqpConnectionHolder.cs index bc828df3b1..f3d0eaea15 100644 --- a/iothub/device/src/Transport/Amqp/AmqpConnectionHolder.cs +++ b/iothub/device/src/Transport/Amqp/AmqpConnectionHolder.cs @@ -36,7 +36,7 @@ public AmqpConnectionHolder(DeviceIdentity deviceIdentity) public AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) diff --git a/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs b/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs index cef42a9dae..937c7b9544 100644 --- a/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs +++ b/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Azure.Devices.Shared; using Microsoft.Azure.Devices.Client.Transport.AmqpIoT; +using Microsoft.Azure.Devices.Client.Exceptions; namespace Microsoft.Azure.Devices.Client.Transport.Amqp { @@ -18,7 +19,7 @@ internal class AmqpConnectionPool : IAmqpUnitManager public AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) diff --git a/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs b/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs index 8b662b27ed..6a49418f01 100644 --- a/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs +++ b/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Client.Transport.AmqpIoT; using Microsoft.Azure.Devices.Shared; @@ -400,8 +401,13 @@ private async Task RoundTripTwinMessageAsync(AmqpTwinMessageType amqpTwinM await _amqpUnit.SendTwinMessageAsync(amqpTwinMessageType, correlationId, reportedProperties, _operationTimeout).ConfigureAwait(false); var receivingTask = taskCompletionSource.Task; + if (await Task.WhenAny(receivingTask, Task.Delay(TimeSpan.FromSeconds(ResponseTimeoutInSeconds), cancellationToken)).ConfigureAwait(false) == receivingTask) { + if ((receivingTask.Exception != null) && (receivingTask.Exception.InnerException != null)) + { + throw receivingTask.Exception.InnerException; + } // Task completed within timeout. // Consider that the task may have faulted or been canceled. // We re-await the task so that any exceptions/cancellation is rethrown. @@ -517,7 +523,7 @@ private async Task DisposeMessageAsync(string lockToken, AmqpIoTDisposeActions o #region Helpers - private void TwinMessageListener(Twin twin, string correlationId, TwinCollection twinCollection) + private void TwinMessageListener(Twin twin, string correlationId, TwinCollection twinCollection, IotHubException ex = default) { if (correlationId == null) { @@ -533,7 +539,14 @@ private void TwinMessageListener(Twin twin, string correlationId, TwinCollection TaskCompletionSource task; if (_twinResponseCompletions.TryRemove(correlationId, out task)) { - task.SetResult(twin); + if(ex == default) + { + task.SetResult(twin); + } + else + { + task.SetException(ex); + } } else { diff --git a/iothub/device/src/Transport/Amqp/AmqpUnit.cs b/iothub/device/src/Transport/Amqp/AmqpUnit.cs index 5e9fa58a51..52eaf78310 100644 --- a/iothub/device/src/Transport/Amqp/AmqpUnit.cs +++ b/iothub/device/src/Transport/Amqp/AmqpUnit.cs @@ -19,7 +19,7 @@ internal class AmqpUnit : IDisposable private readonly DeviceIdentity _deviceIdentity; private readonly Func _onMethodCallback; - private readonly Action _twinMessageListener; + private readonly Action _twinMessageListener; private readonly Func _onModuleMessageReceivedCallback; private readonly Func _onDeviceMessageReceivedCallback; private readonly IAmqpConnectionHolder _amqpConnectionHolder; @@ -54,7 +54,7 @@ public AmqpUnit( DeviceIdentity deviceIdentity, IAmqpConnectionHolder amqpConnectionHolder, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) @@ -735,13 +735,13 @@ private async Task OpenTwinSenderLinkAsync(AmqpIoTSession amqpIoTSession, string } } - private void OnDesiredPropertyReceived(Twin twin, string correlationId, TwinCollection twinCollection) + private void OnDesiredPropertyReceived(Twin twin, string correlationId, TwinCollection twinCollection, IotHubException ex = default) { Logging.Enter(this, twin, nameof(OnDesiredPropertyReceived)); try { - _twinMessageListener?.Invoke(twin, correlationId, twinCollection); + _twinMessageListener?.Invoke(twin, correlationId, twinCollection, ex); } finally { diff --git a/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs b/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs index aa0904aff2..ea31509155 100644 --- a/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs +++ b/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Azure.Devices.Shared; using Microsoft.Azure.Devices.Client.Transport.AmqpIoT; +using Microsoft.Azure.Devices.Client.Exceptions; namespace Microsoft.Azure.Devices.Client.Transport.Amqp { @@ -29,7 +30,7 @@ internal static AmqpUnitManager GetInstance() public AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) diff --git a/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs b/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs index 882e2eae6e..3e1f5a1de2 100644 --- a/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs +++ b/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Client.Transport.AmqpIoT; using Microsoft.Azure.Devices.Shared; @@ -13,7 +14,7 @@ internal interface IAmqpUnitManager AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected); diff --git a/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs b/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs index cde5458672..46e64a8e45 100644 --- a/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs +++ b/iothub/device/src/Transport/AmqpIoT/AmqpIoTReceivingLink.cs @@ -23,7 +23,7 @@ internal class AmqpIoTReceivingLink private Action _onEventsReceived; private Action _onDeviceMessageReceived; private Action _onMethodReceived; - private Action _onTwinMessageReceived; + private Action _onTwinMessageReceived; public AmqpIoTReceivingLink(ReceivingAmqpLink receivingAmqpLink) { @@ -255,7 +255,7 @@ private void DisposeDelivery(AmqpMessage amqpMessage, bool settled, Accepted acc #region Twin handling - internal void RegisterTwinListener(Action onDesiredPropertyReceived) + internal void RegisterTwinListener(Action onDesiredPropertyReceived) { _onTwinMessageReceived = onDesiredPropertyReceived; _receivingAmqpLink.RegisterMessageListener(OnTwinChangesReceived); @@ -280,16 +280,18 @@ private void OnTwinChangesReceived(AmqpMessage amqpMessage) if (status >= 400) { // Handle failures - _onTwinMessageReceived.Invoke(null, correlationId, null); - if (correlationId.StartsWith(AmqpTwinMessageType.Get.ToString(), StringComparison.OrdinalIgnoreCase)) + if (correlationId.StartsWith(AmqpTwinMessageType.Get.ToString(), StringComparison.OrdinalIgnoreCase) + || correlationId.StartsWith(AmqpTwinMessageType.Patch.ToString(), StringComparison.OrdinalIgnoreCase)) { string error = null; using (var reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8)) { error = reader.ReadToEnd(); }; + // Retry for Http status code request timeout, Too many requests and server errors - throw new IotHubException(error, status >= 500 || status == 429 || status == 408); + var exception = new IotHubException(error, status >= 500 || status == 429 || status == 408); + _onTwinMessageReceived.Invoke(null, correlationId, null, exception); } } else @@ -325,7 +327,7 @@ private void OnTwinChangesReceived(AmqpMessage amqpMessage) // This shouldn't happen Logging.Info("Received a correlation Id for Twin operation that does not match Get, Patch or Put request", nameof(OnTwinChangesReceived)); } - _onTwinMessageReceived.Invoke(twin, correlationId, twinProperties); + _onTwinMessageReceived.Invoke(twin, correlationId, twinProperties, null); } } finally From 2e71c390e55d2f2dde5e667836598ecdd2943c5e Mon Sep 17 00:00:00 2001 From: jamdavi <73593426+jamdavi@users.noreply.github.com> Date: Tue, 9 Mar 2021 14:57:34 -0500 Subject: [PATCH 14/29] fix(edge): UnixDomainSocketEndPoint is available in .NET 2.1 and greater (#1816) * UnixDomainSocketEndPoint has been standard since 2.1. Allowing later versions to use the correct class The edge client HSM provider uses UnixDomainSockets (UDS) for communication. Before .NET 2.1 to implement a Unix Socket you had to create your own class to do so. Since 2.1 there has been a native UnixDomainSocketEndPoint class in the runtime. In 2.1 and 3.1 there is no issue. However in 5.0 there are some changes to the way the Socket class handles the native UnixDomainSocketEndPoint class. I didn't dig down extremely deep, but I suspect it's due to the way the endpoint handles the SocketAddress and the string manipulation there seeing as how there is a specific implementation for Windows and for Unix. --- .../Transport/HttpUdsMessageHandler.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/iothub/device/src/ModernDotNet/HsmAuthentication/Transport/HttpUdsMessageHandler.cs b/iothub/device/src/ModernDotNet/HsmAuthentication/Transport/HttpUdsMessageHandler.cs index bb68169b85..b72c87d203 100644 --- a/iothub/device/src/ModernDotNet/HsmAuthentication/Transport/HttpUdsMessageHandler.cs +++ b/iothub/device/src/ModernDotNet/HsmAuthentication/Transport/HttpUdsMessageHandler.cs @@ -25,6 +25,7 @@ protected override async Task SendAsync(HttpRequestMessage using var stream = new HttpBufferedStream(new NetworkStream(socket, true)); byte[] requestBytes = HttpRequestResponseSerializer.SerializeRequest(request); + #if NET451 || NET472 || NETSTANDARD2_0 await stream.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken).ConfigureAwait(false); #else @@ -42,10 +43,25 @@ protected override async Task SendAsync(HttpRequestMessage private async Task GetConnectedSocketAsync() { - var endpoint = new UnixDomainSocketEndPoint(_providerUri.LocalPath); Socket socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); - await socket.ConnectAsync(endpoint).ConfigureAwait(false); + // The Edge Agent uses unix sockets for communication with the modules deployed in docker for HSM. + // For netstandard 2.0 there was no implementation for a Unix Domain Socket (UDS) so we used a version + // that was part of a test that was reused in a number of libraries on the internet. + // + // https://github.com/dotnet/corefx/blob/12b51c6bf153cc237b251a4e264d5e7c0ee84a33/src/System.IO.Pipes/src/System/Net/Sockets/UnixDomainSocketEndPoint.cs + // https://github.com/dotnet/corefx/blob/12b51c6bf153cc237b251a4e264d5e7c0ee84a33/src/System.Net.Sockets/tests/FunctionalTests/UnixDomainSocketTest.cs#L248 + // + // Since then the UnixDomainSocketEndpoint has been added to the dotnet framework and there has been considerable work + // around unix sockets in the BCL. For older versions of the framework we will continue to use the existing class since it works + // fine. For netcore 2.1 and greater as well as .NET 5.0 and greater we'll use the native framework version. + +#if NET451 || NET472 || NETSTANDARD2_0 + var endpoint = new Microsoft.Azure.Devices.Client.HsmAuthentication.Transport.UnixDomainSocketEndPoint(_providerUri.LocalPath); +#else + var endpoint = new System.Net.Sockets.UnixDomainSocketEndPoint(_providerUri.LocalPath); +#endif + await socket.ConnectAsync(endpoint).ConfigureAwait(false); return socket; } } From d3748375cd1ce4d433ff968ef64145048b49523b Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Tue, 9 Mar 2021 16:36:11 -0800 Subject: [PATCH 15/29] feat(e2e) - Enabling soft delete when creating keyvaults (#1820) --- e2e/test/prerequisites/E2ETestsSetup/test-resources.bicep | 2 +- e2e/test/prerequisites/E2ETestsSetup/test-resources.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/test/prerequisites/E2ETestsSetup/test-resources.bicep b/e2e/test/prerequisites/E2ETestsSetup/test-resources.bicep index 5ae37cf3e3..ffb3a2c8cf 100644 --- a/e2e/test/prerequisites/E2ETestsSetup/test-resources.bicep +++ b/e2e/test/prerequisites/E2ETestsSetup/test-resources.bicep @@ -163,7 +163,7 @@ resource keyVault 'Microsoft.KeyVault/vaults@2018-02-14' = { name: 'standard' family: 'A' } - enableSoftDelete: false + enableSoftDelete: true networkAcls: { defaultAction: 'Allow' bypass: 'AzureServices' diff --git a/e2e/test/prerequisites/E2ETestsSetup/test-resources.json b/e2e/test/prerequisites/E2ETestsSetup/test-resources.json index 5a135a6798..2e76516351 100644 --- a/e2e/test/prerequisites/E2ETestsSetup/test-resources.json +++ b/e2e/test/prerequisites/E2ETestsSetup/test-resources.json @@ -176,7 +176,7 @@ "name": "standard", "family": "A" }, - "enableSoftDelete": false, + "enableSoftDelete": true, "networkAcls": { "defaultAction": "Allow", "bypass": "AzureServices", @@ -342,7 +342,7 @@ "properties": {} }, { - "type": "Microsoft.Security/IoTSecuritySolutions", + "type": "Microsoft.Security/iotSecuritySolutions", "apiVersion": "2019-08-01", "name": "[parameters('SecuritySolutionName')]", "location": "[resourceGroup().location]", @@ -484,7 +484,7 @@ }, "workspaceId": { "type": "string", - "value": "[format('{0}', reference(resourceId('Microsoft.OperationalInsights/workspaces', parameters('OperationalInsightsName')), '2017-03-15-preview').customerId)]" + "value": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', parameters('OperationalInsightsName')), '2017-03-15-preview').customerId]" }, "customAllocationPolicyWebhook": { "type": "string", From c4b8097a3ce3b6c28673a638bfcc6c68402c9e90 Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Wed, 10 Mar 2021 17:52:07 -0800 Subject: [PATCH 16/29] fix(e2e) - Change event logging to opt in to specific events only. (#1824) --- e2e/test/E2EMsTestBase.cs | 3 +- e2e/test/Helpers/ConsoleEventListener.cs | 78 +++++++----------------- 2 files changed, 23 insertions(+), 58 deletions(-) diff --git a/e2e/test/E2EMsTestBase.cs b/e2e/test/E2EMsTestBase.cs index cc8ed9cb28..9a06aef960 100644 --- a/e2e/test/E2EMsTestBase.cs +++ b/e2e/test/E2EMsTestBase.cs @@ -23,7 +23,6 @@ namespace Microsoft.Azure.Devices.E2ETests /// public class E2EMsTestBase : IDisposable { - private static readonly string[] s_eventProviders = new string[] { "DotNetty-Default", "Microsoft-Azure-", }; private ConsoleEventListener _listener; // Test specific logger instance @@ -41,7 +40,7 @@ public void TestInitialize() // Note: Events take long and increase run time of the test suite, so only using trace. Logger.Trace($"Starting test - {TestContext.TestName}", SeverityLevel.Information); - _listener = new ConsoleEventListener(s_eventProviders); + _listener = new ConsoleEventListener(); } [TestCleanup] diff --git a/e2e/test/Helpers/ConsoleEventListener.cs b/e2e/test/Helpers/ConsoleEventListener.cs index 57dddccf4f..0ec251a538 100644 --- a/e2e/test/Helpers/ConsoleEventListener.cs +++ b/e2e/test/Helpers/ConsoleEventListener.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -8,82 +9,47 @@ namespace System.Diagnostics.Tracing { public sealed class ConsoleEventListener : EventListener { - private readonly string[] _eventFilters; - private readonly object _lock = new object(); - - public ConsoleEventListener(string filter) - { - _eventFilters = new string[1]; - _eventFilters[0] = filter ?? throw new ArgumentNullException(nameof(filter)); - - InitializeEventSources(); - } - - public ConsoleEventListener(string[] filters) - { - _eventFilters = filters ?? throw new ArgumentNullException(nameof(filters)); - if (_eventFilters.Length == 0) - { - throw new ArgumentException("Filters cannot be empty", nameof(filters)); - } - - foreach (string filter in _eventFilters) - { - if (string.IsNullOrWhiteSpace(filter)) - { - throw new ArgumentNullException(nameof(filters)); - } - } + // Configure this value to filter all the necessary events when OnEventSourceCreated is called. + // OnEventSourceCreated is triggered as soon as the EventListener is registered and an event source is created. + // So trying to configure this value in the ConsoleEventListener constructor does not work. + // The OnEventSourceCreated can be triggered sooner than the filter is initialized in the ConsoleEventListener constructor. + private static string[] _eventFilters = new string[] { "DotNetty-Default", "Microsoft-Azure-Devices" }; - InitializeEventSources(); - } - - private void InitializeEventSources() - { - foreach (EventSource source in EventSource.GetSources()) - { - EnableEvents(source, EventLevel.LogAlways); - } - } + private readonly object _lock = new object(); protected override void OnEventSourceCreated(EventSource eventSource) { - base.OnEventSourceCreated(eventSource); - EnableEvents( - eventSource, - EventLevel.LogAlways + if (_eventFilters.Any(filter => eventSource.Name.StartsWith(filter, StringComparison.OrdinalIgnoreCase))) + { + base.OnEventSourceCreated(eventSource); + EnableEvents( + eventSource, + EventLevel.LogAlways #if !NET451 , EventKeywords.All #endif ); + } } protected override void OnEventWritten(EventWrittenEventArgs eventData) { - if (_eventFilters == null) - { - return; - } - lock (_lock) { - if (_eventFilters.Any(ef => eventData.EventSource.Name.StartsWith(ef, StringComparison.Ordinal))) - { - string eventIdent; + string eventIdent; #if NET451 // net451 doesn't have EventName, so we'll settle for EventId eventIdent = eventData.EventId.ToString(CultureInfo.InvariantCulture); #else - eventIdent = eventData.EventName; + eventIdent = eventData.EventName; #endif - string text = $"{DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffffff", CultureInfo.InvariantCulture)} [{eventData.EventSource.Name}-{eventIdent}]{(eventData.Payload != null ? $" ({string.Join(", ", eventData.Payload)})." : "")}"; + string text = $"{DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffffff", CultureInfo.InvariantCulture)} [{eventData.EventSource.Name}-{eventIdent}]{(eventData.Payload != null ? $" ({string.Join(", ", eventData.Payload)})." : "")}"; - ConsoleColor origForeground = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.DarkYellow; - Console.WriteLine(text); - Debug.WriteLine(text); - Console.ForegroundColor = origForeground; - } + ConsoleColor origForeground = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.DarkYellow; + Console.WriteLine(text); + Debug.WriteLine(text); + Console.ForegroundColor = origForeground; } } } From c75e5d48990585fa6430c4f21e21a0aea01bf37a Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Tue, 9 Feb 2021 15:55:37 -0800 Subject: [PATCH 17/29] (service-client): Design for IoT hub AAD authentication --- iothub/service/design.md | 119 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 iothub/service/design.md diff --git a/iothub/service/design.md b/iothub/service/design.md new file mode 100644 index 0000000000..0eb441112e --- /dev/null +++ b/iothub/service/design.md @@ -0,0 +1,119 @@ +# RBAC API Design for IoT hub + +## Service Client + +```csharp +/// +/// Creates a using AAD token credentials and the specified transport type. +/// +/// IoT hub host name. +/// AAD to authenticate with IoT hub. +/// Specifies whether Amqp or Amqp_WebSocket_Only transport is used. +/// Specifies the AMQP_WS and HTTP proxy settings for service client. +/// The options that allow configuration of the service client instance during initialization. +/// An instance of . +public static ServiceClient Create( + string hostName, + TokenCredential credential, + TransportType transportType, + ServiceClientTransportSettings transportSettings = default, + ServiceClientOptions options = default) + +/// +/// Creates a see using SAS token credentials and the specified transport type. +/// +/// IoT hub host name. +/// to authenticate with IoT hub. +/// Specifies whether Amqp or Amqp_WebSocket_Only transport is used. +/// Specifies the AMQP_WS and HTTP proxy settings for service client. +/// The options that allow configuration of the service client instance during initialization. +/// An instance of . +public static ServiceClient Create( + string hostName, + AzureSasCredential credential, + TransportType transportType, + ServiceClientTransportSettings transportSettings = default, + ServiceClientOptions options = default) +``` + +## Registry Manager + +```csharp +/// +/// Creates an instance of . +/// +/// IoT hub host name. +/// AAD to authenticate with IoT hub. +/// The HTTP transport settings. +/// An instance of . +public static RegistryManager Create( + string hostName, + TokenCredential credential, + HttpTransportSettings transportSettings = default) + +/// +/// Creates an instance of . +/// +/// IoT hub host name. +/// to authenticate with IoT hub. +/// The HTTP transport settings. +/// An instance of . +public static RegistryManager Create( + string hostName, + AzureSasCredential credential, + HttpTransportSettings transportSettings = default) +``` + +## Job Client + +```csharp +/// +/// Creates an instance of . +/// +/// IoT hub host name. +/// AAD to authenticate with IoT hub. +/// The HTTP transport settings. +/// An instance of . +public static JobClient Create( + string hostName, + TokenCredential credential, + HttpTransportSettings transportSettings = default) + +/// +/// Creates an instance of . +/// +/// IoT hub host name. +/// to authenticate with IoT hub. +/// The HTTP transport settings. +/// An instance of . +public static JobClient Create( + string hostName, + AzureSasCredential credential, + HttpTransportSettings transportSettings = default) +``` + +## Digital Twin Client + +```csharp +/// +/// Initializes a new instance of the class. +/// IoT hub host name. +/// AAD to authenticate with IoT hub. +/// The delegating handlers to add to the http client pipeline. You can add handlers for tracing, implementing a retry strategy, routing requests through a proxy, etc. +/// An instance of +public static DigitalTwinClient Create( + string hostName, + TokenCredential credential, + HttpTransportSettings transportSettings = default) + +/// +/// Initializes a new instance of the class. +/// IoT hub host name. +/// to authenticate with IoT hub. +/// The delegating handlers to add to the http client pipeline. You can add handlers for tracing, implementing a retry strategy, routing requests through a proxy, etc. +/// An instance of +public static DigitalTwinClient Create( + string hostName, + AzureSasCredential credential, + HttpTransportSettings transportSettings = default) +``` From 0d7b54d952d9946dc2e45b9217a838e17626ed9c Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Wed, 10 Feb 2021 10:10:57 -0800 Subject: [PATCH 18/29] (service-client: Refactor and add implementation for token credential input) (#1781) --- common/src/service/IotHubConnectionString.cs | 62 +++-------------- iothub/service/src/IotHubConnection.cs | 28 ++++---- iothub/service/src/IotHubCredential.cs | 69 +++++++++++++++++++ iothub/service/src/IotHubTokenCredential.cs | 69 +++++++++++++++++++ .../src/Microsoft.Azure.Devices.csproj | 1 + .../ServiceClientConnectionStringTests.cs | 24 +++---- 6 files changed, 175 insertions(+), 78 deletions(-) create mode 100644 iothub/service/src/IotHubCredential.cs create mode 100644 iothub/service/src/IotHubTokenCredential.cs diff --git a/common/src/service/IotHubConnectionString.cs b/common/src/service/IotHubConnectionString.cs index be59119d68..a23c509351 100644 --- a/common/src/service/IotHubConnectionString.cs +++ b/common/src/service/IotHubConnectionString.cs @@ -11,12 +11,14 @@ namespace Microsoft.Azure.Devices { - internal sealed class IotHubConnectionString : IAuthorizationHeaderProvider, ICbsTokenProvider + /// + /// The properties required for authentication to IoT hub using a connection string. + /// + internal sealed class IotHubConnectionString : IotHubCredential { - private static readonly TimeSpan s_defaultTokenTimeToLive = TimeSpan.FromHours(1); - private const char UserSeparator = '@'; + private static readonly TimeSpan _tokenTimeToLive = TimeSpan.FromHours(1); - public IotHubConnectionString(IotHubConnectionStringBuilder builder) + public IotHubConnectionString(IotHubConnectionStringBuilder builder) : base(builder.HostName) { if (builder == null) { @@ -24,26 +26,14 @@ public IotHubConnectionString(IotHubConnectionStringBuilder builder) } Audience = builder.HostName; - HostName = string.IsNullOrEmpty(builder.GatewayHostName) ? builder.HostName : builder.GatewayHostName; SharedAccessKeyName = builder.SharedAccessKeyName; SharedAccessKey = builder.SharedAccessKey; SharedAccessSignature = builder.SharedAccessSignature; - IotHubName = builder.IotHubName; - HttpsEndpoint = new UriBuilder("https", HostName).Uri; - AmqpEndpoint = new UriBuilder(CommonConstants.AmqpsScheme, builder.HostName, AmqpConstants.DefaultSecurePort).Uri; DeviceId = builder.DeviceId; ModuleId = builder.ModuleId; GatewayHostName = builder.GatewayHostName; } - public string IotHubName { get; private set; } - - public string HostName { get; private set; } - - public Uri HttpsEndpoint { get; private set; } - - public Uri AmqpEndpoint { get; private set; } - public string Audience { get; private set; } public string SharedAccessKeyName { get; private set; } @@ -58,39 +48,17 @@ public IotHubConnectionString(IotHubConnectionStringBuilder builder) public string GatewayHostName { get; private set; } - public string GetUser() - { - var stringBuilder = new StringBuilder(); - stringBuilder.Append(SharedAccessKeyName); - stringBuilder.Append(UserSeparator); - stringBuilder.Append("sas."); - stringBuilder.Append("root."); - stringBuilder.Append(IotHubName); - - return stringBuilder.ToString(); - } - public string GetPassword() { - string password; - if (string.IsNullOrWhiteSpace(SharedAccessSignature)) - { - password = BuildToken(out _); - } - else - { - password = SharedAccessSignature; - } - - return password; + return string.IsNullOrWhiteSpace(SharedAccessSignature) ? BuildToken(out _) : SharedAccessSignature; } - public string GetAuthorizationHeader() + public override string GetAuthorizationHeader() { return GetPassword(); } - Task ICbsTokenProvider.GetTokenAsync(Uri namespaceAddress, string appliesTo, string[] requiredClaims) + public override Task GetTokenAsync(Uri namespaceAddress, string appliesTo, string[] requiredClaims) { string tokenValue; CbsToken token; @@ -108,16 +76,6 @@ Task ICbsTokenProvider.GetTokenAsync(Uri namespaceAddress, string appl return Task.FromResult(token); } - public Uri BuildLinkAddress(string path) - { - var builder = new UriBuilder(AmqpEndpoint) - { - Path = path, - }; - - return builder.Uri; - } - public static IotHubConnectionString Parse(string connectionString) { var builder = IotHubConnectionStringBuilder.Create(connectionString); @@ -130,7 +88,7 @@ private string BuildToken(out TimeSpan ttl) { KeyName = SharedAccessKeyName, Key = SharedAccessKey, - TimeToLive = s_defaultTokenTimeToLive, + TimeToLive = _tokenTimeToLive, Target = Audience }; diff --git a/iothub/service/src/IotHubConnection.cs b/iothub/service/src/IotHubConnection.cs index 8e7cebf434..5a17e2ede5 100644 --- a/iothub/service/src/IotHubConnection.cs +++ b/iothub/service/src/IotHubConnection.cs @@ -51,7 +51,7 @@ internal sealed class IotHubConnection : IDisposable private IOThreadTimer _refreshTokenTimer; #endif - public IotHubConnection(IotHubConnectionString connectionString, AccessRights accessRights, bool useWebSocketOnly, ServiceClientTransportSettings transportSettings) + public IotHubConnection(IotHubCredential credential, AccessRights accessRights, bool useWebSocketOnly, ServiceClientTransportSettings transportSettings) { #if !NET451 _refreshTokenTimer = new IOThreadTimerSlim(s => ((IotHubConnection)s).OnRefreshTokenAsync(), this); @@ -59,7 +59,7 @@ public IotHubConnection(IotHubConnectionString connectionString, AccessRights ac _refreshTokenTimer = new IOThreadTimer(s => ((IotHubConnection)s).OnRefreshTokenAsync(), this, false); #endif - ConnectionString = connectionString; + Credential = credential; _accessRights = accessRights; _faultTolerantSession = new FaultTolerantAmqpObject(CreateSessionAsync, CloseConnection); _useWebSocketOnly = useWebSocketOnly; @@ -71,7 +71,7 @@ internal IotHubConnection(Func> onCreate, Action(onCreate, onClose); } - internal IotHubConnectionString ConnectionString { get; private set; } + internal IotHubCredential Credential { get; private set; } public Task OpenAsync(TimeSpan timeout) { @@ -114,7 +114,7 @@ public async Task CreateSendingLinkAsync(string path, TimeSpan session = await _faultTolerantSession.GetOrCreateAsync(timeoutHelper.RemainingTime()).ConfigureAwait(false); } - Uri linkAddress = ConnectionString.BuildLinkAddress(path); + Uri linkAddress = Credential.BuildLinkAddress(path); var linkSettings = new AmqpLinkSettings { @@ -156,7 +156,7 @@ public async Task CreateReceivingLinkAsync(string path, TimeS session = await _faultTolerantSession.GetOrCreateAsync(timeoutHelper.RemainingTime()).ConfigureAwait(false); } - Uri linkAddress = ConnectionString.BuildLinkAddress(path); + Uri linkAddress = Credential.BuildLinkAddress(path); var linkSettings = new AmqpLinkSettings { @@ -306,7 +306,7 @@ private async Task CreateSessionAsync(TimeSpan timeout) { MaxFrameSize = AmqpConstants.DefaultMaxFrameSize, ContainerId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), // Use a human readable link name to help with debugging - HostName = ConnectionString.AmqpEndpoint.Host, + HostName = Credential.AmqpEndpoint.Host, }; var amqpConnection = new AmqpConnection(transport, amqpSettings, amqpConnectionSettings); @@ -430,7 +430,7 @@ private async Task CreateClientWebSocketTransportAsync(TimeSpan t try { var timeoutHelper = new TimeoutHelper(timeout); - var websocketUri = new Uri($"{ WebSocketConstants.Scheme }{ ConnectionString.HostName}:{ WebSocketConstants.SecurePort}{WebSocketConstants.UriSuffix}"); + var websocketUri = new Uri($"{ WebSocketConstants.Scheme }{ Credential.HostName}:{ WebSocketConstants.SecurePort}{WebSocketConstants.UriSuffix}"); Logging.Info(this, websocketUri, nameof(CreateClientWebSocketTransportAsync)); @@ -494,13 +494,13 @@ private TlsTransportSettings CreateTlsTransportSettings() { var tcpTransportSettings = new TcpTransportSettings { - Host = ConnectionString.HostName, - Port = ConnectionString.AmqpEndpoint.Port, + Host = Credential.HostName, + Port = Credential.AmqpEndpoint.Port, }; var tlsTransportSettings = new TlsTransportSettings(tcpTransportSettings) { - TargetHost = ConnectionString.HostName, + TargetHost = Credential.HostName, Certificate = null, // TODO: add client cert support CertificateValidationCallback = OnRemoteCertificateValidation }; @@ -545,12 +545,12 @@ private async Task SendCbsTokenAsync(AmqpCbsLink cbsLink, TimeSpan timeout) { Logging.Enter(this, cbsLink, timeout, nameof(SendCbsTokenAsync)); - string audience = ConnectionString.AmqpEndpoint.AbsoluteUri; - string resource = ConnectionString.AmqpEndpoint.AbsoluteUri; + string audience = Credential.AmqpEndpoint.AbsoluteUri; + string resource = Credential.AmqpEndpoint.AbsoluteUri; DateTime expiresAtUtc = await cbsLink .SendTokenAsync( - ConnectionString, - ConnectionString.AmqpEndpoint, + Credential, + Credential.AmqpEndpoint, audience, resource, AccessRightsHelper.AccessRightsToStringArray(_accessRights), diff --git a/iothub/service/src/IotHubCredential.cs b/iothub/service/src/IotHubCredential.cs new file mode 100644 index 0000000000..42ec547ae7 --- /dev/null +++ b/iothub/service/src/IotHubCredential.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Azure.Amqp; +using Microsoft.Azure.Devices.Common; + +namespace Microsoft.Azure.Devices +{ + /// + /// The properties required for authentication to IoT hub that are independent of the authentication type. + /// + internal abstract class IotHubCredential + : IAuthorizationHeaderProvider, ICbsTokenProvider + { + private const string HostNameSeparator = "."; + private const string HttpsEndpointPrefix = "https"; + + // Azure.Core (used in IotHubTokenCredential) is not available in NET451. + // So we need this constructor for the build to pass. + protected IotHubCredential() + { + } + + protected IotHubCredential(string hostName) + { + HostName = hostName; + IotHubName = GetIotHubName(hostName); + AmqpEndpoint = new UriBuilder(CommonConstants.AmqpsScheme, HostName, AmqpConstants.DefaultSecurePort).Uri; + HttpsEndpoint = new UriBuilder(HttpsEndpointPrefix, HostName).Uri; + } + + public string IotHubName { get; protected set; } + + public string HostName { get; protected set; } + + public Uri HttpsEndpoint { get; protected set; } + + public Uri AmqpEndpoint { get; protected set; } + + public abstract string GetAuthorizationHeader(); + + public abstract Task GetTokenAsync(Uri namespaceAddress, string appliesTo, string[] requiredClaims); + + public Uri BuildLinkAddress(string path) + { + var builder = new UriBuilder(AmqpEndpoint) + { + Path = path, + }; + + return builder.Uri; + } + + private static string GetIotHubName(string hostName) + { + if (string.IsNullOrWhiteSpace(hostName)) + { + throw new ArgumentNullException($"{nameof(hostName)} is null or empty."); + } + + int index = hostName.IndexOf(HostNameSeparator, StringComparison.OrdinalIgnoreCase); + string iotHubName = index >= 0 ? hostName.Substring(0, index) : hostName; + return iotHubName; + } + } +} diff --git a/iothub/service/src/IotHubTokenCredential.cs b/iothub/service/src/IotHubTokenCredential.cs new file mode 100644 index 0000000000..bb2c1bdde5 --- /dev/null +++ b/iothub/service/src/IotHubTokenCredential.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Azure.Amqp; +using System.Threading; + +#if !NET451 + +using Azure.Core; + +#endif + +namespace Microsoft.Azure.Devices +{ + /// + /// The properties required for authentication to IoT hub using a token credential. + /// + internal class IotHubTokenCredential : IotHubCredential + { +#if NET451 + + public IotHubTokenCredential() + { + throw new InvalidOperationException("nameof(TokenCredential) is not supported in NET451"); + } +#else + private const string _tokenType = "jwt"; + private readonly TokenCredential _credential; + + public IotHubTokenCredential(string hostName, TokenCredential credential) : base(hostName) + { + _credential = credential; + } + +#endif + + public override string GetAuthorizationHeader() + { +#if NET451 + throw new InvalidOperationException($"{nameof(GetAuthorizationHeader)} is not supported on NET451"); + +#else + AccessToken token = _credential.GetToken(new TokenRequestContext(), new CancellationToken()); + return $"Bearer {token.Token}"; + +#endif + } + +#pragma warning disable CS1998 // Disabled as we need to throw exception for NET 451. + + public async override Task GetTokenAsync(Uri namespaceAddress, string appliesTo, string[] requiredClaims) + { +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously +#if NET451 + throw new InvalidOperationException($"{nameof(GetTokenAsync)} is not supported on NET451"); + +#else + AccessToken token = await _credential.GetTokenAsync(new TokenRequestContext(), new CancellationToken()).ConfigureAwait(false); + return new CbsToken( + token.Token, + _tokenType, + token.ExpiresOn.UtcDateTime); +#endif + } + } +} diff --git a/iothub/service/src/Microsoft.Azure.Devices.csproj b/iothub/service/src/Microsoft.Azure.Devices.csproj index 67c8b026a0..d3498902cb 100644 --- a/iothub/service/src/Microsoft.Azure.Devices.csproj +++ b/iothub/service/src/Microsoft.Azure.Devices.csproj @@ -161,6 +161,7 @@ + diff --git a/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs b/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs index 1607a1a6a9..c542f86aea 100644 --- a/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs +++ b/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.Azure.Devices.Api.Test.ConnectionString [TestCategory("Unit")] public class ServiceClientConnectionStringTests { - class TestAuthenticationMethod : IAuthenticationMethod + private class TestAuthenticationMethod : IAuthenticationMethod { public virtual IotHubConnectionStringBuilder Populate(IotHubConnectionStringBuilder iotHubConnectionStringBuilder) { @@ -29,7 +29,7 @@ public void ServiceClientConnectionStringDefaultScopeDefaultCredentialTypeTest() var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); - Assert.IsNotNull(serviceClient.Connection.ConnectionString); + Assert.IsNotNull(serviceClient.Connection.Credential); } [TestMethod] @@ -39,7 +39,7 @@ public void ServiceClientConnectionStringIotHubScopeImplicitSharedAccessSignatur var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); - Assert.IsNotNull(serviceClient.Connection.ConnectionString); + Assert.IsNotNull(serviceClient.Connection.Credential); } [TestMethod] @@ -49,7 +49,7 @@ public void ServiceClientConnectionStringIotHubScopeExplicitSharedAccessSignatur var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); - Assert.IsNotNull(serviceClient.Connection.ConnectionString); + Assert.IsNotNull(serviceClient.Connection.Credential); } [TestMethod] @@ -59,7 +59,7 @@ public void ServiceClientConnectionStringIotHubScopeSharedAccessKeyCredentialTyp var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); - Assert.IsNotNull(serviceClient.Connection.ConnectionString); + Assert.IsNotNull(serviceClient.Connection.Credential); } [TestMethod] @@ -69,7 +69,7 @@ public void ServiceClientConnectionStringDeviceScopeImplicitSharedAccessSignatur var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); - Assert.IsNotNull(serviceClient.Connection.ConnectionString); + Assert.IsNotNull(serviceClient.Connection.Credential); } [TestMethod] @@ -79,7 +79,7 @@ public void ServiceClientConnectionStringDeviceScopeExplicitSharedAccessSignatur var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); - Assert.IsNotNull(serviceClient.Connection.ConnectionString); + Assert.IsNotNull(serviceClient.Connection.Credential); } [TestMethod] @@ -89,7 +89,7 @@ public void ServiceClientConnectionStringDeviceScopeSharedAccessKeyCredentialTyp var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); - Assert.IsNotNull(serviceClient.Connection.ConnectionString); + Assert.IsNotNull(serviceClient.Connection.Credential); } [TestMethod] @@ -117,7 +117,7 @@ public void ServiceClientIotHubConnectionStringBuilderTest() // Hostname without DNS is acceptable for localhost testing. iotHubConnectionStringBuilder.HostName = "adshgfvyregferuehfiuehr"; - + try { iotHubConnectionStringBuilder.HostName = "acme.azure-devices.net"; @@ -181,11 +181,11 @@ public void ServiceClient_ConnectionString_ModuleIdentity_SharedAccessKeyCredent string connectionString = "HostName=testhub.azure-devices-int.net;DeviceId=edgecapabledevice1;ModuleId=testModule;SharedAccessKey=dGVzdFN0cmluZzE=;GatewayHostName=edgehub1.ms.com"; var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); - Assert.IsNotNull(serviceClient.Connection); - IotHubConnectionString iotHubConnectionString = serviceClient.Connection.ConnectionString; + Assert.IsNotNull(serviceClient.Connection); + IotHubConnectionString iotHubConnectionString = (IotHubConnectionString)serviceClient.Connection.Credential; Assert.IsNotNull(iotHubConnectionString); Assert.AreEqual("testhub.azure-devices-int.net", iotHubConnectionString.Audience); - Assert.AreEqual("edgehub1.ms.com", iotHubConnectionString.HostName); + Assert.AreEqual("testhub.azure-devices-int.net", iotHubConnectionString.HostName); Assert.AreEqual("edgecapabledevice1", iotHubConnectionString.DeviceId); Assert.AreEqual("testModule", iotHubConnectionString.ModuleId); Assert.AreEqual("dGVzdFN0cmluZzE=", iotHubConnectionString.SharedAccessKey); From cbc9b3ee0e47617b91b1b2810380d42737d7d585 Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Wed, 10 Feb 2021 16:12:41 -0800 Subject: [PATCH 19/29] (service-client): Refactor and add sas credential (#1786) --- common/src/service/IotHubConnectionString.cs | 21 +------ iothub/service/src/IoTHubSasCredential.cs | 48 +++++++++++++++ iothub/service/src/IotHubConnection.cs | 4 +- ...ntial.cs => IotHubConnectionProperties.cs} | 13 ++-- .../src/IotHubSasCredentialProperties.cs | 59 +++++++++++++++++++ ....cs => IotHubTokenCredentialProperties.cs} | 20 ++++--- .../ServiceClientConnectionStringTests.cs | 3 - .../tests/IotHubConnectionPropertiesTests.cs | 30 ++++++++++ 8 files changed, 163 insertions(+), 35 deletions(-) create mode 100644 iothub/service/src/IoTHubSasCredential.cs rename iothub/service/src/{IotHubCredential.cs => IotHubConnectionProperties.cs} (85%) create mode 100644 iothub/service/src/IotHubSasCredentialProperties.cs rename iothub/service/src/{IotHubTokenCredential.cs => IotHubTokenCredentialProperties.cs} (74%) create mode 100644 iothub/service/tests/IotHubConnectionPropertiesTests.cs diff --git a/common/src/service/IotHubConnectionString.cs b/common/src/service/IotHubConnectionString.cs index a23c509351..2ea31ebf95 100644 --- a/common/src/service/IotHubConnectionString.cs +++ b/common/src/service/IotHubConnectionString.cs @@ -14,11 +14,12 @@ namespace Microsoft.Azure.Devices /// /// The properties required for authentication to IoT hub using a connection string. /// - internal sealed class IotHubConnectionString : IotHubCredential + internal sealed class IotHubConnectionString + : IotHubConnectionProperties { private static readonly TimeSpan _tokenTimeToLive = TimeSpan.FromHours(1); - public IotHubConnectionString(IotHubConnectionStringBuilder builder) : base(builder.HostName) + public IotHubConnectionString(IotHubConnectionStringBuilder builder) : base(builder?.HostName) { if (builder == null) { @@ -29,9 +30,6 @@ public IotHubConnectionString(IotHubConnectionStringBuilder builder) : base(buil SharedAccessKeyName = builder.SharedAccessKeyName; SharedAccessKey = builder.SharedAccessKey; SharedAccessSignature = builder.SharedAccessSignature; - DeviceId = builder.DeviceId; - ModuleId = builder.ModuleId; - GatewayHostName = builder.GatewayHostName; } public string Audience { get; private set; } @@ -42,12 +40,6 @@ public IotHubConnectionString(IotHubConnectionStringBuilder builder) : base(buil public string SharedAccessSignature { get; private set; } - public string DeviceId { get; private set; } - - public string ModuleId { get; private set; } - - public string GatewayHostName { get; private set; } - public string GetPassword() { return string.IsNullOrWhiteSpace(SharedAccessSignature) ? BuildToken(out _) : SharedAccessSignature; @@ -92,13 +84,6 @@ private string BuildToken(out TimeSpan ttl) Target = Audience }; - if (DeviceId != null) - { - builder.Target = string.IsNullOrEmpty(ModuleId) - ? "{0}/devices/{1}".FormatInvariant(Audience, WebUtility.UrlEncode(DeviceId)) - : "{0}/devices/{1}/modules/{2}".FormatInvariant(Audience, WebUtility.UrlEncode(DeviceId), WebUtility.UrlEncode(ModuleId)); - } - ttl = builder.TimeToLive; return builder.ToSignature(); diff --git a/iothub/service/src/IoTHubSasCredential.cs b/iothub/service/src/IoTHubSasCredential.cs new file mode 100644 index 0000000000..a21179f98b --- /dev/null +++ b/iothub/service/src/IoTHubSasCredential.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; + +#if !NET451 + +using Azure; + +namespace Microsoft.Azure.Devices +{ + /// + /// Shared access signature credential used to authenticate with IoT hub. + /// + public class IotHubSasCredential + : AzureSasCredential + { + /// + /// Creates an instance of . + /// + /// Shared access signature used to authenticate with IoT hub. + /// The shared access signature expiry in UTC. + public IotHubSasCredential(string signature, DateTime expiresOnUtc) : base(signature) + { + ExpiresOnUtc = expiresOnUtc; + } + + /// + /// The shared access signature expiry in UTC. + /// + public DateTime ExpiresOnUtc { get; private set; } + + /// + /// Updates the shared access signature. This is intended to be used when you've + /// regenerated your shared access signature and want to update clients. + /// + /// Shared access signature used to authenticate with IoT hub. + /// The shared access signature expiry in UTC. + public void Update(string signature, DateTime expiresOnUtc) + { + Update(signature); + ExpiresOnUtc = expiresOnUtc; + } + } +} + +#endif diff --git a/iothub/service/src/IotHubConnection.cs b/iothub/service/src/IotHubConnection.cs index 5a17e2ede5..645f634345 100644 --- a/iothub/service/src/IotHubConnection.cs +++ b/iothub/service/src/IotHubConnection.cs @@ -51,7 +51,7 @@ internal sealed class IotHubConnection : IDisposable private IOThreadTimer _refreshTokenTimer; #endif - public IotHubConnection(IotHubCredential credential, AccessRights accessRights, bool useWebSocketOnly, ServiceClientTransportSettings transportSettings) + public IotHubConnection(IotHubConnectionProperties credential, AccessRights accessRights, bool useWebSocketOnly, ServiceClientTransportSettings transportSettings) { #if !NET451 _refreshTokenTimer = new IOThreadTimerSlim(s => ((IotHubConnection)s).OnRefreshTokenAsync(), this); @@ -71,7 +71,7 @@ internal IotHubConnection(Func> onCreate, Action(onCreate, onClose); } - internal IotHubCredential Credential { get; private set; } + internal IotHubConnectionProperties Credential { get; private set; } public Task OpenAsync(TimeSpan timeout) { diff --git a/iothub/service/src/IotHubCredential.cs b/iothub/service/src/IotHubConnectionProperties.cs similarity index 85% rename from iothub/service/src/IotHubCredential.cs rename to iothub/service/src/IotHubConnectionProperties.cs index 42ec547ae7..572b326ee4 100644 --- a/iothub/service/src/IotHubCredential.cs +++ b/iothub/service/src/IotHubConnectionProperties.cs @@ -12,7 +12,7 @@ namespace Microsoft.Azure.Devices /// /// The properties required for authentication to IoT hub that are independent of the authentication type. /// - internal abstract class IotHubCredential + internal abstract class IotHubConnectionProperties : IAuthorizationHeaderProvider, ICbsTokenProvider { private const string HostNameSeparator = "."; @@ -20,12 +20,17 @@ internal abstract class IotHubCredential // Azure.Core (used in IotHubTokenCredential) is not available in NET451. // So we need this constructor for the build to pass. - protected IotHubCredential() + protected IotHubConnectionProperties() { } - protected IotHubCredential(string hostName) + protected IotHubConnectionProperties(string hostName) { + if (string.IsNullOrWhiteSpace(hostName)) + { + throw new ArgumentNullException(nameof(hostName)); + } + HostName = hostName; IotHubName = GetIotHubName(hostName); AmqpEndpoint = new UriBuilder(CommonConstants.AmqpsScheme, HostName, AmqpConstants.DefaultSecurePort).Uri; @@ -54,7 +59,7 @@ public Uri BuildLinkAddress(string path) return builder.Uri; } - private static string GetIotHubName(string hostName) + internal static string GetIotHubName(string hostName) { if (string.IsNullOrWhiteSpace(hostName)) { diff --git a/iothub/service/src/IotHubSasCredentialProperties.cs b/iothub/service/src/IotHubSasCredentialProperties.cs new file mode 100644 index 0000000000..44b37bf9c8 --- /dev/null +++ b/iothub/service/src/IotHubSasCredentialProperties.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Azure.Amqp; + +#if !NET451 + +using Azure; + +#endif + +namespace Microsoft.Azure.Devices +{ + internal class IotHubSasCredentialProperties : IotHubConnectionProperties + { +#if NET451 + + public IotHubSasCredentialProperties() + { + throw new InvalidOperationException("IotHubSasCredential is not supported on NET451"); + } +#else + private readonly IotHubSasCredential _credential; + + public IotHubSasCredentialProperties(string hostName, IotHubSasCredential credential) : base(hostName) + { + _credential = credential; + } + +#endif + + public override string GetAuthorizationHeader() + { +#if NET451 + throw new InvalidOperationException($"IotHubSasCredential is not supported on NET451"); + +#else + return _credential.Signature; +#endif + } + + public override Task GetTokenAsync(Uri namespaceAddress, string appliesTo, string[] requiredClaims) + { +#if NET451 + throw new InvalidOperationException($"IotHubSasCredential is not supported on NET451"); + +#else + var token = new CbsToken( + _credential.Signature, + CbsConstants.IotHubSasTokenType, + _credential.ExpiresOnUtc); + return Task.FromResult(token); +#endif + } + } +} diff --git a/iothub/service/src/IotHubTokenCredential.cs b/iothub/service/src/IotHubTokenCredentialProperties.cs similarity index 74% rename from iothub/service/src/IotHubTokenCredential.cs rename to iothub/service/src/IotHubTokenCredentialProperties.cs index bb2c1bdde5..e0b4da50a7 100644 --- a/iothub/service/src/IotHubTokenCredential.cs +++ b/iothub/service/src/IotHubTokenCredentialProperties.cs @@ -18,19 +18,23 @@ namespace Microsoft.Azure.Devices /// /// The properties required for authentication to IoT hub using a token credential. /// - internal class IotHubTokenCredential : IotHubCredential + internal class IotHubTokenCrendentialProperties + : IotHubConnectionProperties { +#if !NET451 + private const string _tokenType = "jwt"; + private readonly TokenCredential _credential; +#endif + #if NET451 - public IotHubTokenCredential() + public IotHubTokenCrendentialProperties() { - throw new InvalidOperationException("nameof(TokenCredential) is not supported in NET451"); + throw new InvalidOperationException("TokenCredential is not supported on NET451"); } #else - private const string _tokenType = "jwt"; - private readonly TokenCredential _credential; - public IotHubTokenCredential(string hostName, TokenCredential credential) : base(hostName) + public IotHubTokenCrendentialProperties(string hostName, TokenCredential credential) : base(hostName) { _credential = credential; } @@ -40,7 +44,7 @@ public IotHubTokenCredential(string hostName, TokenCredential credential) : base public override string GetAuthorizationHeader() { #if NET451 - throw new InvalidOperationException($"{nameof(GetAuthorizationHeader)} is not supported on NET451"); + throw new InvalidOperationException($"TokenCredential is not supported on NET451"); #else AccessToken token = _credential.GetToken(new TokenRequestContext(), new CancellationToken()); @@ -55,7 +59,7 @@ public async override Task GetTokenAsync(Uri namespaceAddress, string { #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously #if NET451 - throw new InvalidOperationException($"{nameof(GetTokenAsync)} is not supported on NET451"); + throw new InvalidOperationException($"TokenCredential is not supported on NET451"); #else AccessToken token = await _credential.GetTokenAsync(new TokenRequestContext(), new CancellationToken()).ConfigureAwait(false); diff --git a/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs b/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs index c542f86aea..0feae6597f 100644 --- a/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs +++ b/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs @@ -186,10 +186,7 @@ public void ServiceClient_ConnectionString_ModuleIdentity_SharedAccessKeyCredent Assert.IsNotNull(iotHubConnectionString); Assert.AreEqual("testhub.azure-devices-int.net", iotHubConnectionString.Audience); Assert.AreEqual("testhub.azure-devices-int.net", iotHubConnectionString.HostName); - Assert.AreEqual("edgecapabledevice1", iotHubConnectionString.DeviceId); - Assert.AreEqual("testModule", iotHubConnectionString.ModuleId); Assert.AreEqual("dGVzdFN0cmluZzE=", iotHubConnectionString.SharedAccessKey); - Assert.AreEqual("edgehub1.ms.com", iotHubConnectionString.GatewayHostName); Assert.IsNotNull(iotHubConnectionString.GetPassword()); } } diff --git a/iothub/service/tests/IotHubConnectionPropertiesTests.cs b/iothub/service/tests/IotHubConnectionPropertiesTests.cs new file mode 100644 index 0000000000..3ba9c652e6 --- /dev/null +++ b/iothub/service/tests/IotHubConnectionPropertiesTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Azure.Devices.Tests +{ + [TestClass] + [TestCategory("Unit")] + public class IotHubConnectionPropertiesTests + { + [TestMethod] + [DataRow("acme.azure-devices.net", "acme")] + [DataRow("Acme-1.azure-devices.net", "Acme-1")] + [DataRow("acme2.azure-devices.net", "acme2")] + [DataRow("3acme.azure-devices.net", "3acme")] + [DataRow("4-acme.azure-devices.net", "4-acme")] + public void IotHubConnectionPropertiesGetHubNameTest(string hostName, string expectedHubName) + { + // act + string hubName = IotHubConnectionProperties.GetIotHubName(hostName); + + // assert + hubName.Should().Be(expectedHubName); + } + } +} From eb4ba7e93dc440599b279f119cd0c6cf81a5f98c Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Wed, 10 Feb 2021 20:14:18 -0800 Subject: [PATCH 20/29] (service-client): Add constructors in service client to accept aad and sas tokens. (#1787) --- iothub/service/src/AmqpServiceClient.cs | 17 ++--- iothub/service/src/ServiceClient.cs | 83 +++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/iothub/service/src/AmqpServiceClient.cs b/iothub/service/src/AmqpServiceClient.cs index 0282b17f07..26ca335659 100644 --- a/iothub/service/src/AmqpServiceClient.cs +++ b/iothub/service/src/AmqpServiceClient.cs @@ -24,11 +24,11 @@ internal sealed class AmqpServiceClient : ServiceClient private const string PurgeMessageQueueFormat = "/devices/{0}/commands?" + ClientApiVersionHelper.ApiVersionQueryString; private const string DeviceMethodUriFormat = "/twins/{0}/methods?" + ClientApiVersionHelper.ApiVersionQueryString; private const string ModuleMethodUriFormat = "/twins/{0}/modules/{1}/methods?" + ClientApiVersionHelper.ApiVersionQueryString; + private const string _sendingPath = "/messages/deviceBound"; private static readonly TimeSpan s_defaultOperationTimeout = TimeSpan.FromSeconds(100); private readonly FaultTolerantAmqpObject _faultTolerantSendingLink; - private readonly string _sendingPath; private readonly AmqpFeedbackReceiver _feedbackReceiver; private readonly AmqpFileNotificationReceiver _fileNotificationReceiver; private readonly IHttpClientHelper _httpClientHelper; @@ -37,21 +37,24 @@ internal sealed class AmqpServiceClient : ServiceClient private int _sendingDeliveryTag; - public AmqpServiceClient(IotHubConnectionString iotHubConnectionString, bool useWebSocketOnly, ServiceClientTransportSettings transportSettings, ServiceClientOptions options) + public AmqpServiceClient( + IotHubConnectionProperties connectionProperties, + bool useWebSocketOnly, + ServiceClientTransportSettings transportSettings, + ServiceClientOptions options) { - var iotHubConnection = new IotHubConnection(iotHubConnectionString, AccessRights.ServiceConnect, useWebSocketOnly, transportSettings); + var iotHubConnection = new IotHubConnection(connectionProperties, AccessRights.ServiceConnect, useWebSocketOnly, transportSettings); Connection = iotHubConnection; OpenTimeout = IotHubConnection.DefaultOpenTimeout; OperationTimeout = IotHubConnection.DefaultOperationTimeout; - _sendingPath = "/messages/deviceBound"; _faultTolerantSendingLink = new FaultTolerantAmqpObject(CreateSendingLinkAsync, Connection.CloseLink); _feedbackReceiver = new AmqpFeedbackReceiver(Connection); _fileNotificationReceiver = new AmqpFileNotificationReceiver(Connection); - _iotHubName = iotHubConnectionString.IotHubName; + _iotHubName = connectionProperties.IotHubName; _clientOptions = options; _httpClientHelper = new HttpClientHelper( - iotHubConnectionString.HttpsEndpoint, - iotHubConnectionString, + connectionProperties.HttpsEndpoint, + connectionProperties, ExceptionHandlingHelper.GetDefaultErrorMapping(), s_defaultOperationTimeout, transportSettings.HttpProxy); diff --git a/iothub/service/src/ServiceClient.cs b/iothub/service/src/ServiceClient.cs index ebadb0e2d8..2376c04689 100644 --- a/iothub/service/src/ServiceClient.cs +++ b/iothub/service/src/ServiceClient.cs @@ -6,6 +6,13 @@ using System.Threading.Tasks; using Microsoft.Azure.Devices.Shared; +#if !NET451 + +using Azure; +using Azure.Core; + +#endif + namespace Microsoft.Azure.Devices { /// @@ -53,6 +60,82 @@ public static ServiceClient CreateFromConnectionString(string connectionString, return CreateFromConnectionString(connectionString, TransportType.Amqp, options); } +#if !NET451 + + /// + /// Creates a using Azure Active Directory credentials and the specified transport type. + /// + /// IoT hub host name. + /// Azure Active Directory credentials to authenticate with IoT hub. See + /// Specifies whether Amqp or Amqp_WebSocket_Only transport is used. + /// Specifies the AMQP_WS and HTTP proxy settings for service client. + /// The options that allow configuration of the service client instance during initialization. + /// An instance of . + public static ServiceClient Create( + string hostName, + TokenCredential credential, + TransportType transportType, + ServiceClientTransportSettings transportSettings = default, + ServiceClientOptions options = default) + { + if (string.IsNullOrEmpty(hostName)) + { + throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty"); + } + + if (credential == null) + { + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); + } + + var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential); + bool useWebSocketOnly = transportType == TransportType.Amqp_WebSocket_Only; + + return new AmqpServiceClient( + tokenCredentialProperties, + useWebSocketOnly, + transportSettings ?? new ServiceClientTransportSettings(), + options); + } + + /// + /// Creates a using SAS token and the specified transport type. + /// + /// IoT hub host name. + /// Credential that generates a SAS token to authenticate with IoT hub. See + /// Specifies whether Amqp or Amqp_WebSocket_Only transport is used. + /// Specifies the AMQP_WS and HTTP proxy settings for service client. + /// The options that allow configuration of the service client instance during initialization. + /// An instance of . + public static ServiceClient Create( + string hostName, + IotHubSasCredential credential, + TransportType transportType, + ServiceClientTransportSettings transportSettings = default, + ServiceClientOptions options = default) + { + if (string.IsNullOrEmpty(hostName)) + { + throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty"); + } + + if (credential == null) + { + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); + } + + var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential); + bool useWebSocketOnly = transportType == TransportType.Amqp_WebSocket_Only; + + return new AmqpServiceClient( + sasCredentialProperties, + useWebSocketOnly, + transportSettings ?? new ServiceClientTransportSettings(), + options); + } + +#endif + /// public void Dispose() { From 6d490b1c3a481120e2c9e1cf081ecfca0e9cbdf6 Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Wed, 10 Feb 2021 20:41:07 -0800 Subject: [PATCH 21/29] (service-client): Add constructors in registry manager to accept aad and sas tokens. (#1788) --- iothub/service/src/HttpRegistryManager.cs | 12 ++--- iothub/service/src/RegistryManager.cs | 63 +++++++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/iothub/service/src/HttpRegistryManager.cs b/iothub/service/src/HttpRegistryManager.cs index bef9961e76..509608d8db 100644 --- a/iothub/service/src/HttpRegistryManager.cs +++ b/iothub/service/src/HttpRegistryManager.cs @@ -54,12 +54,12 @@ internal class HttpRegistryManager : RegistryManager private IHttpClientHelper _httpClientHelper; private readonly string _iotHubName; - internal HttpRegistryManager(IotHubConnectionString connectionString, HttpTransportSettings transportSettings) + internal HttpRegistryManager(IotHubConnectionProperties connectionProperties, HttpTransportSettings transportSettings) { - _iotHubName = connectionString.IotHubName; + _iotHubName = connectionProperties.IotHubName; _httpClientHelper = new HttpClientHelper( - connectionString.HttpsEndpoint, - connectionString, + connectionProperties.HttpsEndpoint, + connectionProperties, ExceptionHandlingHelper.GetDefaultErrorMapping(), s_defaultOperationTimeout, transportSettings.Proxy); @@ -735,7 +735,6 @@ public override Task UpdateDevices2Async(IEnumerabl { Logging.Exit(this, $"Updating multiple devices: count: {devices?.Count()} - Force update: {forceUpdate}", nameof(UpdateDevices2Async)); } - } public override Task RemoveDeviceAsync(string deviceId) @@ -937,7 +936,6 @@ public override Task GetRegistryStatisticsAsync(Cancellation return _httpClientHelper.GetAsync(GetStatisticsUri(), errorMappingOverrides, null, cancellationToken); } - catch (Exception ex) { Logging.Error(this, $"{nameof(GetRegistryStatisticsAsync)} threw an exception: {ex}", nameof(GetRegistryStatisticsAsync)); @@ -1037,7 +1035,6 @@ public override Task> GetModulesOnDeviceAsync(string deviceI try { - EnsureInstanceNotClosed(); return _httpClientHelper.GetAsync>( @@ -1473,7 +1470,6 @@ public override Task ImportDevicesAsync(JobProperties jobParamete Logging.Enter(this, $"Import Job running with {jobParameters}", nameof(ImportDevicesAsync)); try { - jobParameters.Type = JobType.ImportDevices; return CreateJobAsync(jobParameters, cancellationToken); } diff --git a/iothub/service/src/RegistryManager.cs b/iothub/service/src/RegistryManager.cs index be036bf286..71a8963a6e 100644 --- a/iothub/service/src/RegistryManager.cs +++ b/iothub/service/src/RegistryManager.cs @@ -7,6 +7,13 @@ using System.Threading.Tasks; using Microsoft.Azure.Devices.Shared; +#if !NET451 + +using Azure; +using Azure.Core; + +#endif + namespace Microsoft.Azure.Devices { /// @@ -46,6 +53,62 @@ public static RegistryManager CreateFromConnectionString(string connectionString return new HttpRegistryManager(iotHubConnectionString, transportSettings); } +#if !NET451 + + /// + /// Creates an instance of . + /// + /// IoT hub host name. + /// Azure Active Directory credentials to authenticate with IoT hub. See + /// The HTTP transport settings. + /// An instance of . + public static RegistryManager Create( + string hostName, + TokenCredential credential, + HttpTransportSettings transportSettings = default) + { + if (string.IsNullOrEmpty(hostName)) + { + throw new ArgumentNullException($"{nameof(hostName)} is null or empty"); + } + + if (credential == null) + { + throw new ArgumentNullException($"{nameof(credential)} is null"); + } + + var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential); + return new HttpRegistryManager(tokenCredentialProperties, transportSettings ?? new HttpTransportSettings()); + } + + /// + /// Creates an instance of . + /// + /// IoT hub host name. + /// Credential that generates a SAS token to authenticate with IoT hub. See + /// The HTTP transport settings. + /// An instance of . + public static RegistryManager Create( + string hostName, + IotHubSasCredential credential, + HttpTransportSettings transportSettings = default) + { + if (string.IsNullOrEmpty(hostName)) + { + throw new ArgumentNullException($"{nameof(hostName)} is null or empty"); + } + + if (credential == null) + { + throw new ArgumentNullException($"{nameof(credential)} is null"); + } + + var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential); + return new HttpRegistryManager(sasCredentialProperties, transportSettings ?? new HttpTransportSettings()); + } + +#endif + /// public void Dispose() { From ae64908939c82c1d4b7ee3c8f6057daec692d62f Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Thu, 11 Feb 2021 09:53:04 -0800 Subject: [PATCH 22/29] (service-client): Add constructors in job client to accept aad and sas tokens. (#1789) --- iothub/service/src/JobClient/HttpJobClient.cs | 6 +- iothub/service/src/JobClient/JobClient.cs | 72 +++++++++++++++++-- iothub/service/src/RegistryManager.cs | 10 +-- iothub/service/src/ServiceClient.cs | 2 +- 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/iothub/service/src/JobClient/HttpJobClient.cs b/iothub/service/src/JobClient/HttpJobClient.cs index 7a74dea26d..1cfd50fe0d 100644 --- a/iothub/service/src/JobClient/HttpJobClient.cs +++ b/iothub/service/src/JobClient/HttpJobClient.cs @@ -28,11 +28,11 @@ internal class HttpJobClient : JobClient private IHttpClientHelper _httpClientHelper; - internal HttpJobClient(IotHubConnectionString connectionString, HttpTransportSettings transportSettings) + internal HttpJobClient(IotHubConnectionProperties connectionProperties, HttpTransportSettings transportSettings) { _httpClientHelper = new HttpClientHelper( - connectionString.HttpsEndpoint, - connectionString, + connectionProperties.HttpsEndpoint, + connectionProperties, ExceptionHandlingHelper.GetDefaultErrorMapping(), s_defaultOperationTimeout, transportSettings.Proxy); diff --git a/iothub/service/src/JobClient/JobClient.cs b/iothub/service/src/JobClient/JobClient.cs index 82ffdc905d..289b6e5e73 100644 --- a/iothub/service/src/JobClient/JobClient.cs +++ b/iothub/service/src/JobClient/JobClient.cs @@ -1,13 +1,19 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.Azure.Devices.Shared; +using System; +using System.Threading; +using System.Threading.Tasks; + +#if !NET451 + +using Azure; +using Azure.Core; + +#endif namespace Microsoft.Azure.Devices { - using Microsoft.Azure.Devices.Shared; - using System; - using System.Threading; - using System.Threading.Tasks; - /// /// Job management /// @@ -41,6 +47,62 @@ public static JobClient CreateFromConnectionString(string connectionString, Http return new HttpJobClient(iotHubConnectionString, transportSettings); } +#if !NET451 + + /// + /// Creates an instance of . + /// + /// IoT hub host name. + /// Azure Active Directory credentials to authenticate with IoT hub. See + /// The HTTP transport settings. + /// An instance of . + public static JobClient Create( + string hostName, + TokenCredential credential, + HttpTransportSettings transportSettings = default) + { + if (string.IsNullOrEmpty(hostName)) + { + throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty"); + } + + if (credential == null) + { + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); + } + + var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential); + return new HttpJobClient(tokenCredentialProperties, transportSettings ?? new HttpTransportSettings()); + } + + /// + /// Creates an instance of . + /// + /// IoT hub host name. + /// Credential that generates a SAS token to authenticate with IoT hub. See . + /// The HTTP transport settings. + /// An instance of . + public static JobClient Create( + string hostName, + IotHubSasCredential credential, + HttpTransportSettings transportSettings = default) + { + if (string.IsNullOrEmpty(hostName)) + { + throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty"); + } + + if (credential == null) + { + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); + } + + var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential); + return new HttpJobClient(sasCredentialProperties, transportSettings ?? new HttpTransportSettings()); + } + +#endif + /// public void Dispose() { diff --git a/iothub/service/src/RegistryManager.cs b/iothub/service/src/RegistryManager.cs index 71a8963a6e..54a909545b 100644 --- a/iothub/service/src/RegistryManager.cs +++ b/iothub/service/src/RegistryManager.cs @@ -69,12 +69,12 @@ public static RegistryManager Create( { if (string.IsNullOrEmpty(hostName)) { - throw new ArgumentNullException($"{nameof(hostName)} is null or empty"); + throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty"); } if (credential == null) { - throw new ArgumentNullException($"{nameof(credential)} is null"); + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); } var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential); @@ -85,7 +85,7 @@ public static RegistryManager Create( /// Creates an instance of . /// /// IoT hub host name. - /// Credential that generates a SAS token to authenticate with IoT hub. See + /// Credential that generates a SAS token to authenticate with IoT hub. See . /// The HTTP transport settings. /// An instance of . public static RegistryManager Create( @@ -95,12 +95,12 @@ public static RegistryManager Create( { if (string.IsNullOrEmpty(hostName)) { - throw new ArgumentNullException($"{nameof(hostName)} is null or empty"); + throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty"); } if (credential == null) { - throw new ArgumentNullException($"{nameof(credential)} is null"); + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); } var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential); diff --git a/iothub/service/src/ServiceClient.cs b/iothub/service/src/ServiceClient.cs index 2376c04689..dd3102695e 100644 --- a/iothub/service/src/ServiceClient.cs +++ b/iothub/service/src/ServiceClient.cs @@ -102,7 +102,7 @@ public static ServiceClient Create( /// Creates a using SAS token and the specified transport type. /// /// IoT hub host name. - /// Credential that generates a SAS token to authenticate with IoT hub. See + /// Credential that generates a SAS token to authenticate with IoT hub. See . /// Specifies whether Amqp or Amqp_WebSocket_Only transport is used. /// Specifies the AMQP_WS and HTTP proxy settings for service client. /// The options that allow configuration of the service client instance during initialization. From b1c424c8b3db2d59ae3509d08758586039ef7efc Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Thu, 11 Feb 2021 11:42:33 -0800 Subject: [PATCH 23/29] (service-client): Add constructors to accept aad and sas tokens for digital twins client. (#1790) --- ... DigitalTwinConnectionStringCredential.cs} | 25 ++-- .../DigitalTwinSasCredential.cs | 30 +++++ ...=> DigitalTwinServiceClientCredentials.cs} | 11 +- .../DigitalTwinTokenCredential.cs | 32 +++++ .../src/DigitalTwin/DigitalTwinClient.cs | 120 +++++++++++++----- iothub/service/src/JobClient/JobClient.cs | 4 +- iothub/service/src/RegistryManager.cs | 4 +- iothub/service/src/ServiceClient.cs | 4 +- 8 files changed, 176 insertions(+), 54 deletions(-) rename iothub/service/src/DigitalTwin/Authentication/{SharedAccessKeyCredentials.cs => DigitalTwinConnectionStringCredential.cs} (73%) create mode 100644 iothub/service/src/DigitalTwin/Authentication/DigitalTwinSasCredential.cs rename iothub/service/src/DigitalTwin/Authentication/{IotServiceClientCredentials.cs => DigitalTwinServiceClientCredentials.cs} (72%) create mode 100644 iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs diff --git a/iothub/service/src/DigitalTwin/Authentication/SharedAccessKeyCredentials.cs b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinConnectionStringCredential.cs similarity index 73% rename from iothub/service/src/DigitalTwin/Authentication/SharedAccessKeyCredentials.cs rename to iothub/service/src/DigitalTwin/Authentication/DigitalTwinConnectionStringCredential.cs index 345cd58d00..03838d7f40 100644 --- a/iothub/service/src/DigitalTwin/Authentication/SharedAccessKeyCredentials.cs +++ b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinConnectionStringCredential.cs @@ -7,12 +7,14 @@ namespace Microsoft.Azure.Devices.Authentication { /// - /// Allows authentication to the API using a Shared Access Key + /// Allows authentication to the API using a Shared Access Key generated from the connection string provided. + /// The PnP client is auto generated from swagger and needs to implement a specific class to pass to the protocol layer + /// unlike the rest of the clients which are hand-written. So, this implementation for authentication is specific to digital twin (Pnp). /// - internal class SharedAccessKeyCredentials : IotServiceClientCredentials + internal class DigitalTwinConnectionStringCredential : DigitalTwinServiceClientCredentials { // Time buffer before expiry when the token should be renewed, expressed as a percentage of the time to live. - // The token will be renewed when it has 15% or less of the sas token's lifespan left. + // The token will be renewed when it has 15% or less of the SAS token's lifespan left. private const int RenewalTimeBufferPercentage = 15; private readonly object _sasLock = new object(); @@ -26,22 +28,20 @@ internal class SharedAccessKeyCredentials : IotServiceClientCredentials private DateTimeOffset _tokenExpiryTime; /// - /// Initializes a new instance of class. + /// Initializes a new instance of class. /// - /// The IoT Hub connection string. - internal SharedAccessKeyCredentials(string connectionString) + /// The IoT Hub connection string properties. + internal DigitalTwinConnectionStringCredential(IotHubConnectionString connectionString) { - var iotHubConnectionString = IotHubConnectionString.Parse(connectionString); - - _sharedAccessKey = iotHubConnectionString.SharedAccessKey; - _sharedAccessPolicy = iotHubConnectionString.SharedAccessKeyName; - _audience = iotHubConnectionString.Audience; + _sharedAccessKey = connectionString.SharedAccessKey; + _sharedAccessPolicy = connectionString.SharedAccessKeyName; + _audience = connectionString.Audience; _cachedSasToken = null; } /// - protected override string GetSasToken() + public override string GetAuthorizationHeader() { lock (_sasLock) { @@ -76,6 +76,5 @@ private bool TokenShouldBeGenerated() DateTimeOffset tokenExpiryTimeWithBuffer = _tokenExpiryTime.AddMilliseconds(-bufferTimeInMilliseconds); return DateTimeOffset.UtcNow.CompareTo(tokenExpiryTimeWithBuffer) >= 0; } - } } diff --git a/iothub/service/src/DigitalTwin/Authentication/DigitalTwinSasCredential.cs b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinSasCredential.cs new file mode 100644 index 0000000000..eb93a2e9ec --- /dev/null +++ b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinSasCredential.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; +using Azure; +using Microsoft.Azure.Devices.Authentication; + +namespace Microsoft.Azure.Devices.DigitalTwin.Authentication +{ + /// + /// Allows authentication to the API using a Shared Access Key provided by custom implementation. + /// The PnP client is auto generated from swagger and needs to implement a specific class to pass to the protocol layer + /// unlike the rest of the clients which are hand-written. So, this implementation for authentication is specific to digital twin (Pnp). + /// + internal class DigitalTwinSasCredential : DigitalTwinServiceClientCredentials + { + private AzureSasCredential _credential; + + public DigitalTwinSasCredential(AzureSasCredential credential) + { + _credential = credential; + } + + public override string GetAuthorizationHeader() + { + return _credential.Signature; + } + } +} diff --git a/iothub/service/src/DigitalTwin/Authentication/IotServiceClientCredentials.cs b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinServiceClientCredentials.cs similarity index 72% rename from iothub/service/src/DigitalTwin/Authentication/IotServiceClientCredentials.cs rename to iothub/service/src/DigitalTwin/Authentication/DigitalTwinServiceClientCredentials.cs index 7c7edb43ea..c96134381a 100644 --- a/iothub/service/src/DigitalTwin/Authentication/IotServiceClientCredentials.cs +++ b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinServiceClientCredentials.cs @@ -11,10 +11,13 @@ namespace Microsoft.Azure.Devices.Authentication { - internal abstract class IotServiceClientCredentials : ServiceClientCredentials + /// + /// This class adds the authentication tokens to the header before calling the digital twin APIs. + /// + internal abstract class DigitalTwinServiceClientCredentials : ServiceClientCredentials, IAuthorizationHeaderProvider { /// - /// Add a SAS token to the outgoing http request, then send it to the next pipeline segment + /// Add a JWT for Azure Active Directory or SAS token to the outgoing http request, then send it to the next pipeline segment. /// /// The request that is being sent /// The cancellation token @@ -23,7 +26,7 @@ public override Task ProcessHttpRequestAsync(HttpRequestMessage request, Cancell { request.ThrowIfNull(nameof(HttpRequestMessage)); - request.Headers.Add(HttpRequestHeader.Authorization.ToString(), GetSasToken()); + request.Headers.Add(HttpRequestHeader.Authorization.ToString(), GetAuthorizationHeader()); request.Headers.Add(HttpRequestHeader.UserAgent.ToString(), Utils.GetClientVersion()); return base.ProcessHttpRequestAsync(request, cancellationToken); @@ -33,6 +36,6 @@ public override Task ProcessHttpRequestAsync(HttpRequestMessage request, Cancell /// Return a SAS token /// /// A SAS token - protected abstract string GetSasToken(); + public abstract string GetAuthorizationHeader(); } } diff --git a/iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs new file mode 100644 index 0000000000..a6611e99f7 --- /dev/null +++ b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using Azure.Core; +using Microsoft.Azure.Devices.Authentication; + +namespace Microsoft.Azure.Devices.DigitalTwin.Authentication +{ + /// + /// Allows authentication to the API using a JWT token generated for Azure active directory. + /// The PnP client is auto generated from swagger and needs to implement a specific class to pass to the protocol layer + /// unlike the rest of the clients which are hand-written. so, this implementation for authentication is specific to digital twin (Pnp). + /// + internal class DigitalTwinTokenCredential : DigitalTwinServiceClientCredentials + { + private TokenCredential _credential; + + public DigitalTwinTokenCredential(TokenCredential credential) + { + _credential = credential; + } + + public override string GetAuthorizationHeader() + { + AccessToken token = _credential.GetToken(new TokenRequestContext(), new CancellationToken()); + return $"Bearer {token.Token}"; + } + } +} diff --git a/iothub/service/src/DigitalTwin/DigitalTwinClient.cs b/iothub/service/src/DigitalTwin/DigitalTwinClient.cs index 03d4c4cb6d..06c0f59521 100644 --- a/iothub/service/src/DigitalTwin/DigitalTwinClient.cs +++ b/iothub/service/src/DigitalTwin/DigitalTwinClient.cs @@ -10,6 +10,10 @@ using Microsoft.Azure.Devices.Generated; using Microsoft.Rest; using Newtonsoft.Json; +using Microsoft.Azure.Devices.DigitalTwin.Authentication; +using Azure; +using Azure.Core; +using PnpDigitalTwin = Microsoft.Azure.Devices.Generated.DigitalTwin; namespace Microsoft.Azure.Devices { @@ -18,8 +22,16 @@ namespace Microsoft.Azure.Devices /// public class DigitalTwinClient : IDisposable { + private const string HttpsEndpointPrefix = "https"; private readonly IotHubGatewayServiceAPIs _client; - private readonly DigitalTwin _protocolLayer; + private readonly PnpDigitalTwin _protocolLayer; + + private DigitalTwinClient(string hostName, DigitalTwinServiceClientCredentials credentials, params DelegatingHandler[] handlers) + { + var httpsEndpoint = new UriBuilder(HttpsEndpointPrefix, hostName).Uri; + _client = new IotHubGatewayServiceAPIs(httpsEndpoint, credentials, handlers); + _protocolLayer = new PnpDigitalTwin(_client); + } /// /// Initializes a new instance of the class. @@ -30,14 +42,60 @@ public static DigitalTwinClient CreateFromConnectionString(string connectionStri connectionString.ThrowIfNullOrWhiteSpace(nameof(connectionString)); var iotHubConnectionString = IotHubConnectionString.Parse(connectionString); - var sharedAccessKeyCredential = new SharedAccessKeyCredentials(connectionString); - return new DigitalTwinClient(iotHubConnectionString.HttpsEndpoint, sharedAccessKeyCredential, handlers); + var connectionStringCredential = new DigitalTwinConnectionStringCredential(iotHubConnectionString); + return new DigitalTwinClient(iotHubConnectionString.HostName, connectionStringCredential, handlers); + } + + /// + /// Creates an instance of . + /// + /// IoT hub host name. + /// Azure Active Directory credentials to authenticate with IoT hub. See + /// The delegating handlers to add to the http client pipeline. You can add handlers for tracing, implementing a retry strategy, routing requests through a proxy, etc. + /// An instance of . + public static DigitalTwinClient Create( + string hostName, + TokenCredential credential, + params DelegatingHandler[] handlers) + { + if (string.IsNullOrEmpty(hostName)) + { + throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty"); + } + + if (credential == null) + { + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null"); + } + + var tokenCredential = new DigitalTwinTokenCredential(credential); + return new DigitalTwinClient(hostName, tokenCredential, handlers); } - private DigitalTwinClient(Uri uri, IotServiceClientCredentials credentials, params DelegatingHandler[] handlers) + /// + /// Creates an instance of . + /// + /// IoT hub host name. + /// Credential that generates a SAS token to authenticate with IoT hub. See . + /// The delegating handlers to add to the http client pipeline. You can add handlers for tracing, implementing a retry strategy, routing requests through a proxy, etc. + /// An instance of . + public static DigitalTwinClient Create( + string hostName, + AzureSasCredential credential, + params DelegatingHandler[] handlers) { - _client = new IotHubGatewayServiceAPIs(uri, credentials, handlers); - _protocolLayer = new DigitalTwin(_client); + if (string.IsNullOrEmpty(hostName)) + { + throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty"); + } + + if (credential == null) + { + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null"); + } + + var sasCredential = new DigitalTwinSasCredential(credential); + return new DigitalTwinClient(hostName, sasCredential, handlers); } /// @@ -69,9 +127,9 @@ public async Task> GetDigitalTwi /// The cancellationToken. /// The http response. public Task> UpdateDigitalTwinAsync( - string digitalTwinId, - string digitalTwinUpdateOperations, - DigitalTwinUpdateRequestOptions requestOptions = default, + string digitalTwinId, + string digitalTwinUpdateOperations, + DigitalTwinUpdateRequestOptions requestOptions = default, CancellationToken cancellationToken = default) { return _protocolLayer.UpdateDigitalTwinWithHttpMessagesAsync(digitalTwinId, digitalTwinUpdateOperations, requestOptions?.IfMatch, null, cancellationToken); @@ -87,19 +145,19 @@ public Task> UpdateDigital /// The cancellationToken. /// The application/json command invocation response and the http response. public async Task> InvokeCommandAsync( - string digitalTwinId, - string commandName, - string payload = default, - DigitalTwinInvokeCommandRequestOptions requestOptions = default, + string digitalTwinId, + string commandName, + string payload = default, + DigitalTwinInvokeCommandRequestOptions requestOptions = default, CancellationToken cancellationToken = default) { using HttpOperationResponse response = await _protocolLayer.InvokeRootLevelCommandWithHttpMessagesAsync( - digitalTwinId, - commandName, - payload, - requestOptions?.ConnectTimeoutInSeconds, - requestOptions?.ResponseTimeoutInSeconds, - null, + digitalTwinId, + commandName, + payload, + requestOptions?.ConnectTimeoutInSeconds, + requestOptions?.ResponseTimeoutInSeconds, + null, cancellationToken) .ConfigureAwait(false); return new HttpOperationResponse @@ -122,21 +180,21 @@ public async TaskThe cancellationToken. /// The application/json command invocation response and the http response. public async Task> InvokeComponentCommandAsync( - string digitalTwinId, - string componentName, - string commandName, - string payload = default, - DigitalTwinInvokeCommandRequestOptions requestOptions = default, + string digitalTwinId, + string componentName, + string commandName, + string payload = default, + DigitalTwinInvokeCommandRequestOptions requestOptions = default, CancellationToken cancellationToken = default) { using HttpOperationResponse response = await _protocolLayer.InvokeComponentCommandWithHttpMessagesAsync( - digitalTwinId, - componentName, - commandName, - payload, - requestOptions?.ConnectTimeoutInSeconds, - requestOptions?.ResponseTimeoutInSeconds, - null, + digitalTwinId, + componentName, + commandName, + payload, + requestOptions?.ConnectTimeoutInSeconds, + requestOptions?.ResponseTimeoutInSeconds, + null, cancellationToken) .ConfigureAwait(false); return new HttpOperationResponse diff --git a/iothub/service/src/JobClient/JobClient.cs b/iothub/service/src/JobClient/JobClient.cs index 289b6e5e73..671790e72e 100644 --- a/iothub/service/src/JobClient/JobClient.cs +++ b/iothub/service/src/JobClient/JobClient.cs @@ -68,7 +68,7 @@ public static JobClient Create( if (credential == null) { - throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null"); } var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential); @@ -94,7 +94,7 @@ public static JobClient Create( if (credential == null) { - throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null"); } var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential); diff --git a/iothub/service/src/RegistryManager.cs b/iothub/service/src/RegistryManager.cs index 54a909545b..1aca479b39 100644 --- a/iothub/service/src/RegistryManager.cs +++ b/iothub/service/src/RegistryManager.cs @@ -74,7 +74,7 @@ public static RegistryManager Create( if (credential == null) { - throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null"); } var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential); @@ -100,7 +100,7 @@ public static RegistryManager Create( if (credential == null) { - throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null"); } var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential); diff --git a/iothub/service/src/ServiceClient.cs b/iothub/service/src/ServiceClient.cs index dd3102695e..94ab557ad7 100644 --- a/iothub/service/src/ServiceClient.cs +++ b/iothub/service/src/ServiceClient.cs @@ -85,7 +85,7 @@ public static ServiceClient Create( if (credential == null) { - throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null"); } var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential); @@ -121,7 +121,7 @@ public static ServiceClient Create( if (credential == null) { - throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null or empty"); + throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null"); } var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential); From 5cbe79d74b9bd6d37413546accba24635f98629d Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Thu, 18 Feb 2021 16:34:46 -0800 Subject: [PATCH 24/29] fix(service-client): Support for AzureSasCredential for a better user experience (#1797) --- iothub/service/src/IoTHubSasCredential.cs | 48 ------- .../src/IotHubSasCredentialProperties.cs | 26 +++- iothub/service/src/JobClient/JobClient.cs | 4 +- iothub/service/src/RegistryManager.cs | 4 +- iothub/service/src/ServiceClient.cs | 4 +- .../IotHubSasCredentialPropertiesTests.cs | 121 ++++++++++++++++++ 6 files changed, 150 insertions(+), 57 deletions(-) delete mode 100644 iothub/service/src/IoTHubSasCredential.cs create mode 100644 iothub/service/tests/IotHubSasCredentialPropertiesTests.cs diff --git a/iothub/service/src/IoTHubSasCredential.cs b/iothub/service/src/IoTHubSasCredential.cs deleted file mode 100644 index a21179f98b..0000000000 --- a/iothub/service/src/IoTHubSasCredential.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; -using System.Collections.Generic; -using System.Text; - -#if !NET451 - -using Azure; - -namespace Microsoft.Azure.Devices -{ - /// - /// Shared access signature credential used to authenticate with IoT hub. - /// - public class IotHubSasCredential - : AzureSasCredential - { - /// - /// Creates an instance of . - /// - /// Shared access signature used to authenticate with IoT hub. - /// The shared access signature expiry in UTC. - public IotHubSasCredential(string signature, DateTime expiresOnUtc) : base(signature) - { - ExpiresOnUtc = expiresOnUtc; - } - - /// - /// The shared access signature expiry in UTC. - /// - public DateTime ExpiresOnUtc { get; private set; } - - /// - /// Updates the shared access signature. This is intended to be used when you've - /// regenerated your shared access signature and want to update clients. - /// - /// Shared access signature used to authenticate with IoT hub. - /// The shared access signature expiry in UTC. - public void Update(string signature, DateTime expiresOnUtc) - { - Update(signature); - ExpiresOnUtc = expiresOnUtc; - } - } -} - -#endif diff --git a/iothub/service/src/IotHubSasCredentialProperties.cs b/iothub/service/src/IotHubSasCredentialProperties.cs index 44b37bf9c8..756fe3865a 100644 --- a/iothub/service/src/IotHubSasCredentialProperties.cs +++ b/iothub/service/src/IotHubSasCredentialProperties.cs @@ -5,6 +5,8 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Azure.Amqp; +using System.Globalization; +using System.Linq; #if !NET451 @@ -23,9 +25,9 @@ public IotHubSasCredentialProperties() throw new InvalidOperationException("IotHubSasCredential is not supported on NET451"); } #else - private readonly IotHubSasCredential _credential; + private readonly AzureSasCredential _credential; - public IotHubSasCredentialProperties(string hostName, IotHubSasCredential credential) : base(hostName) + public IotHubSasCredentialProperties(string hostName, AzureSasCredential credential) : base(hostName) { _credential = credential; } @@ -48,10 +50,28 @@ public override Task GetTokenAsync(Uri namespaceAddress, string applie throw new InvalidOperationException($"IotHubSasCredential is not supported on NET451"); #else + // Parse the SAS token to find the expiration date and time. + // SharedAccessSignature sr=ENCODED(dh://myiothub.azure-devices.net/a/b/c?myvalue1=a)&sig=&se=[&skn=] + var tokenParts = _credential.Signature.Split('&').ToList(); + var expiresAtTokenPart = tokenParts.Where(tokenPart => tokenPart.StartsWith("se=", StringComparison.OrdinalIgnoreCase)); + + if (!expiresAtTokenPart.Any()) + { + throw new InvalidOperationException($"There is no expiration time on {nameof(AzureSasCredential)} signature."); + } + + string expiresAtStr = expiresAtTokenPart.First().Split('=')[1]; + bool isSuccess = DateTime.TryParse(expiresAtStr, out DateTime expiresAt); + + if (!isSuccess) + { + throw new InvalidOperationException($"Invalid expiration time on {nameof(AzureSasCredential)} signature."); + } + var token = new CbsToken( _credential.Signature, CbsConstants.IotHubSasTokenType, - _credential.ExpiresOnUtc); + expiresAt); return Task.FromResult(token); #endif } diff --git a/iothub/service/src/JobClient/JobClient.cs b/iothub/service/src/JobClient/JobClient.cs index 671790e72e..6c963c30c9 100644 --- a/iothub/service/src/JobClient/JobClient.cs +++ b/iothub/service/src/JobClient/JobClient.cs @@ -79,12 +79,12 @@ public static JobClient Create( /// Creates an instance of . /// /// IoT hub host name. - /// Credential that generates a SAS token to authenticate with IoT hub. See . + /// Credential that generates a SAS token to authenticate with IoT hub. See . /// The HTTP transport settings. /// An instance of . public static JobClient Create( string hostName, - IotHubSasCredential credential, + AzureSasCredential credential, HttpTransportSettings transportSettings = default) { if (string.IsNullOrEmpty(hostName)) diff --git a/iothub/service/src/RegistryManager.cs b/iothub/service/src/RegistryManager.cs index 1aca479b39..3683a0e0ca 100644 --- a/iothub/service/src/RegistryManager.cs +++ b/iothub/service/src/RegistryManager.cs @@ -85,12 +85,12 @@ public static RegistryManager Create( /// Creates an instance of . /// /// IoT hub host name. - /// Credential that generates a SAS token to authenticate with IoT hub. See . + /// Credential that generates a SAS token to authenticate with IoT hub. See . /// The HTTP transport settings. /// An instance of . public static RegistryManager Create( string hostName, - IotHubSasCredential credential, + AzureSasCredential credential, HttpTransportSettings transportSettings = default) { if (string.IsNullOrEmpty(hostName)) diff --git a/iothub/service/src/ServiceClient.cs b/iothub/service/src/ServiceClient.cs index 94ab557ad7..e8032f79eb 100644 --- a/iothub/service/src/ServiceClient.cs +++ b/iothub/service/src/ServiceClient.cs @@ -102,14 +102,14 @@ public static ServiceClient Create( /// Creates a using SAS token and the specified transport type. /// /// IoT hub host name. - /// Credential that generates a SAS token to authenticate with IoT hub. See . + /// Credential that generates a SAS token to authenticate with IoT hub. See . /// Specifies whether Amqp or Amqp_WebSocket_Only transport is used. /// Specifies the AMQP_WS and HTTP proxy settings for service client. /// The options that allow configuration of the service client instance during initialization. /// An instance of . public static ServiceClient Create( string hostName, - IotHubSasCredential credential, + AzureSasCredential credential, TransportType transportType, ServiceClientTransportSettings transportSettings = default, ServiceClientOptions options = default) diff --git a/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs b/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs new file mode 100644 index 0000000000..45d5ed8f6e --- /dev/null +++ b/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Net; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.Azure.Amqp; +using FluentAssertions; + +#if !NET451 + +using Azure; + +#endif + +namespace Microsoft.Azure.Devices.Tests +{ + [TestClass] + [TestCategory("Unit")] + public class IotHubSasCredentialPropertiesTests + { + private const string _hostName = "myiothub.azure-devices.net"; + +#if !NET451 + + [TestMethod] + public async Task TestCbsTokenGeneration_Succeeds() + { + // arrange + DateTime expiresAtUtc = DateTime.UtcNow; + DateTime updatedExpiresAtUtc = DateTime.UtcNow.AddDays(1); + + string token = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}&se={2}", + WebUtility.UrlEncode(_hostName), + WebUtility.UrlEncode("signature"), + expiresAtUtc); + + string updatedToken = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}&se={2}", + WebUtility.UrlEncode(_hostName), + WebUtility.UrlEncode("signature"), + updatedExpiresAtUtc); + + var azureSasCredential = new AzureSasCredential(token); + var iotHubSasCredentialProperties = new IotHubSasCredentialProperties(_hostName, azureSasCredential); + + // act + + CbsToken cbsToken = await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); + azureSasCredential.Update(updatedToken); + CbsToken updatedCbsToken = await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); + + // assert + cbsToken.ExpiresAtUtc.ToString().Should().Be(expiresAtUtc.ToString()); + updatedCbsToken.ExpiresAtUtc.ToString().Should().Be(updatedExpiresAtUtc.ToString()); + } + + [TestMethod] + public async Task TestCbsTokenGeneration_InvalidExpirationDateTimeFormat_Fails() + { + // arrange + string token = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}&se={2}", + WebUtility.UrlEncode(_hostName), + WebUtility.UrlEncode("signature"), + "01:01:2021"); + + var azureSasCredential = new AzureSasCredential(token); + var iotHubSasCredentialProperties = new IotHubSasCredentialProperties(_hostName, azureSasCredential); + + try + { + // act + await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); + + Assert.Fail("The parsing of date time in invalid format on the SAS token should have caused an exception."); + } + catch (InvalidOperationException ex) + { + // assert + ex.Message.Should().Be($"Invalid expiration time on {nameof(AzureSasCredential)} signature."); + } + } + + [TestMethod] + public async Task TestCbsTokenGeneration_MissingExpiration_Fails() + { + // arrange + string token = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}", + WebUtility.UrlEncode(_hostName), + WebUtility.UrlEncode("signature")); + + var azureSasCredential = new AzureSasCredential(token); + var iotHubSasCredentialProperties = new IotHubSasCredentialProperties(_hostName, azureSasCredential); + + try + { + // act + await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); + + Assert.Fail("The missing expiry on the SAS token should have caused an exception."); + } + catch (InvalidOperationException ex) + { + // assert + ex.Message.Should().Be($"There is no expiration time on {nameof(AzureSasCredential)} signature."); + } + } + +#endif + } +} From 296ccd92ffc1c6a28225de1863811981e1397bbf Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Fri, 19 Feb 2021 11:00:07 -0800 Subject: [PATCH 25/29] doc(service-client): Update readme about the differnt client and operations (#1798) --- iothub/service/src/DigitalTwin/DigitalTwinClient.cs | 1 + iothub/service/src/JobClient/JobClient.cs | 1 + iothub/service/src/RegistryManager.cs | 1 + iothub/service/src/ServiceClient.cs | 6 ++++-- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/iothub/service/src/DigitalTwin/DigitalTwinClient.cs b/iothub/service/src/DigitalTwin/DigitalTwinClient.cs index 06c0f59521..48d287e37e 100644 --- a/iothub/service/src/DigitalTwin/DigitalTwinClient.cs +++ b/iothub/service/src/DigitalTwin/DigitalTwinClient.cs @@ -19,6 +19,7 @@ namespace Microsoft.Azure.Devices { /// /// The Digital Twins Service Client contains methods to retrieve and update digital twin information, and invoke commands on a digital twin device. + /// For more information, see /// public class DigitalTwinClient : IDisposable { diff --git a/iothub/service/src/JobClient/JobClient.cs b/iothub/service/src/JobClient/JobClient.cs index 6c963c30c9..a48d8d2b7e 100644 --- a/iothub/service/src/JobClient/JobClient.cs +++ b/iothub/service/src/JobClient/JobClient.cs @@ -21,6 +21,7 @@ public abstract class JobClient : IDisposable { /// /// Creates a JobClient from the Iot Hub connection string. + /// For more information, see /// /// The Iot Hub connection string. /// A JobClient instance. diff --git a/iothub/service/src/RegistryManager.cs b/iothub/service/src/RegistryManager.cs index 3683a0e0ca..b953d0b501 100644 --- a/iothub/service/src/RegistryManager.cs +++ b/iothub/service/src/RegistryManager.cs @@ -18,6 +18,7 @@ namespace Microsoft.Azure.Devices { /// /// Contains methods that services can use to perform create, remove, update and delete operations on devices. + /// For more information, see /// [System.Diagnostics.CodeAnalysis.SuppressMessage( "Naming", diff --git a/iothub/service/src/ServiceClient.cs b/iothub/service/src/ServiceClient.cs index e8032f79eb..b88e399938 100644 --- a/iothub/service/src/ServiceClient.cs +++ b/iothub/service/src/ServiceClient.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Threading; @@ -36,8 +38,8 @@ public enum TransportType #pragma warning restore CA1707 // Identifiers should not contain underscores /// - /// Contains methods that services can use to send messages to devices/modules, - /// invoke a direct method on a device/module and deliver notifications for file upload and cloud-to-device operations. + /// Contains methods that services can use to send messages to devices + /// For more information, see /// public abstract class ServiceClient : IDisposable { From ee87095c304e685006c5089bc31901ee31583531 Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Fri, 19 Feb 2021 15:18:59 -0800 Subject: [PATCH 26/29] tests(service-client): E2E tests for aad auth on all our clients (#1800) --- e2e/test/E2ETests.csproj | 1 + e2e/test/config/Configuration.IoTHub.cs | 33 +++- .../ConnectionStatusChangeHandlerTests.cs | 2 +- e2e/test/iothub/DeviceTokenRefreshE2ETests.cs | 12 +- .../TokenCredentialAuthenticationTests.cs | 151 ++++++++++++++++++ 5 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 e2e/test/iothub/TokenCredentialAuthenticationTests.cs diff --git a/e2e/test/E2ETests.csproj b/e2e/test/E2ETests.csproj index 3684595457..f025a01846 100644 --- a/e2e/test/E2ETests.csproj +++ b/e2e/test/E2ETests.csproj @@ -45,6 +45,7 @@ + diff --git a/e2e/test/config/Configuration.IoTHub.cs b/e2e/test/config/Configuration.IoTHub.cs index ffc96e3129..782c2590f0 100644 --- a/e2e/test/config/Configuration.IoTHub.cs +++ b/e2e/test/config/Configuration.IoTHub.cs @@ -3,6 +3,13 @@ using System; using System.Security.Cryptography.X509Certificates; + +#if !NET451 + +using Azure.Identity; + +#endif + using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.Azure.Devices.E2ETests @@ -14,6 +21,24 @@ public static partial class IoTHub public static string ConnectionString => GetValue("IOTHUB_CONN_STRING_CSHARP"); public static string X509ChainDeviceName => GetValue("IOTHUB_X509_CHAIN_DEVICE_NAME"); + public static string GetIotHubHostName() + { + ConnectionStringParser connectionString = new ConnectionStringParser(ConnectionString); + return connectionString.IotHubHostName; + } + +#if !NET451 + + public static ClientSecretCredential GetClientSecretCredential() + { + return new ClientSecretCredential( + GetValue("IOTHUB_TENANT_ID"), + GetValue("IOTHUB_CLIENT_ID"), + GetValue("IOTHUB_CLIENT_SECRET")); + } + +#endif + public static X509Certificate2 GetCertificateWithPrivateKey() { const string hubPfxCert = "IOTHUB_X509_PFX_CERTIFICATE"; @@ -65,9 +90,9 @@ public static X509Certificate2 GetIntermediate2Certificate() /// public const string InvalidProxyServerAddress = "127.0.0.1:1234"; - public class DeviceConnectionStringParser + public class ConnectionStringParser { - public DeviceConnectionStringParser(string connectionString) + public ConnectionStringParser(string connectionString) { string[] parts = connectionString.Split(';'); foreach (string part in parts) @@ -77,7 +102,7 @@ public DeviceConnectionStringParser(string connectionString) switch (tv[0].ToUpperInvariant()) { case "HOSTNAME": - IoTHub = part.Substring("HOSTNAME=".Length); + IotHubHostName = part.Substring("HOSTNAME=".Length); break; case "SHAREDACCESSKEY": @@ -94,7 +119,7 @@ public DeviceConnectionStringParser(string connectionString) } } - public string IoTHub + public string IotHubHostName { get; private set; diff --git a/e2e/test/iothub/ConnectionStatusChangeHandlerTests.cs b/e2e/test/iothub/ConnectionStatusChangeHandlerTests.cs index e9ecabd8d5..f7e83f8a9e 100644 --- a/e2e/test/iothub/ConnectionStatusChangeHandlerTests.cs +++ b/e2e/test/iothub/ConnectionStatusChangeHandlerTests.cs @@ -109,7 +109,7 @@ private async Task DeviceClient_Gives_ConnectionStatus_DeviceDisabled_Base( TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, DevicePrefix + $"_{Guid.NewGuid()}").ConfigureAwait(false); string deviceConnectionString = testDevice.ConnectionString; - var config = new Configuration.IoTHub.DeviceConnectionStringParser(deviceConnectionString); + var config = new Configuration.IoTHub.ConnectionStringParser(deviceConnectionString); string deviceId = config.DeviceID; ConnectionStatus? status = null; diff --git a/e2e/test/iothub/DeviceTokenRefreshE2ETests.cs b/e2e/test/iothub/DeviceTokenRefreshE2ETests.cs index 9e5e69688a..f307a9eab1 100644 --- a/e2e/test/iothub/DeviceTokenRefreshE2ETests.cs +++ b/e2e/test/iothub/DeviceTokenRefreshE2ETests.cs @@ -30,8 +30,8 @@ public async Task DeviceClient_Not_Exist_AMQP() { TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, DevicePrefix).ConfigureAwait(false); - var config = new Configuration.IoTHub.DeviceConnectionStringParser(testDevice.ConnectionString); - using (DeviceClient deviceClient = DeviceClient.CreateFromConnectionString($"HostName={config.IoTHub};DeviceId=device_id_not_exist;SharedAccessKey={config.SharedAccessKey}", Client.TransportType.Amqp_Tcp_Only)) + var config = new Configuration.IoTHub.ConnectionStringParser(testDevice.ConnectionString); + using (DeviceClient deviceClient = DeviceClient.CreateFromConnectionString($"HostName={config.IotHubHostName};DeviceId=device_id_not_exist;SharedAccessKey={config.SharedAccessKey}", Client.TransportType.Amqp_Tcp_Only)) { await deviceClient.OpenAsync().ConfigureAwait(false); } @@ -43,9 +43,9 @@ public async Task DeviceClient_Bad_Credentials_AMQP() { TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, DevicePrefix).ConfigureAwait(false); - var config = new Configuration.IoTHub.DeviceConnectionStringParser(testDevice.ConnectionString); + var config = new Configuration.IoTHub.ConnectionStringParser(testDevice.ConnectionString); string invalidKey = Convert.ToBase64String(Encoding.UTF8.GetBytes("invalid_key")); - using (DeviceClient deviceClient = DeviceClient.CreateFromConnectionString($"HostName={config.IoTHub};DeviceId={config.DeviceID};SharedAccessKey={invalidKey}", Client.TransportType.Amqp_Tcp_Only)) + using (DeviceClient deviceClient = DeviceClient.CreateFromConnectionString($"HostName={config.IotHubHostName};DeviceId={config.DeviceID};SharedAccessKey={invalidKey}", Client.TransportType.Amqp_Tcp_Only)) { await deviceClient.OpenAsync().ConfigureAwait(false); } @@ -82,8 +82,8 @@ public async Task DeviceClient_TokenConnectionDoubleRelease_Ok() string deviceConnectionString = testDevice.ConnectionString; - var config = new Configuration.IoTHub.DeviceConnectionStringParser(deviceConnectionString); - string iotHub = config.IoTHub; + var config = new Configuration.IoTHub.ConnectionStringParser(deviceConnectionString); + string iotHub = config.IotHubHostName; string deviceId = config.DeviceID; string key = config.SharedAccessKey; diff --git a/e2e/test/iothub/TokenCredentialAuthenticationTests.cs b/e2e/test/iothub/TokenCredentialAuthenticationTests.cs new file mode 100644 index 0000000000..aa42ee400c --- /dev/null +++ b/e2e/test/iothub/TokenCredentialAuthenticationTests.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Azure.Devices.Client; +using Microsoft.Azure.Devices.Common.Exceptions; +using Microsoft.Azure.Devices.E2ETests.Helpers; +using Microsoft.Azure.Devices.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#if !NET451 + +using Microsoft.Rest; +using Azure.Core; +using Azure.Identity; + +#endif + +using ClientOptions = Microsoft.Azure.Devices.Client.ClientOptions; + +namespace Microsoft.Azure.Devices.E2ETests.iothub +{ + /// + /// Tests to ensure authentication using Azure active directory succeeds in all the clients. + /// + [TestClass] + [TestCategory("E2E")] + [TestCategory("IoTHub")] + public class TokenCredentialAuthenticationTests : E2EMsTestBase + { + private readonly string _devicePrefix = $"E2E_{nameof(TokenCredentialAuthenticationTests)}_"; + +#if !NET451 + + [Ignore] + [LoggedTestMethod] + public async Task RegistryManager_Http_TokenCredentialAuth_Success() + { + // arrange + using var registryManager = RegistryManager.Create( + Configuration.IoTHub.GetIotHubHostName(), + Configuration.IoTHub.GetClientSecretCredential()); + + var device = new Device(Guid.NewGuid().ToString()); + + // act + Device createdDevice = await registryManager.AddDeviceAsync(device).ConfigureAwait(false); + + // assert + Assert.IsNotNull(createdDevice); + + // cleanup + await registryManager.RemoveDeviceAsync(device.Id).ConfigureAwait(false); + } + + [Ignore] + [LoggedTestMethod] + public async Task JobClient_Http_TokenCredentialAuth_Success() + { + // arrange + using var jobClient = JobClient.Create( + Configuration.IoTHub.GetIotHubHostName(), + Configuration.IoTHub.GetClientSecretCredential()); + + string jobId = "JOBSAMPLE" + Guid.NewGuid().ToString(); + string jobDeviceId = "JobsSample_Device"; + string query = $"DeviceId IN ['{jobDeviceId}']"; + Twin twin = new Twin(jobDeviceId); + + try + { + // act + JobResponse createJobResponse = await jobClient + .ScheduleTwinUpdateAsync( + jobId, + query, + twin, + DateTime.UtcNow, + (long)TimeSpan.FromMinutes(2).TotalSeconds) + .ConfigureAwait(false); + } + catch (ThrottlingException) + { + // Concurrent jobs can be rejected, but it still means authentication was successful. Ignore the exception. + } + } + + [Ignore] + [LoggedTestMethod] + public async Task DigitalTwinClient_Http_TokenCredentialAuth_Success() + { + // arrange + TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); + string thermostatModelId = "dtmi:com:example:TemperatureController;1"; + + // Create a device client instance initializing it with the "Thermostat" model. + var options = new ClientOptions + { + ModelId = thermostatModelId, + }; + using DeviceClient deviceClient = testDevice.CreateDeviceClient(Client.TransportType.Mqtt, options); + + // Call openAsync() to open the device's connection, so that the ModelId is sent over Mqtt CONNECT packet. + await deviceClient.OpenAsync().ConfigureAwait(false); + + using var digitalTwinClient = DigitalTwinClient.Create( + Configuration.IoTHub.GetIotHubHostName(), + Configuration.IoTHub.GetClientSecretCredential()); + + // act + HttpOperationResponse response = await digitalTwinClient + .GetDigitalTwinAsync(testDevice.Id) + .ConfigureAwait(false); + ThermostatTwin twin = response.Body; + + // assert + twin.Metadata.ModelId.Should().Be(thermostatModelId); + + // cleanup + await testDevice.RemoveDeviceAsync().ConfigureAwait(false); + } + + [Ignore] + [LoggedTestMethod] + public async Task Service_Amqp_TokenCredentialAuth_Success() + { + // arrange + TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); + using DeviceClient deviceClient = testDevice.CreateDeviceClient(Client.TransportType.Mqtt); + await deviceClient.OpenAsync().ConfigureAwait(false); + + using var serviceClient = ServiceClient.Create( + Configuration.IoTHub.GetIotHubHostName(), + Configuration.IoTHub.GetClientSecretCredential(), + TransportType.Amqp); + + // act + await serviceClient.OpenAsync().ConfigureAwait(false); + using var message = new Message(Encoding.ASCII.GetBytes("Hello, Cloud!")); + await serviceClient.SendAsync(testDevice.Id, message); + + // cleanup + await testDevice.RemoveDeviceAsync().ConfigureAwait(false); + } + +#endif + } +} From 8c9d25323b4f627d726085b8b40394207e64bed9 Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Thu, 25 Feb 2021 16:29:14 -0800 Subject: [PATCH 27/29] test(service-client): Adding e2e tests for sas credential auth for IoT hub. (#1806) --- e2e/test/config/Configuration.IoTHub.cs | 72 ++++-- .../SasCredentialAuthenticationTests.cs | 221 ++++++++++++++++++ .../src/IotHubSasCredentialProperties.cs | 10 +- .../IotHubSasCredentialPropertiesTests.cs | 24 +- 4 files changed, 301 insertions(+), 26 deletions(-) create mode 100644 e2e/test/iothub/SasCredentialAuthenticationTests.cs diff --git a/e2e/test/config/Configuration.IoTHub.cs b/e2e/test/config/Configuration.IoTHub.cs index 782c2590f0..f0f11b1317 100644 --- a/e2e/test/config/Configuration.IoTHub.cs +++ b/e2e/test/config/Configuration.IoTHub.cs @@ -3,10 +3,15 @@ using System; using System.Security.Cryptography.X509Certificates; +using System.Net; +using System.Globalization; +using System.Text; +using System.Security.Cryptography; #if !NET451 using Azure.Identity; +using Azure; #endif @@ -37,6 +42,16 @@ public static ClientSecretCredential GetClientSecretCredential() GetValue("IOTHUB_CLIENT_SECRET")); } + public static string GetIotHubSharedAccessSignature(TimeSpan timeToLive) + { + ConnectionStringParser connectionString = new ConnectionStringParser(ConnectionString); + return GenerateSasToken( + connectionString.IotHubHostName, + connectionString.SharedAccessKey, + timeToLive, + connectionString.SharedAccessKeyName); + } + #endif public static X509Certificate2 GetCertificateWithPrivateKey() @@ -90,6 +105,39 @@ public static X509Certificate2 GetIntermediate2Certificate() /// public const string InvalidProxyServerAddress = "127.0.0.1:1234"; +#if !NET451 + + private static string GenerateSasToken(string resourceUri, string sharedAccessKey, TimeSpan timeToLive, string policyName = default) + { + DateTime epochTime = new DateTime(1970, 1, 1); + DateTime expiresOn = DateTime.UtcNow.Add(timeToLive); + TimeSpan secondsFromEpochTime = expiresOn.Subtract(epochTime); + long seconds = Convert.ToInt64(secondsFromEpochTime.TotalSeconds, CultureInfo.InvariantCulture); + string expiry = Convert.ToString(seconds, CultureInfo.InvariantCulture); + + string stringToSign = WebUtility.UrlEncode(resourceUri) + "\n" + expiry; + + HMACSHA256 hmac = new HMACSHA256(Convert.FromBase64String(sharedAccessKey)); + string signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign))); + + // SharedAccessSignature sr=ENCODED(dh://myiothub.azure-devices.net/a/b/c?myvalue1=a)&sig=&se=[&skn=] + string token = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}&se={2}", + WebUtility.UrlEncode(resourceUri), + WebUtility.UrlEncode(signature), + expiry); + + if (!string.IsNullOrWhiteSpace(policyName)) + { + token += "&skn=" + policyName; + } + + return token; + } + +#endif + public class ConnectionStringParser { public ConnectionStringParser(string connectionString) @@ -113,29 +161,23 @@ public ConnectionStringParser(string connectionString) DeviceID = part.Substring("DEVICEID=".Length); break; + case "SHAREDACCESSKEYNAME": + SharedAccessKeyName = part.Substring("SHAREDACCESSKEYNAME=".Length); + break; + default: throw new NotSupportedException("Unrecognized tag found in test ConnectionString."); } } } - public string IotHubHostName - { - get; - private set; - } + public string IotHubHostName { get; private set; } - public string DeviceID - { - get; - private set; - } + public string DeviceID { get; private set; } - public string SharedAccessKey - { - get; - private set; - } + public string SharedAccessKey { get; private set; } + + public string SharedAccessKeyName { get; private set; } } } } diff --git a/e2e/test/iothub/SasCredentialAuthenticationTests.cs b/e2e/test/iothub/SasCredentialAuthenticationTests.cs new file mode 100644 index 0000000000..56eb80e9a1 --- /dev/null +++ b/e2e/test/iothub/SasCredentialAuthenticationTests.cs @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Azure.Devices.Client; +using Microsoft.Azure.Devices.Common.Exceptions; +using Microsoft.Azure.Devices.E2ETests.Helpers; +using Microsoft.Azure.Devices.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Azure.Amqp; + +#if !NET451 + +using Microsoft.Rest; +using Azure; + +#endif + +using ClientOptions = Microsoft.Azure.Devices.Client.ClientOptions; + +namespace Microsoft.Azure.Devices.E2ETests.iothub +{ + /// + /// Tests to ensure authentication using SAS credential succeeds in all the clients. + /// + [TestClass] + [TestCategory("E2E")] + [TestCategory("IoTHub")] + public class SasCredentialAuthenticationTests : E2EMsTestBase + { + private readonly string _devicePrefix = $"E2E_{nameof(SasCredentialAuthenticationTests)}_"; + +#if !NET451 + + [LoggedTestMethod] + public async Task RegistryManager_Http_SasCredentialAuth_Success() + { + // arrange + string signature = Configuration.IoTHub.GetIotHubSharedAccessSignature(TimeSpan.FromHours(1)); + using var registryManager = RegistryManager.Create( + Configuration.IoTHub.GetIotHubHostName(), + new AzureSasCredential(signature)); + + var device = new Device(Guid.NewGuid().ToString()); + + // act + Device createdDevice = await registryManager.AddDeviceAsync(device).ConfigureAwait(false); + + // assert + Assert.IsNotNull(createdDevice); + + // cleanup + await registryManager.RemoveDeviceAsync(device.Id).ConfigureAwait(false); + } + + [LoggedTestMethod] + public async Task RegistryManager_Http_SasCredentialAuth_Renewed_Success() + { + // arrange + string signature = Configuration.IoTHub.GetIotHubSharedAccessSignature(TimeSpan.FromHours(-1)); + var sasCredential = new AzureSasCredential(signature); + using var registryManager = RegistryManager.Create( + Configuration.IoTHub.GetIotHubHostName(), + sasCredential); + + var device = new Device(Guid.NewGuid().ToString()); + + // act + try + { + await registryManager.AddDeviceAsync(device).ConfigureAwait(false); + Assert.Fail("The SAS token is expired so the call should fail with an exception"); + } + catch (UnauthorizedException) + { + // Expected to be unauthorized exception. + } + signature = Configuration.IoTHub.GetIotHubSharedAccessSignature(TimeSpan.FromHours(1)); + sasCredential.Update(signature); + Device createdDevice = await registryManager.AddDeviceAsync(device).ConfigureAwait(false); + + // assert + Assert.IsNotNull(createdDevice); + + // cleanup + await registryManager.RemoveDeviceAsync(device.Id).ConfigureAwait(false); + } + + [LoggedTestMethod] + public async Task JobClient_Http_SasCredentialAuth_Success() + { + // arrange + string signature = Configuration.IoTHub.GetIotHubSharedAccessSignature(TimeSpan.FromHours(1)); + using var jobClient = JobClient.Create( + Configuration.IoTHub.GetIotHubHostName(), + new AzureSasCredential(signature)); + + string jobId = "JOBSAMPLE" + Guid.NewGuid().ToString(); + string jobDeviceId = "JobsSample_Device"; + string query = $"DeviceId IN ['{jobDeviceId}']"; + Twin twin = new Twin(jobDeviceId); + + try + { + // act + JobResponse createJobResponse = await jobClient + .ScheduleTwinUpdateAsync( + jobId, + query, + twin, + DateTime.UtcNow, + (long)TimeSpan.FromMinutes(2).TotalSeconds) + .ConfigureAwait(false); + } + catch (ThrottlingException) + { + // Concurrent jobs can be rejected, but it still means authentication was successful. Ignore the exception. + } + } + + [LoggedTestMethod] + public async Task DigitalTwinClient_Http_SasCredentialAuth_Success() + { + // arrange + TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); + string thermostatModelId = "dtmi:com:example:TemperatureController;1"; + + // Create a device client instance initializing it with the "Thermostat" model. + var options = new ClientOptions + { + ModelId = thermostatModelId, + }; + using DeviceClient deviceClient = testDevice.CreateDeviceClient(Client.TransportType.Mqtt, options); + + // Call openAsync() to open the device's connection, so that the ModelId is sent over Mqtt CONNECT packet. + await deviceClient.OpenAsync().ConfigureAwait(false); + + string signature = Configuration.IoTHub.GetIotHubSharedAccessSignature(TimeSpan.FromHours(1)); + using var digitalTwinClient = DigitalTwinClient.Create( + Configuration.IoTHub.GetIotHubHostName(), + new AzureSasCredential(signature)); + + // act + HttpOperationResponse response = await digitalTwinClient + .GetDigitalTwinAsync(testDevice.Id) + .ConfigureAwait(false); + ThermostatTwin twin = response.Body; + + // assert + twin.Metadata.ModelId.Should().Be(thermostatModelId); + + // cleanup + await testDevice.RemoveDeviceAsync().ConfigureAwait(false); + } + + [LoggedTestMethod] + public async Task Service_Amqp_SasCredentialAuth_Success() + { + // arrange + TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); + using DeviceClient deviceClient = testDevice.CreateDeviceClient(Client.TransportType.Mqtt); + await deviceClient.OpenAsync().ConfigureAwait(false); + + string signature = Configuration.IoTHub.GetIotHubSharedAccessSignature(TimeSpan.FromHours(1)); + using var serviceClient = ServiceClient.Create( + Configuration.IoTHub.GetIotHubHostName(), + new AzureSasCredential(signature), + TransportType.Amqp); + + // act + await serviceClient.OpenAsync().ConfigureAwait(false); + using var message = new Message(Encoding.ASCII.GetBytes("Hello, Cloud!")); + + await serviceClient.SendAsync(testDevice.Id, message); + + // cleanup + await testDevice.RemoveDeviceAsync().ConfigureAwait(false); + } + + [LoggedTestMethod] + public async Task Service_Amqp_SasCredentialAuth_Renewed_Success() + { + // arrange + TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); + using DeviceClient deviceClient = testDevice.CreateDeviceClient(Client.TransportType.Mqtt); + await deviceClient.OpenAsync().ConfigureAwait(false); + + string signature = Configuration.IoTHub.GetIotHubSharedAccessSignature(TimeSpan.FromHours(-1)); + var sasCredential = new AzureSasCredential(signature); + using var serviceClient = ServiceClient.Create( + Configuration.IoTHub.GetIotHubHostName(), + sasCredential, + TransportType.Amqp); + + // act + try + { + await serviceClient.OpenAsync().ConfigureAwait(false); + Assert.Fail("The SAS token is expired so the call should fail with an exception"); + } + catch (AmqpException ex) when (ex.Error.Description.Contains("401")) + { + // Expected to get an unauthorized exception. + } + + signature = Configuration.IoTHub.GetIotHubSharedAccessSignature(TimeSpan.FromHours(1)); + sasCredential.Update(signature); + await serviceClient.OpenAsync().ConfigureAwait(false); + using var message = new Message(Encoding.ASCII.GetBytes("Hello, Cloud!")); + await serviceClient.SendAsync(testDevice.Id, message); + + // cleanup + await testDevice.RemoveDeviceAsync().ConfigureAwait(false); + } + +#endif + } +} diff --git a/iothub/service/src/IotHubSasCredentialProperties.cs b/iothub/service/src/IotHubSasCredentialProperties.cs index 756fe3865a..426f4348a3 100644 --- a/iothub/service/src/IotHubSasCredentialProperties.cs +++ b/iothub/service/src/IotHubSasCredentialProperties.cs @@ -51,7 +51,7 @@ public override Task GetTokenAsync(Uri namespaceAddress, string applie #else // Parse the SAS token to find the expiration date and time. - // SharedAccessSignature sr=ENCODED(dh://myiothub.azure-devices.net/a/b/c?myvalue1=a)&sig=&se=[&skn=] + // SharedAccessSignature sr=ENCODED(dh://myiothub.azure-devices.net/a/b/c?myvalue1=a)&sig=&se=[&skn=] var tokenParts = _credential.Signature.Split('&').ToList(); var expiresAtTokenPart = tokenParts.Where(tokenPart => tokenPart.StartsWith("se=", StringComparison.OrdinalIgnoreCase)); @@ -61,13 +61,17 @@ public override Task GetTokenAsync(Uri namespaceAddress, string applie } string expiresAtStr = expiresAtTokenPart.First().Split('=')[1]; - bool isSuccess = DateTime.TryParse(expiresAtStr, out DateTime expiresAt); + bool isSuccess = double.TryParse(expiresAtStr, out double secondsFromEpochTime); if (!isSuccess) { - throw new InvalidOperationException($"Invalid expiration time on {nameof(AzureSasCredential)} signature."); + throw new InvalidOperationException($"Invalid seconds from epoch time on {nameof(AzureSasCredential)} signature."); } + DateTime epochTime = new DateTime(1970, 1, 1); + TimeSpan timeToLiveFromEpochTime = TimeSpan.FromSeconds(secondsFromEpochTime); + DateTime expiresAt = epochTime.Add(timeToLiveFromEpochTime); + var token = new CbsToken( _credential.Signature, CbsConstants.IotHubSasTokenType, diff --git a/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs b/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs index 45d5ed8f6e..1dedc28808 100644 --- a/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs +++ b/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs @@ -30,22 +30,30 @@ public class IotHubSasCredentialPropertiesTests public async Task TestCbsTokenGeneration_Succeeds() { // arrange - DateTime expiresAtUtc = DateTime.UtcNow; - DateTime updatedExpiresAtUtc = DateTime.UtcNow.AddDays(1); + DateTime epochTime = new DateTime(1970, 1, 1); + DateTime expiresAt = DateTime.UtcNow.Add(TimeSpan.FromHours(1)); + TimeSpan secondsFromEpochTime = expiresAt.Subtract(epochTime); + long seconds = Convert.ToInt64(secondsFromEpochTime.TotalSeconds, CultureInfo.InvariantCulture); + string expiry = Convert.ToString(seconds, CultureInfo.InvariantCulture); + + DateTime updatedExpiresAt = DateTime.UtcNow.Add(TimeSpan.FromHours(2)); + TimeSpan updatedSecondsFromEpochTime = updatedExpiresAt.Subtract(epochTime); + long updatedSeconds = Convert.ToInt64(updatedSecondsFromEpochTime.TotalSeconds, CultureInfo.InvariantCulture); + string updatedExpiry = Convert.ToString(updatedSeconds, CultureInfo.InvariantCulture); string token = string.Format( CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}", WebUtility.UrlEncode(_hostName), WebUtility.UrlEncode("signature"), - expiresAtUtc); + expiry); string updatedToken = string.Format( CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}", WebUtility.UrlEncode(_hostName), WebUtility.UrlEncode("signature"), - updatedExpiresAtUtc); + updatedExpiry); var azureSasCredential = new AzureSasCredential(token); var iotHubSasCredentialProperties = new IotHubSasCredentialProperties(_hostName, azureSasCredential); @@ -57,8 +65,8 @@ public async Task TestCbsTokenGeneration_Succeeds() CbsToken updatedCbsToken = await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); // assert - cbsToken.ExpiresAtUtc.ToString().Should().Be(expiresAtUtc.ToString()); - updatedCbsToken.ExpiresAtUtc.ToString().Should().Be(updatedExpiresAtUtc.ToString()); + Math.Abs(expiresAt.Subtract(cbsToken.ExpiresAtUtc).TotalSeconds).Should().BeLessThan(1); + Math.Abs(updatedExpiresAt.Subtract(updatedCbsToken.ExpiresAtUtc).TotalSeconds).Should().BeLessThan(1); } [TestMethod] @@ -80,12 +88,12 @@ public async Task TestCbsTokenGeneration_InvalidExpirationDateTimeFormat_Fails() // act await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); - Assert.Fail("The parsing of date time in invalid format on the SAS token should have caused an exception."); + Assert.Fail("The parsing of seconds from string to long should have caused an exception."); } catch (InvalidOperationException ex) { // assert - ex.Message.Should().Be($"Invalid expiration time on {nameof(AzureSasCredential)} signature."); + ex.Message.Should().Be($"Invalid seconds from epoch time on {nameof(AzureSasCredential)} signature."); } } From b0652d11287c476144820aa40c41bc47963ffe8a Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Fri, 26 Feb 2021 13:13:53 -0800 Subject: [PATCH 28/29] feature(service-client): Adding chaching for aad tokens. (#1807) --- iothub/service/src/Common/TokenHelper.cs | 21 ++++++++ .../DigitalTwinTokenCredential.cs | 18 ++++++- .../src/IotHubTokenCredentialProperties.cs | 20 +++++++- iothub/service/tests/TestTokenCredential.cs | 40 +++++++++++++++ iothub/service/tests/TokenHelperTests.cs | 49 +++++++++++++++++++ 5 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 iothub/service/src/Common/TokenHelper.cs create mode 100644 iothub/service/tests/TestTokenCredential.cs create mode 100644 iothub/service/tests/TokenHelperTests.cs diff --git a/iothub/service/src/Common/TokenHelper.cs b/iothub/service/src/Common/TokenHelper.cs new file mode 100644 index 0000000000..614ac390d5 --- /dev/null +++ b/iothub/service/src/Common/TokenHelper.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; + +namespace Microsoft.Azure.Devices.Common +{ + internal static class TokenHelper + { + /// + /// Determines if the given token expiry date time is close to expiry. The date and time is + /// considered close to expiry if it has less than 10 minutes relative to the current time. + /// + /// The token expiration date and time. + /// True if the token expiry has less than 10 minutes relative to the current time, otherwise false. + public static bool IsCloseToExpiry(DateTimeOffset expiry) + { + TimeSpan timeToExpiry = expiry - DateTimeOffset.UtcNow; + return timeToExpiry.TotalMinutes < 10; + } + } +} diff --git a/iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs index a6611e99f7..b731a01385 100644 --- a/iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs +++ b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs @@ -6,6 +6,7 @@ using System.Threading; using Azure.Core; using Microsoft.Azure.Devices.Authentication; +using Microsoft.Azure.Devices.Common; namespace Microsoft.Azure.Devices.DigitalTwin.Authentication { @@ -16,6 +17,8 @@ namespace Microsoft.Azure.Devices.DigitalTwin.Authentication /// internal class DigitalTwinTokenCredential : DigitalTwinServiceClientCredentials { + private readonly object _tokenLock = new object(); + private AccessToken? _cachedAccessToken; private TokenCredential _credential; public DigitalTwinTokenCredential(TokenCredential credential) @@ -25,8 +28,19 @@ public DigitalTwinTokenCredential(TokenCredential credential) public override string GetAuthorizationHeader() { - AccessToken token = _credential.GetToken(new TokenRequestContext(), new CancellationToken()); - return $"Bearer {token.Token}"; + lock (_tokenLock) + { + // A new token is generated if it is the first time or the cached token is close to expiry. + if (!_cachedAccessToken.HasValue + || TokenHelper.IsCloseToExpiry(_cachedAccessToken.Value.ExpiresOn)) + { + _cachedAccessToken = _credential.GetToken( + new TokenRequestContext(), + new CancellationToken()); + } + } + + return $"Bearer {_cachedAccessToken.Value.Token}"; } } } diff --git a/iothub/service/src/IotHubTokenCredentialProperties.cs b/iothub/service/src/IotHubTokenCredentialProperties.cs index e0b4da50a7..a75e0b250c 100644 --- a/iothub/service/src/IotHubTokenCredentialProperties.cs +++ b/iothub/service/src/IotHubTokenCredentialProperties.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Azure.Amqp; using System.Threading; +using Microsoft.Azure.Devices.Common; #if !NET451 @@ -24,6 +25,8 @@ internal class IotHubTokenCrendentialProperties #if !NET451 private const string _tokenType = "jwt"; private readonly TokenCredential _credential; + private readonly object _tokenLock = new object(); + private AccessToken? _cachedAccessToken; #endif #if NET451 @@ -41,20 +44,33 @@ public IotHubTokenCrendentialProperties(string hostName, TokenCredential credent #endif + // The HTTP protocol uses this method to get the bearer token for authentication. public override string GetAuthorizationHeader() { #if NET451 throw new InvalidOperationException($"TokenCredential is not supported on NET451"); #else - AccessToken token = _credential.GetToken(new TokenRequestContext(), new CancellationToken()); - return $"Bearer {token.Token}"; + lock (_tokenLock) + { + // A new token is generated if it is the first time or the cached token is close to expiry. + if (!_cachedAccessToken.HasValue + || TokenHelper.IsCloseToExpiry(_cachedAccessToken.Value.ExpiresOn)) + { + _cachedAccessToken = _credential.GetToken( + new TokenRequestContext(), + new CancellationToken()); + } + } + + return $"Bearer {_cachedAccessToken.Value.Token}"; #endif } #pragma warning disable CS1998 // Disabled as we need to throw exception for NET 451. + // The AMQP protocol uses this method to get a CBS token for authentication. public async override Task GetTokenAsync(Uri namespaceAddress, string appliesTo, string[] requiredClaims) { #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously diff --git a/iothub/service/tests/TestTokenCredential.cs b/iothub/service/tests/TestTokenCredential.cs new file mode 100644 index 0000000000..f8ddf15f7f --- /dev/null +++ b/iothub/service/tests/TestTokenCredential.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +#if !NET451 + +using Azure.Core; + +namespace Microsoft.Azure.Devices.Tests +{ + /// + /// Implementation of TokenCredential class for unit tests. + /// + public class TestTokenCredential : TokenCredential + { + public const string TokenValue = "token"; + private DateTimeOffset _expiry; + + public TestTokenCredential(DateTimeOffset expiry) + { + _expiry = expiry; + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new AccessToken(TokenValue, _expiry); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(GetToken(requestContext, cancellationToken)); + } + } +} + +#endif diff --git a/iothub/service/tests/TokenHelperTests.cs b/iothub/service/tests/TokenHelperTests.cs new file mode 100644 index 0000000000..083868020b --- /dev/null +++ b/iothub/service/tests/TokenHelperTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading; +using FluentAssertions; +using Microsoft.Azure.Devices.Common; + +#if !NET451 + +using Microsoft.Azure.Devices.DigitalTwin.Authentication; +using Azure.Core; + +#endif + +namespace Microsoft.Azure.Devices.Tests +{ + [TestClass] + [TestCategory("Unit")] + public class TokenHelperTests + { +#if !NET451 + + [TestMethod] + [DataRow(15, false)] // 15 minutes to expiry + [DataRow(2, true)] // 2 minutes to expiry + [DataRow(-2, true)] // Expired 2 minutes ago + [DataRow(-15, true)] // Expired 15 minutes ago + public void TestIsTokenCloseToExpiry_Succeeds(int offsetInMinutes, bool expectedIsExpired) + { + // arrange + var expiry = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(offsetInMinutes); + var tokenCredential = new TestTokenCredential(expiry); + + // act + AccessToken token = tokenCredential.GetToken( + default(TokenRequestContext), + new CancellationToken()); + bool isExpired = TokenHelper.IsCloseToExpiry(token.ExpiresOn); + + // assert + isExpired.Should().Be(expectedIsExpired); + } + +#endif + } +} From 30ffbc9d1b9ea43b318a3175f851218e8aea039e Mon Sep 17 00:00:00 2001 From: Sindhu Nagesh Date: Thu, 4 Mar 2021 13:06:42 -0800 Subject: [PATCH 29/29] fix(service-client)- Add IoT hub token scope. (#1812) --- common/src/service/CommonConstants.cs | 14 +++++++++++++- .../Authentication/DigitalTwinTokenCredential.cs | 2 +- .../service/src/IotHubTokenCredentialProperties.cs | 6 ++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/common/src/service/CommonConstants.cs b/common/src/service/CommonConstants.cs index e18cd1ee09..35de847097 100644 --- a/common/src/service/CommonConstants.cs +++ b/common/src/service/CommonConstants.cs @@ -5,13 +5,14 @@ namespace Microsoft.Azure.Devices.Common { using System; - static class CommonConstants + internal static class CommonConstants { // Custom HTTP response contents internal const string ErrorCode = "errorCode"; // TODO: move these to ConfigProvider public const string DeviceAudienceFormat = "{0}/devices/{1}"; + public const string MediaTypeForDeviceManagementApis = "application/json"; public const string AmqpsScheme = "amqps"; public const string AmqpScheme = "amqp"; @@ -23,11 +24,13 @@ static class CommonConstants // IotHub WindowsFabric Constants public const int WindowsFabricRetryLimit = 20; + public const int WindowsFabricRetryWaitInMilliseconds = 3000; public const int WindowsFabricClientConnectionPort = 19000; // AzureStorage Constants public const int AzureStorageRetryLimit = 3; + public const int AzureStorageRetryWaitInMilliseconds = 3000; public const string IotHubApplicationName = "fabric:/microsoft.azure.devices.container"; @@ -42,19 +45,23 @@ static class CommonConstants // EventHub public const int EventHubEndpointPortNumber = 5671; + public const string EventHubConnectionStringTemplate = "{0};PartitionCount={1}"; // Namespace paths public const string ResourceProviderNamespace = "Microsoft.Devices"; + public const string ResourceProviderServiceResourceType = ResourceProviderNamespace + "/IotHubs"; public const string ResourceProviderBasePathTemplate = "/subscriptions/{0}/resourceGroups/{1}/providers/" + ResourceProviderServiceResourceType + "/{2}"; // Runtime Retry Constants public const int RuntimeRetryLimit = 3; + public const int RuntimeRetryWaitInMilliseconds = 5000; // Device URI Templates public const string DeviceEventPathTemplate = "/devices/{0}/messages/events"; + public const string DeviceBoundPathTemplate = "/devices/{0}/messages/deviceBound"; public const string DeviceBoundPathCompleteTemplate = DeviceBoundPathTemplate + "/{1}"; public const string DeviceBoundPathAbandonTemplate = DeviceBoundPathCompleteTemplate + "/abandon"; @@ -65,6 +72,7 @@ static class CommonConstants // IotHub provisioning terminal states (CSM/ARM) public const string ProvisioningStateSucceed = "Succeeded"; + public const string ProvisioningStateFailed = "Failed"; public const string ProvisioningStateCanceled = "Canceled"; @@ -79,6 +87,7 @@ static class CommonConstants // Service configurable parameters public const string PartitionCount = "PartitionCount"; + public const string TargetReplicaSetSize = "TargetReplicaSetSize"; public const string MinReplicaSetSize = "MinReplicaSetSize"; public const string SkuMaxUnitOverride = "SkuMaxUnitOverride"; @@ -118,8 +127,11 @@ static class CommonConstants // Custom HTTP headers public const string IotHubErrorCode = "IotHubErrorCode"; + public const string HttpErrorCodeName = "iothub-errorcode"; + public static readonly string[] IotHubAadTokenScopes = new string[] { "https://iothubs.azure.net/.default" }; + //Service Analytics related public static class ServiceAnalytics { diff --git a/iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs index b731a01385..3b0bc31fe7 100644 --- a/iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs +++ b/iothub/service/src/DigitalTwin/Authentication/DigitalTwinTokenCredential.cs @@ -35,7 +35,7 @@ public override string GetAuthorizationHeader() || TokenHelper.IsCloseToExpiry(_cachedAccessToken.Value.ExpiresOn)) { _cachedAccessToken = _credential.GetToken( - new TokenRequestContext(), + new TokenRequestContext(CommonConstants.IotHubAadTokenScopes), new CancellationToken()); } } diff --git a/iothub/service/src/IotHubTokenCredentialProperties.cs b/iothub/service/src/IotHubTokenCredentialProperties.cs index a75e0b250c..00c3c13605 100644 --- a/iothub/service/src/IotHubTokenCredentialProperties.cs +++ b/iothub/service/src/IotHubTokenCredentialProperties.cs @@ -58,7 +58,7 @@ public override string GetAuthorizationHeader() || TokenHelper.IsCloseToExpiry(_cachedAccessToken.Value.ExpiresOn)) { _cachedAccessToken = _credential.GetToken( - new TokenRequestContext(), + new TokenRequestContext(CommonConstants.IotHubAadTokenScopes), new CancellationToken()); } } @@ -78,7 +78,9 @@ public async override Task GetTokenAsync(Uri namespaceAddress, string throw new InvalidOperationException($"TokenCredential is not supported on NET451"); #else - AccessToken token = await _credential.GetTokenAsync(new TokenRequestContext(), new CancellationToken()).ConfigureAwait(false); + AccessToken token = await _credential.GetTokenAsync( + new TokenRequestContext(CommonConstants.IotHubAadTokenScopes), + new CancellationToken()).ConfigureAwait(false); return new CbsToken( token.Token, _tokenType,