diff --git a/iothub/service/src/IoTHubSasCredential.cs b/iothub/service/src/IoTHubSasCredential.cs deleted file mode 100644 index a21179f98b..0000000000 --- a/iothub/service/src/IoTHubSasCredential.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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.Collections.Generic; -using System.Text; - -#if !NET451 - -using Azure; - -namespace Microsoft.Azure.Devices -{ - /// - /// Shared access signature credential used to authenticate with IoT hub. - /// - public class IotHubSasCredential - : AzureSasCredential - { - /// - /// Creates an instance of . - /// - /// Shared access signature used to authenticate with IoT hub. - /// The shared access signature expiry in UTC. - public IotHubSasCredential(string signature, DateTime expiresOnUtc) : base(signature) - { - ExpiresOnUtc = expiresOnUtc; - } - - /// - /// The shared access signature expiry in UTC. - /// - public DateTime ExpiresOnUtc { get; private set; } - - /// - /// Updates the shared access signature. This is intended to be used when you've - /// regenerated your shared access signature and want to update clients. - /// - /// Shared access signature used to authenticate with IoT hub. - /// The shared access signature expiry in UTC. - public void Update(string signature, DateTime expiresOnUtc) - { - Update(signature); - ExpiresOnUtc = expiresOnUtc; - } - } -} - -#endif diff --git a/iothub/service/src/IotHubSasCredentialProperties.cs b/iothub/service/src/IotHubSasCredentialProperties.cs index 44b37bf9c8..756fe3865a 100644 --- a/iothub/service/src/IotHubSasCredentialProperties.cs +++ b/iothub/service/src/IotHubSasCredentialProperties.cs @@ -5,6 +5,8 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Azure.Amqp; +using System.Globalization; +using System.Linq; #if !NET451 @@ -23,9 +25,9 @@ public IotHubSasCredentialProperties() throw new InvalidOperationException("IotHubSasCredential is not supported on NET451"); } #else - private readonly IotHubSasCredential _credential; + private readonly AzureSasCredential _credential; - public IotHubSasCredentialProperties(string hostName, IotHubSasCredential credential) : base(hostName) + public IotHubSasCredentialProperties(string hostName, AzureSasCredential credential) : base(hostName) { _credential = credential; } @@ -48,10 +50,28 @@ public override Task GetTokenAsync(Uri namespaceAddress, string applie throw new InvalidOperationException($"IotHubSasCredential is not supported on NET451"); #else + // Parse the SAS token to find the expiration date and time. + // SharedAccessSignature sr=ENCODED(dh://myiothub.azure-devices.net/a/b/c?myvalue1=a)&sig=&se=[&skn=] + var tokenParts = _credential.Signature.Split('&').ToList(); + var expiresAtTokenPart = tokenParts.Where(tokenPart => tokenPart.StartsWith("se=", StringComparison.OrdinalIgnoreCase)); + + if (!expiresAtTokenPart.Any()) + { + throw new InvalidOperationException($"There is no expiration time on {nameof(AzureSasCredential)} signature."); + } + + string expiresAtStr = expiresAtTokenPart.First().Split('=')[1]; + bool isSuccess = DateTime.TryParse(expiresAtStr, out DateTime expiresAt); + + if (!isSuccess) + { + throw new InvalidOperationException($"Invalid expiration time on {nameof(AzureSasCredential)} signature."); + } + var token = new CbsToken( _credential.Signature, CbsConstants.IotHubSasTokenType, - _credential.ExpiresOnUtc); + expiresAt); return Task.FromResult(token); #endif } diff --git a/iothub/service/src/JobClient/JobClient.cs b/iothub/service/src/JobClient/JobClient.cs index 671790e72e..6c963c30c9 100644 --- a/iothub/service/src/JobClient/JobClient.cs +++ b/iothub/service/src/JobClient/JobClient.cs @@ -79,12 +79,12 @@ public static JobClient Create( /// Creates an instance of . /// /// IoT hub host name. - /// Credential that generates a SAS token to authenticate with IoT hub. See . + /// Credential that generates a SAS token to authenticate with IoT hub. See . /// The HTTP transport settings. /// An instance of . public static JobClient Create( string hostName, - IotHubSasCredential credential, + AzureSasCredential credential, HttpTransportSettings transportSettings = default) { if (string.IsNullOrEmpty(hostName)) diff --git a/iothub/service/src/RegistryManager.cs b/iothub/service/src/RegistryManager.cs index 1aca479b39..3683a0e0ca 100644 --- a/iothub/service/src/RegistryManager.cs +++ b/iothub/service/src/RegistryManager.cs @@ -85,12 +85,12 @@ public static RegistryManager Create( /// Creates an instance of . /// /// IoT hub host name. - /// Credential that generates a SAS token to authenticate with IoT hub. See . + /// Credential that generates a SAS token to authenticate with IoT hub. See . /// The HTTP transport settings. /// An instance of . public static RegistryManager Create( string hostName, - IotHubSasCredential credential, + AzureSasCredential credential, HttpTransportSettings transportSettings = default) { if (string.IsNullOrEmpty(hostName)) diff --git a/iothub/service/src/ServiceClient.cs b/iothub/service/src/ServiceClient.cs index d1a0650735..b93224fcba 100644 --- a/iothub/service/src/ServiceClient.cs +++ b/iothub/service/src/ServiceClient.cs @@ -101,14 +101,14 @@ public static ServiceClient Create( /// Creates a using SAS token and the specified transport type. /// /// IoT hub host name. - /// Credential that generates a SAS token to authenticate with IoT hub. See . + /// Credential that generates a SAS token to authenticate with IoT hub. See . /// Specifies whether Amqp or Amqp_WebSocket_Only transport is used. /// Specifies the AMQP_WS and HTTP proxy settings for service client. /// The options that allow configuration of the service client instance during initialization. /// An instance of . public static ServiceClient Create( string hostName, - IotHubSasCredential credential, + AzureSasCredential credential, TransportType transportType, ServiceClientTransportSettings transportSettings = default, ServiceClientOptions options = default) diff --git a/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs b/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs new file mode 100644 index 0000000000..45d5ed8f6e --- /dev/null +++ b/iothub/service/tests/IotHubSasCredentialPropertiesTests.cs @@ -0,0 +1,121 @@ +// 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.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Net; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.Azure.Amqp; +using FluentAssertions; + +#if !NET451 + +using Azure; + +#endif + +namespace Microsoft.Azure.Devices.Tests +{ + [TestClass] + [TestCategory("Unit")] + public class IotHubSasCredentialPropertiesTests + { + private const string _hostName = "myiothub.azure-devices.net"; + +#if !NET451 + + [TestMethod] + public async Task TestCbsTokenGeneration_Succeeds() + { + // arrange + DateTime expiresAtUtc = DateTime.UtcNow; + DateTime updatedExpiresAtUtc = DateTime.UtcNow.AddDays(1); + + string token = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}&se={2}", + WebUtility.UrlEncode(_hostName), + WebUtility.UrlEncode("signature"), + expiresAtUtc); + + string updatedToken = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}&se={2}", + WebUtility.UrlEncode(_hostName), + WebUtility.UrlEncode("signature"), + updatedExpiresAtUtc); + + var azureSasCredential = new AzureSasCredential(token); + var iotHubSasCredentialProperties = new IotHubSasCredentialProperties(_hostName, azureSasCredential); + + // act + + CbsToken cbsToken = await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); + azureSasCredential.Update(updatedToken); + CbsToken updatedCbsToken = await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); + + // assert + cbsToken.ExpiresAtUtc.ToString().Should().Be(expiresAtUtc.ToString()); + updatedCbsToken.ExpiresAtUtc.ToString().Should().Be(updatedExpiresAtUtc.ToString()); + } + + [TestMethod] + public async Task TestCbsTokenGeneration_InvalidExpirationDateTimeFormat_Fails() + { + // arrange + string token = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}&se={2}", + WebUtility.UrlEncode(_hostName), + WebUtility.UrlEncode("signature"), + "01:01:2021"); + + var azureSasCredential = new AzureSasCredential(token); + var iotHubSasCredentialProperties = new IotHubSasCredentialProperties(_hostName, azureSasCredential); + + try + { + // act + await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); + + Assert.Fail("The parsing of date time in invalid format on the SAS token should have caused an exception."); + } + catch (InvalidOperationException ex) + { + // assert + ex.Message.Should().Be($"Invalid expiration time on {nameof(AzureSasCredential)} signature."); + } + } + + [TestMethod] + public async Task TestCbsTokenGeneration_MissingExpiration_Fails() + { + // arrange + string token = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}", + WebUtility.UrlEncode(_hostName), + WebUtility.UrlEncode("signature")); + + var azureSasCredential = new AzureSasCredential(token); + var iotHubSasCredentialProperties = new IotHubSasCredentialProperties(_hostName, azureSasCredential); + + try + { + // act + await iotHubSasCredentialProperties.GetTokenAsync(null, null, null).ConfigureAwait(false); + + Assert.Fail("The missing expiry on the SAS token should have caused an exception."); + } + catch (InvalidOperationException ex) + { + // assert + ex.Message.Should().Be($"There is no expiration time on {nameof(AzureSasCredential)} signature."); + } + } + +#endif + } +}