Skip to content

Commit

Permalink
Merge pull request #2015 from Azure/abmisr/previewUpdate
Browse files Browse the repository at this point in the history
Bring updates from master to preview
  • Loading branch information
abhipsaMisra authored Jun 8, 2021
2 parents 0b97aea + 86a022a commit 8ee81c8
Show file tree
Hide file tree
Showing 28 changed files with 527 additions and 93 deletions.
2 changes: 0 additions & 2 deletions common/src/service/CommonConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ internal static class CommonConstants

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
{
Expand Down
133 changes: 107 additions & 26 deletions e2e/test/iothub/AuthenticationWithTokenRefreshDisposalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,38 +55,115 @@ public async Task DeviceSak_ReusableAuthenticationMethod_SingleDevicePerConnecti
[LoggedTestMethod]
public async Task DeviceSak_ReusableAuthenticationMethod_MuxedDevicesPerConnection_Amqp()
{
await ReuseAuthenticationMethod_MuxedDevices(Client.TransportType.Amqp_Tcp_Only, 2);
await ReuseAuthenticationMethod_MuxedDevices(Client.TransportType.Amqp_Tcp_Only, 2).ConfigureAwait(false); ;
}

[LoggedTestMethod]
public async Task DeviceSak_ReusableAuthenticationMethod_MuxedDevicesPerConnection_AmqpWs()
{
await ReuseAuthenticationMethod_MuxedDevices(Client.TransportType.Amqp_WebSocket_Only, 2);
await ReuseAuthenticationMethod_MuxedDevices(Client.TransportType.Amqp_WebSocket_Only, 2).ConfigureAwait(false); ;
}

[LoggedTestMethod]
public async Task DeviceClient_AuthenticationMethodDisposesTokenRefresher_Http()
{
await AuthenticationMethodDisposesTokenRefresher(Client.TransportType.Http1).ConfigureAwait(false);
}

[LoggedTestMethod]
public async Task DeviceClient_AuthenticationMethodDisposesTokenRefresher_Amqp()
{
await AuthenticationMethodDisposesTokenRefresher(Client.TransportType.Amqp_Tcp_Only).ConfigureAwait(false);
}

[LoggedTestMethod]
public async Task DeviceClient_AuthenticationMethodDisposesTokenRefresher_AmqpWs()
{
await AuthenticationMethodDisposesTokenRefresher(Client.TransportType.Amqp_WebSocket_Only).ConfigureAwait(false);
}

// Even on encountering an exception, the MQTT layer keeps on reattempting CONNECT when communicating via DotNetty's TCP stack.
// As a result, instead of throwing the actual exception encountered an IotHubCommunicationException is thrown (on operation timeout).
// This is not an issue when the communication is over websockets (where we use the sdk's websocket implementation).
// This test has been ignored until we root-cause the issue on DotNetty's TCP stack.
[Ignore]
[LoggedTestMethod]
public async Task DeviceClient_AuthenticationMethodDisposesTokenRefresher_Mqtt()
{
await AuthenticationMethodDisposesTokenRefresher(Client.TransportType.Mqtt_Tcp_Only).ConfigureAwait(false);
}

[LoggedTestMethod]
public async Task DeviceClient_AuthenticationMethodDisposesTokenRefresher_MqttWs()
{
await AuthenticationMethodDisposesTokenRefresher(Client.TransportType.Mqtt_WebSocket_Only).ConfigureAwait(false);
}

private async Task AuthenticationMethodDisposesTokenRefresher(Client.TransportType transport)
{
AuthenticationWithTokenRefresh authenticationMethod = null;
DeviceClient testClient = null;
try
{
TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false);
authenticationMethod = new DeviceAuthenticationSasToken(testDevice.ConnectionString, disposeWithClient: true);

// Create an instance of the device client, send a test message and then close and dispose it.
testClient = DeviceClient.Create(testDevice.IoTHubHostName, authenticationMethod, transport);
using var message1 = new Client.Message();
await testClient.SendEventAsync(message1).ConfigureAwait(false);
await testClient.CloseAsync();
testClient.Dispose();
testClient = null;
Logger.Trace("Test with instance 1 completed");

// Perform the same steps again, reusing the previously created authentication method instance.
// Since the default behavior is to dispose AuthenticationWithTokenRefresh authentication method on DeviceClient disposal,
// this should now throw an ObjectDisposedException.
testClient = DeviceClient.Create(testDevice.IoTHubHostName, authenticationMethod, transport);
using var message2 = new Client.Message();

Func<Task> act = async () => await testClient.SendEventAsync(message2).ConfigureAwait(false);
await act.Should().ThrowAsync<ObjectDisposedException>();
}
finally
{
testClient?.Dispose();
authenticationMethod?.Dispose();
}
}

