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();
+ }
+ }
+}