From 987e0a2792f39b970eaf54d08d399e6909533498 Mon Sep 17 00:00:00 2001 From: Nick Patilsen <110431552+patilsnr@users.noreply.github.com> Date: Fri, 4 Aug 2023 13:16:57 -0700 Subject: [PATCH] Add OnBehalfOf authentication to V2 for edgehub (#3353) * add onBehalfOf and update sas utils * added test file * fix test errors p1 * comments * unit tests * misc * variable names * misc --- ...lientAuthenticationForEdgeHubOnBehalfOf.cs | 60 +++++++++++++++++++ .../Security/SharedAccessSignatureBuilder.cs | 25 ++++++++ .../EdgeDeviceOnBehalfOfTests.cs | 55 +++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 iothub/device/src/Authentication/ClientAuthenticationForEdgeHubOnBehalfOf.cs create mode 100644 iothub/device/tests/Authentication/EdgeDeviceOnBehalfOfTests.cs diff --git a/iothub/device/src/Authentication/ClientAuthenticationForEdgeHubOnBehalfOf.cs b/iothub/device/src/Authentication/ClientAuthenticationForEdgeHubOnBehalfOf.cs new file mode 100644 index 0000000000..4e79f7ad00 --- /dev/null +++ b/iothub/device/src/Authentication/ClientAuthenticationForEdgeHubOnBehalfOf.cs @@ -0,0 +1,60 @@ +// 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.Tasks; + +namespace Microsoft.Azure.Devices.Client.Authentication +{ + /// + /// Authentication method that generates shared access signature (SAS) token with refresh, based on a provided shared access key (SAK). + /// Build for using $edgeHub in IoT Edge to authenticate on behalf of leaf devices or modules only. + /// + public class ClientAuthenticationForEdgeHubOnBehalfOf : ClientAuthenticationWithSharedAccessKeyRefresh + { + /// + /// Creates an instance of this class. + /// + /// Shared access key value for the $edgehub module. + /// Identifier of the higher-layer parent device that connects directly to IoT Hub. + /// Device identifier of the lower-layer device that authenticates through the parent IoT device. + /// Module identifier. + /// + /// The suggested time to live value for the generated SAS tokens. + /// The default value is 1 hour. + /// + /// + /// The time buffer before expiry when the token should be renewed, expressed as a percentage of the time to live. + /// The default behavior is that the token will be renewed when it has 15% or less of its lifespan left. + /// + public ClientAuthenticationForEdgeHubOnBehalfOf( + string sharedAccessKey, + string parentDeviceId, + string deviceId, + string moduleId = null, + TimeSpan sasTokenTimeToLive = default, + int sasTokenRenewalBuffer = default) + : base( + sharedAccessKey, + deviceId, + moduleId, + sasTokenTimeToLive, + sasTokenRenewalBuffer) + { + ParentDeviceId = parentDeviceId; + } + + /// + /// Gets the shared access key name. + /// + public string ParentDeviceId { get; private set; } + + /// + protected override Task SafeCreateNewTokenAsync(string iotHub, TimeSpan suggestedTimeToLive) + { + string audience = SharedAccessSignatureBuilder.BuildAudience(iotHub, ParentDeviceId, "$edgeHub"); + string sasToken = SharedAccessSignatureBuilder.BuildSignature(null, SharedAccessKey, null, TimeSpan.FromMinutes(60), audience, null, null); + return Task.FromResult(sasToken); + } + } +} diff --git a/iothub/device/src/Authentication/Security/SharedAccessSignatureBuilder.cs b/iothub/device/src/Authentication/Security/SharedAccessSignatureBuilder.cs index a513b77c82..83439c9c7c 100644 --- a/iothub/device/src/Authentication/Security/SharedAccessSignatureBuilder.cs +++ b/iothub/device/src/Authentication/Security/SharedAccessSignatureBuilder.cs @@ -145,5 +145,30 @@ internal static string Sign(string requestString, string key) using var algorithm = new HMACSHA256(Convert.FromBase64String(key)); return Convert.ToBase64String(algorithm.ComputeHash(Encoding.UTF8.GetBytes(requestString))); } + + internal static string GetSignature(string encodedURI, string key, long expiry) + { + return Sign($"{encodedURI}\n{expiry}", key); + } + + internal static string GetToken(string encodedURI, string key, long expiry = 0, int defaultTimeToLive = 60) + { + long expiryValue = (expiry == 0) ? DateTimeOffset.UtcNow.AddMinutes(defaultTimeToLive).ToUnixTimeSeconds() : expiry; + string sig = WebUtility.UrlEncode(Sign($"{encodedURI}\n{expiryValue}", key)); + + return $"SharedAccessSignature sr={encodedURI}&sig={sig}&se={expiryValue}"; + } + + internal static string GetDeviceToken(string hostname, string deviceId, string key, string moduleId = null, long expiry = 0) + { + return GetToken(GetDeviceResourceURI(hostname, deviceId, moduleId), key, expiry); + } + + private static string GetDeviceResourceURI(string hostname, string deviceId, string moduleId) + { + return moduleId == null + ? WebUtility.UrlEncode(FormattableString.Invariant($"{hostname}/devices/{WebUtility.UrlEncode(deviceId)}")) + : WebUtility.UrlEncode(FormattableString.Invariant($"{hostname}/devices/{WebUtility.UrlEncode(deviceId)}/modules/{WebUtility.UrlEncode(moduleId)}")); + } } } diff --git a/iothub/device/tests/Authentication/EdgeDeviceOnBehalfOfTests.cs b/iothub/device/tests/Authentication/EdgeDeviceOnBehalfOfTests.cs new file mode 100644 index 0000000000..bb64edddd0 --- /dev/null +++ b/iothub/device/tests/Authentication/EdgeDeviceOnBehalfOfTests.cs @@ -0,0 +1,55 @@ +using Microsoft.Azure.Devices.Client; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Text; +using System.Threading.Tasks; +using System; +using FluentAssertions; +using Microsoft.Azure.Devices.Client.Authentication; + +namespace Microsoft.Azure.Devices.Client.Tests.OnBehalfOf +{ + [TestClass] + public class EdgeDeviceOnBehalfOfTests + { + private static string _testKey => Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.Empty.ToString("N"))); + + [TestMethod] + [DataRow("test-edge-device", "test-leaf-device", "test-leaf-device-module")] + [DataRow("test-edge-device", null, "test-edge-module")] + public async Task ConnectDeviceOnBehalfOf_Amqp(string edgeDeviceId, string leafDeviceId, string edgeModuleId) + { + var edgeHubCs = new IotHubConnectionString("e4k-hub.azure-devices.net", null, edgeDeviceId, edgeModuleId, null, _testKey, null); + leafDeviceId ??= edgeDeviceId; + + IAuthenticationMethod leafAuth = new ClientAuthenticationForEdgeHubOnBehalfOf( + edgeHubCs.SharedAccessKey!, + edgeHubCs.DeviceId!, + leafDeviceId, + edgeModuleId, + TimeSpan.FromMinutes(10), + 5); + + IotHubModuleClient leafClient = new(edgeHubCs.IotHubHostName, leafAuth, + new IotHubClientOptions( + new IotHubClientAmqpSettings + { + ConnectionPoolSettings = new AmqpConnectionPoolSettings() + { + UsePooling = true, + MaxPoolSize = 10 + } + })); + + await leafClient.OpenAsync(); + long tick = Environment.TickCount; + await leafClient.UpdateReportedPropertiesAsync(new ReportedProperties { ["tick"] = tick }); + var twin = await leafClient.GetTwinPropertiesAsync(); + twin.Should().NotBeNull(); + twin.Reported.Should().NotBeNull(); + twin.Reported["tick"].Should().NotBeNull(); + tick.Should().Be((long)twin.Reported["tick"]); + await leafClient.CloseAsync(); + await leafClient.DisposeAsync(); + } + } +}