private async Task ReuseAuthenticationMethod_SingleDevice(Client.TransportType transport)
{
TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false);
var authenticationMethod = new DeviceAuthenticationSasToken(testDevice.ConnectionString);

// Create an instance of the device client, send a test message and then close and dispose it.
DeviceClient deviceClient = DeviceClient.Create(testDevice.IoTHubHostName, authenticationMethod, transport);
using var message1 = new Client.Message();
await deviceClient.SendEventAsync(message1).ConfigureAwait(false);
await deviceClient.CloseAsync();
deviceClient.Dispose();
Logger.Trace("Test with instance 1 completed");

// Perform the same steps again, reusing the previously created authentication method instance to ensure
// that the sdk did not dispose the user supplied authentication method instance.
DeviceClient deviceClient2 = DeviceClient.Create(testDevice.IoTHubHostName, authenticationMethod, transport);
using var message2 = new Client.Message();
await deviceClient2.SendEventAsync(message2).ConfigureAwait(false);
await deviceClient2.CloseAsync();
deviceClient2.Dispose();
Logger.Trace("Test with instance 2 completed, reused the previously created authentication method instance for the device client.");

authenticationMethod.Dispose();
AuthenticationWithTokenRefresh authenticationMethod = null;
DeviceClient testClient = null;
try
{
TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false);
authenticationMethod = new DeviceAuthenticationSasToken(testDevice.ConnectionString, disposeWithClient: false);

// Create an instance of the device client, send a test message and then close and dispose it.
testClient = DeviceClient.Create(testDevice.IoTHubHostName, authenticationMethod, transport);
using var message1 = new Client.Message();
await testClient.SendEventAsync(message1).ConfigureAwait(false);
await testClient.CloseAsync();
testClient.Dispose();
testClient = null;
Logger.Trace("Test with instance 1 completed");

// Perform the same steps again, reusing the previously created authentication method instance to ensure
// that the sdk did not dispose the user supplied authentication method instance.
testClient = DeviceClient.Create(testDevice.IoTHubHostName, authenticationMethod, transport);
using var message2 = new Client.Message();
await testClient.SendEventAsync(message2).ConfigureAwait(false);
await testClient.CloseAsync();
Logger.Trace("Test with instance 2 completed, reused the previously created authentication method instance for the device client.");
}
finally
{
authenticationMethod?.Dispose();
testClient?.Dispose();
}
}

