Skip to content

Commit 987e0a2

Browse files
authored
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
1 parent b15aab8 commit 987e0a2

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.Azure.Devices.Client.Authentication
8+
{
9+
/// <summary>
10+
/// Authentication method that generates shared access signature (SAS) token with refresh, based on a provided shared access key (SAK).
11+
/// Build for using $edgeHub in IoT Edge to authenticate on behalf of leaf devices or modules only.
12+
/// </summary>
13+
public class ClientAuthenticationForEdgeHubOnBehalfOf : ClientAuthenticationWithSharedAccessKeyRefresh
14+
{
15+
/// <summary>
16+
/// Creates an instance of this class.
17+
/// </summary>
18+
/// <param name="sharedAccessKey">Shared access key value for the $edgehub module.</param>
19+
/// <param name="parentDeviceId">Identifier of the higher-layer parent device that connects directly to IoT Hub.</param>
20+
/// <param name="deviceId">Device identifier of the lower-layer device that authenticates through the parent IoT device.</param>
21+
/// <param name="moduleId">Module identifier.</param>
22+
/// <param name="sasTokenTimeToLive">
23+
/// The suggested time to live value for the generated SAS tokens.
24+
/// The default value is 1 hour.
25+
/// </param>
26+
/// <param name="sasTokenRenewalBuffer">
27+
/// The time buffer before expiry when the token should be renewed, expressed as a percentage of the time to live.
28+
/// The default behavior is that the token will be renewed when it has 15% or less of its lifespan left.
29+
/// </param>
30+
public ClientAuthenticationForEdgeHubOnBehalfOf(
31+
string sharedAccessKey,
32+
string parentDeviceId,
33+
string deviceId,
34+
string moduleId = null,
35+
TimeSpan sasTokenTimeToLive = default,
36+
int sasTokenRenewalBuffer = default)
37+
: base(
38+
sharedAccessKey,
39+
deviceId,
40+
moduleId,
41+
sasTokenTimeToLive,
42+
sasTokenRenewalBuffer)
43+
{
44+
ParentDeviceId = parentDeviceId;
45+
}
46+
47+
/// <summary>
48+
/// Gets the shared access key name.
49+
/// </summary>
50+
public string ParentDeviceId { get; private set; }
51+
52+
///<inheritdoc/>
53+
protected override Task<string> SafeCreateNewTokenAsync(string iotHub, TimeSpan suggestedTimeToLive)
54+
{
55+
string audience = SharedAccessSignatureBuilder.BuildAudience(iotHub, ParentDeviceId, "$edgeHub");
56+
string sasToken = SharedAccessSignatureBuilder.BuildSignature(null, SharedAccessKey, null, TimeSpan.FromMinutes(60), audience, null, null);
57+
return Task.FromResult(sasToken);
58+
}
59+
}
60+
}

iothub/device/src/Authentication/Security/SharedAccessSignatureBuilder.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,5 +145,30 @@ internal static string Sign(string requestString, string key)
145145
using var algorithm = new HMACSHA256(Convert.FromBase64String(key));
146146
return Convert.ToBase64String(algorithm.ComputeHash(Encoding.UTF8.GetBytes(requestString)));
147147
}
148+
149+
internal static string GetSignature(string encodedURI, string key, long expiry)
150+
{
151+
return Sign($"{encodedURI}\n{expiry}", key);
152+
}
153+
154+
internal static string GetToken(string encodedURI, string key, long expiry = 0, int defaultTimeToLive = 60)
155+
{
156+
long expiryValue = (expiry == 0) ? DateTimeOffset.UtcNow.AddMinutes(defaultTimeToLive).ToUnixTimeSeconds() : expiry;
157+
string sig = WebUtility.UrlEncode(Sign($"{encodedURI}\n{expiryValue}", key));
158+
159+
return $"SharedAccessSignature sr={encodedURI}&sig={sig}&se={expiryValue}";
160+
}
161+
162+
internal static string GetDeviceToken(string hostname, string deviceId, string key, string moduleId = null, long expiry = 0)
163+
{
164+
return GetToken(GetDeviceResourceURI(hostname, deviceId, moduleId), key, expiry);
165+
}
166+
167+
private static string GetDeviceResourceURI(string hostname, string deviceId, string moduleId)
168+
{
169+
return moduleId == null
170+
? WebUtility.UrlEncode(FormattableString.Invariant($"{hostname}/devices/{WebUtility.UrlEncode(deviceId)}"))
171+
: WebUtility.UrlEncode(FormattableString.Invariant($"{hostname}/devices/{WebUtility.UrlEncode(deviceId)}/modules/{WebUtility.UrlEncode(moduleId)}"));
172+
}
148173
}
149174
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using Microsoft.Azure.Devices.Client;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using System;
6+
using FluentAssertions;
7+
using Microsoft.Azure.Devices.Client.Authentication;
8+
9+
namespace Microsoft.Azure.Devices.Client.Tests.OnBehalfOf
10+
{
11+
[TestClass]
12+
public class EdgeDeviceOnBehalfOfTests
13+
{
14+
private static string _testKey => Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.Empty.ToString("N")));
15+
16+
[TestMethod]
17+
[DataRow("test-edge-device", "test-leaf-device", "test-leaf-device-module")]
18+
[DataRow("test-edge-device", null, "test-edge-module")]
19+
public async Task ConnectDeviceOnBehalfOf_Amqp(string edgeDeviceId, string leafDeviceId, string edgeModuleId)
20+
{
21+
var edgeHubCs = new IotHubConnectionString("e4k-hub.azure-devices.net", null, edgeDeviceId, edgeModuleId, null, _testKey, null);
22+
leafDeviceId ??= edgeDeviceId;
23+
24+
IAuthenticationMethod leafAuth = new ClientAuthenticationForEdgeHubOnBehalfOf(
25+
edgeHubCs.SharedAccessKey!,
26+
edgeHubCs.DeviceId!,
27+
leafDeviceId,
28+
edgeModuleId,
29+
TimeSpan.FromMinutes(10),
30+
5);
31+
32+
IotHubModuleClient leafClient = new(edgeHubCs.IotHubHostName, leafAuth,
33+
new IotHubClientOptions(
34+
new IotHubClientAmqpSettings
35+
{
36+
ConnectionPoolSettings = new AmqpConnectionPoolSettings()
37+
{
38+
UsePooling = true,
39+
MaxPoolSize = 10
40+
}
41+
}));
42+
43+
await leafClient.OpenAsync();
44+
long tick = Environment.TickCount;
45+
await leafClient.UpdateReportedPropertiesAsync(new ReportedProperties { ["tick"] = tick });
46+
var twin = await leafClient.GetTwinPropertiesAsync();
47+
twin.Should().NotBeNull();
48+
twin.Reported.Should().NotBeNull();
49+
twin.Reported["tick"].Should().NotBeNull();
50+
tick.Should().Be((long)twin.Reported["tick"]);
51+
await leafClient.CloseAsync();
52+
await leafClient.DisposeAsync();
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)