Skip to content

Commit

Permalink
Add OnBehalfOf authentication to V2 for edgehub (#3353)
Browse files Browse the repository at this point in the history
* add onBehalfOf and update sas utils

* added test file

* fix test errors p1

* comments

* unit tests

* misc

* variable names

* misc
  • Loading branch information
patilsnr authored Aug 4, 2023
1 parent b15aab8 commit 987e0a2
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
public class ClientAuthenticationForEdgeHubOnBehalfOf : ClientAuthenticationWithSharedAccessKeyRefresh
{
/// <summary>
/// Creates an instance of this class.
/// </summary>
/// <param name="sharedAccessKey">Shared access key value for the $edgehub module.</param>
/// <param name="parentDeviceId">Identifier of the higher-layer parent device that connects directly to IoT Hub.</param>
/// <param name="deviceId">Device identifier of the lower-layer device that authenticates through the parent IoT device.</param>
/// <param name="moduleId">Module identifier.</param>
/// <param name="sasTokenTimeToLive">
/// The suggested time to live value for the generated SAS tokens.
/// The default value is 1 hour.
/// </param>
/// <param name="sasTokenRenewalBuffer">
/// 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.
/// </param>
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;
}

/// <summary>
/// Gets the shared access key name.
/// </summary>
public string ParentDeviceId { get; private set; }

///<inheritdoc/>
protected override Task<string> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)}"));
}
}
}
55 changes: 55 additions & 0 deletions iothub/device/tests/Authentication/EdgeDeviceOnBehalfOfTests.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}

0 comments on commit 987e0a2

Please sign in to comment.