private async Task ReuseAuthenticationMethod_MuxedDevices(Client.TransportType transport, int devicesCount)
Expand All @@ -110,7 +187,7 @@ private async Task ReuseAuthenticationMethod_MuxedDevices(Client.TransportType t
{
TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false);
#pragma warning disable CA2000 // Dispose objects before losing scope - the authentication method is disposed at the end of the test.
var authenticationMethod = new DeviceAuthenticationSasToken(testDevice.ConnectionString);
var authenticationMethod = new DeviceAuthenticationSasToken(testDevice.ConnectionString, disposeWithClient: false);
#pragma warning restore CA2000 // Dispose objects before losing scope

testDevices.Add(testDevice);
Expand Down Expand Up @@ -215,9 +292,13 @@ private class DeviceAuthenticationSasToken : DeviceAuthenticationWithTokenRefres
private const string SasTokenTargetFormat = "{0}/devices/{1}";
private readonly IotHubConnectionStringBuilder _connectionStringBuilder;

private static readonly int s_suggestedSasTimeToLiveInSeconds = (int)TimeSpan.FromMinutes(30).TotalSeconds;
private static readonly int s_sasRenewalBufferPercentage = 50;

public DeviceAuthenticationSasToken(
string connectionString)
: base(GetDeviceIdFromConnectionString(connectionString))
string connectionString,
bool disposeWithClient)
: base(GetDeviceIdFromConnectionString(connectionString), s_suggestedSasTimeToLiveInSeconds, s_sasRenewalBufferPercentage, disposeWithClient)
{
if (connectionString == null)
{
Expand Down
38 changes: 34 additions & 4 deletions iothub/device/src/AuthenticationWithTokenRefresh.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,35 @@ public abstract class AuthenticationWithTokenRefresh : IAuthenticationMethod, ID
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationWithTokenRefresh"/> class.
/// </summary>
/// <remarks>
/// This constructor will create an authentication method instance that will be disposed when its
/// associated device/ module client instance is disposed. To reuse the authentication method instance across multiple client instance lifetimes,
/// use <see cref="AuthenticationWithTokenRefresh(int, int, bool)"/> constructor and set <c>disposeWithClient</c> to <c>false</c>.
/// </remarks>
/// <param name="suggestedTimeToLiveSeconds">Token time to live suggested value. The implementations of this abstract
/// may choose to ignore this value.</param>
/// <param name="timeBufferPercentage">Time buffer before expiry when the token should be renewed expressed as
/// a percentage of the time to live.</param>
public AuthenticationWithTokenRefresh(
int suggestedTimeToLiveSeconds,
int timeBufferPercentage)
: this(suggestedTimeToLiveSeconds, timeBufferPercentage, true)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationWithTokenRefresh"/> class.
/// </summary>
/// <param name="suggestedTimeToLiveSeconds">Token time to live suggested value. The implementations of this abstract
/// may choose to ignore this value.</param>
/// <param name="timeBufferPercentage">Time buffer before expiry when the token should be renewed expressed as
/// a percentage of the time to live.</param>
/// <param name="disposeWithClient "><c>true</c> if the authentication method should be disposed of by the client
/// when the client using this instance is itself disposed; <c>false</c> if you intend to reuse the authentication method.</param>
public AuthenticationWithTokenRefresh(
int suggestedTimeToLiveSeconds,
int timeBufferPercentage,
bool disposeWithClient)
{
if (suggestedTimeToLiveSeconds < 0)
{
Expand All @@ -48,6 +70,8 @@ public AuthenticationWithTokenRefresh(
ExpiresOn = DateTime.UtcNow.AddSeconds(-_suggestedTimeToLiveSeconds);
Debug.Assert(IsExpiring);
UpdateTimeBufferSeconds(_suggestedTimeToLiveSeconds);

DisposalWithClient = disposeWithClient;
}

/// <summary>
Expand All @@ -65,21 +89,27 @@ public AuthenticationWithTokenRefresh(
/// </summary>
public bool IsExpiring => (ExpiresOn - DateTime.UtcNow).TotalSeconds <= _bufferSeconds;

// This internal property is used by the sdk to determine if the instance was created by the sdk,
// and thus, if it should be disposed by the sdk.
internal bool InstanceCreatedBySdk { get; set; }
// This internal property is used by the sdk to determine if the authentication method
// should be disposed when the client that it is initialized with is disposed.
internal bool DisposalWithClient { get; }

/// <summary>
/// Gets a snapshot of the security token associated with the device. This call is thread-safe.
/// </summary>
public async Task<string> GetTokenAsync(string iotHub)
{
if (_isDisposed)
{
throw new ObjectDisposedException("The authentication method instance has already been disposed, so this client is no longer usable. " +
"Please close and dispose your current client instance. To continue carrying out operations from your device/ module, " +
"create a new authentication method instance and use it for reinitializing your client.");
}

if (!IsExpiring)
{
return _token;
}

Debug.Assert(_lock != null);
await _lock.WaitAsync().ConfigureAwait(false);

try
Expand Down
4 changes: 3 additions & 1 deletion iothub/device/src/DeviceAuthenticationWithSakRefresh.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ internal DeviceAuthenticationWithSakRefresh(
string deviceId,
IotHubConnectionString connectionString,
TimeSpan sasTokenTimeToLive,
int sasTokenRenewalBuffer) : base(deviceId, (int)sasTokenTimeToLive.TotalSeconds, sasTokenRenewalBuffer)
int sasTokenRenewalBuffer,
bool disposeWithClient)
: base(deviceId, (int)sasTokenTimeToLive.TotalSeconds, sasTokenRenewalBuffer, disposeWithClient)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
}
Expand Down
37 changes: 36 additions & 1 deletion iothub/device/src/DeviceAuthenticationWithTokenRefresh.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public abstract class DeviceAuthenticationWithTokenRefresh : AuthenticationWithT
/// Initializes a new instance of the <see cref="DeviceAuthenticationWithTokenRefresh"/> class using default
/// TTL and TTL buffer time settings.
/// </summary>
/// <remarks>
/// This constructor will create an authentication method instance that will be disposed when its
/// associated device client instance is disposed. To reuse the authentication method instance across multiple client instance lifetimes,
/// use <see cref="DeviceAuthenticationWithTokenRefresh(string, int, int, bool)"/> constructor and set <c>disposeWithClient</c> to <c>false</c>.
/// </remarks>
/// <param name="deviceId">Device Identifier.</param>
public DeviceAuthenticationWithTokenRefresh(string deviceId)
: this(deviceId, DefaultTimeToLiveSeconds, DefaultBufferPercentage)
Expand All @@ -27,6 +32,11 @@ public DeviceAuthenticationWithTokenRefresh(string deviceId)
/// <summary>
/// Initializes a new instance of the <see cref="DeviceAuthenticationWithTokenRefresh"/> class.
/// </summary>
/// <remarks>
/// This constructor will create an authentication method instance that will be disposed when its
/// associated device client instance is disposed. To reuse the authentication method instance across multiple client instance lifetimes,
/// use <see cref="DeviceAuthenticationWithTokenRefresh(string, int, int, bool)"/> constructor and set <c>disposeWithClient</c> to <c>false</c>.
/// </remarks>
/// <param name="deviceId">Device Identifier.</param>
/// <param name="suggestedTimeToLiveSeconds">
/// The suggested time to live value for the generated SAS tokens.
Expand All @@ -40,7 +50,32 @@ public DeviceAuthenticationWithTokenRefresh(
string deviceId,
int suggestedTimeToLiveSeconds,
int timeBufferPercentage)
: base(SetSasTokenSuggestedTimeToLiveSeconds(suggestedTimeToLiveSeconds), SetSasTokenRenewalBufferPercentage(timeBufferPercentage))
: this(deviceId, suggestedTimeToLiveSeconds, timeBufferPercentage, true)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DeviceAuthenticationWithTokenRefresh"/> class.
/// </summary>
/// <param name="deviceId">Device Identifier.</param>
/// <param name="suggestedTimeToLiveSeconds">
/// The suggested time to live value for the generated SAS tokens.
/// The default value is 1 hour.
/// </param>
/// <param name="timeBufferPercentage">
/// 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>
///<param name="disposeWithClient ">
///<c>true</c> if the authentication method should be disposed of by the client
/// when the client using this instance is itself disposed; <c>false</c> if you intend to reuse the authentication method.
/// </param>
public DeviceAuthenticationWithTokenRefresh(
string deviceId,
int suggestedTimeToLiveSeconds,
int timeBufferPercentage,
bool disposeWithClient)
: base(SetSasTokenSuggestedTimeToLiveSeconds(suggestedTimeToLiveSeconds), SetSasTokenRenewalBufferPercentage(timeBufferPercentage), disposeWithClient)
{
if (deviceId.IsNullOrWhiteSpace())
{
Expand Down
Loading

0 comments on commit 8ee81c8

Please sign in to comment.