From 9db34d683a240d145f90adf70f63d532ef304ec3 Mon Sep 17 00:00:00 2001
From: Abhipsa Misra <22563986+abhipsaMisra@users.noreply.github.com>
Date: Mon, 15 Jun 2020 17:35:11 -0700
Subject: [PATCH] IoT hub service client authentication via connection string
(#12731)
* feat(e2e-tests): Add initial setup for E2E tests
* feat(iot-service): Add authentication via connection string
---
sdk/iot/Azure.Iot.Hub.Service/CodeMaid.config | 4 +
.../Azure.Iot.Hub.Service.netstandard2.0.cs | 6 +-
.../Authentication/FixedSasTokenProvider.cs | 28 +++++
.../src/Authentication/ISasTokenProvider.cs | 17 +++
.../Authentication/IotHubConnectionString.cs | 62 +++++++++++
.../SasTokenAuthenticationPolicy.cs | 41 +++++++
.../SasTokenProviderWithSharedAccessKey.cs | 81 ++++++++++++++
.../SharedAccessSignatureBuilder.cs | 80 +++++++++++++
.../SharedAccessSignatureConstants.cs | 26 +++++
.../src/Azure.Iot.Hub.Service.csproj | 3 +
.../src/IoTHubServiceClient.cs | 105 +++++++++++++++++-
.../tests/Azure.Iot.Hub.Service.Tests.csproj | 20 +++-
.../tests/E2eTestBase.cs | 46 ++++++++
.../tests/IotHubServiceTestEnvironment.cs | 16 +++
.../tests/TestSettings.cs | 86 ++++++++++++++
.../tests/prerequisites/setup.ps1 | 5 +
16 files changed, 615 insertions(+), 11 deletions(-)
create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/FixedSasTokenProvider.cs
create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/ISasTokenProvider.cs
create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/IotHubConnectionString.cs
create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenAuthenticationPolicy.cs
create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenProviderWithSharedAccessKey.cs
create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureBuilder.cs
create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureConstants.cs
create mode 100644 sdk/iot/Azure.Iot.Hub.Service/tests/E2eTestBase.cs
create mode 100644 sdk/iot/Azure.Iot.Hub.Service/tests/IotHubServiceTestEnvironment.cs
create mode 100644 sdk/iot/Azure.Iot.Hub.Service/tests/TestSettings.cs
diff --git a/sdk/iot/Azure.Iot.Hub.Service/CodeMaid.config b/sdk/iot/Azure.Iot.Hub.Service/CodeMaid.config
index 5382ce6fd8f0..698934e0d176 100644
--- a/sdk/iot/Azure.Iot.Hub.Service/CodeMaid.config
+++ b/sdk/iot/Azure.Iot.Hub.Service/CodeMaid.config
@@ -36,6 +36,10 @@
False
+
+ False
+
diff --git a/sdk/iot/Azure.Iot.Hub.Service/api/Azure.Iot.Hub.Service.netstandard2.0.cs b/sdk/iot/Azure.Iot.Hub.Service/api/Azure.Iot.Hub.Service.netstandard2.0.cs
index ca6d447ba1f4..e7f94731e4d7 100644
--- a/sdk/iot/Azure.Iot.Hub.Service/api/Azure.Iot.Hub.Service.netstandard2.0.cs
+++ b/sdk/iot/Azure.Iot.Hub.Service/api/Azure.Iot.Hub.Service.netstandard2.0.cs
@@ -50,8 +50,10 @@ public enum IfMatchPrecondition
public partial class IoTHubServiceClient
{
protected IoTHubServiceClient() { }
- public IoTHubServiceClient(System.Uri endpoint) { }
- public IoTHubServiceClient(System.Uri endpoint, Azure.Iot.Hub.Service.IoTHubServiceClientOptions options) { }
+ public IoTHubServiceClient(string connectionString) { }
+ public IoTHubServiceClient(string connectionString, Azure.Iot.Hub.Service.IoTHubServiceClientOptions options) { }
+ public IoTHubServiceClient(System.Uri endpoint, Azure.Core.TokenCredential credential) { }
+ public IoTHubServiceClient(System.Uri endpoint, Azure.Core.TokenCredential credential, Azure.Iot.Hub.Service.IoTHubServiceClientOptions options) { }
public Azure.Iot.Hub.Service.DevicesClient Devices { get { throw null; } }
public Azure.Iot.Hub.Service.FilesClient Files { get { throw null; } }
public Azure.Iot.Hub.Service.JobsClient Jobs { get { throw null; } }
diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/FixedSasTokenProvider.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/FixedSasTokenProvider.cs
new file mode 100644
index 000000000000..6c9fad218911
--- /dev/null
+++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/FixedSasTokenProvider.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Azure.Iot.Hub.Service.Authentication
+{
+ ///
+ /// Implementation of a shared access signature provider with a fixed token.
+ ///
+ internal class FixedSasTokenProvider : ISasTokenProvider
+ {
+ private readonly string _sharedAccessSignature;
+
+ // Protected constructor, to allow mocking
+ protected FixedSasTokenProvider()
+ {
+ }
+
+ internal FixedSasTokenProvider(string sharedAccessSignature)
+ {
+ _sharedAccessSignature = sharedAccessSignature;
+ }
+
+ public string GetSasToken()
+ {
+ return _sharedAccessSignature;
+ }
+ }
+}
diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/ISasTokenProvider.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/ISasTokenProvider.cs
new file mode 100644
index 000000000000..d6d1bcc174b9
--- /dev/null
+++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/ISasTokenProvider.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Azure.Iot.Hub.Service.Authentication
+{
+ ///
+ /// The token provider interface for shared access signature based authentication.
+ ///
+ internal interface ISasTokenProvider
+ {
+ ///
+ /// Retrieve the shared access signature to be used.
+ ///
+ /// The shared access signature to be used for authenticating HTTP requests to the service. It is called once per HTTP request.
+ public string GetSasToken();
+ }
+}
diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/IotHubConnectionString.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/IotHubConnectionString.cs
new file mode 100644
index 000000000000..258baad4c769
--- /dev/null
+++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/IotHubConnectionString.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Azure.Core;
+
+namespace Azure.Iot.Hub.Service.Authentication
+{
+ ///
+ /// Implementation for creating the shared access signature token provider.
+ ///
+ internal class IotHubConnectionString
+ {
+ private const string HostNameIdentifier = "HostName";
+ private const string SharedAccessKeyIdentifier = "SharedAccessKey";
+ private const string SharedAccessKeyNameIdentifier = "SharedAccessKeyName";
+ private const string SharedAccessSignatureIdentifier = "SharedAccessSignature";
+
+ private readonly ConnectionString _connectionString;
+ private readonly string _sharedAccessPolicy;
+ private readonly string _sharedAccessKey;
+ private readonly string _sharedAccessSignature;
+
+ internal IotHubConnectionString(string connectionString)
+ {
+ _connectionString = ConnectionString.Parse(connectionString);
+ _sharedAccessKey = _connectionString.GetNonRequired(SharedAccessKeyIdentifier);
+ _sharedAccessPolicy = _connectionString.GetNonRequired(SharedAccessKeyNameIdentifier);
+ _sharedAccessSignature = _connectionString.GetNonRequired(SharedAccessSignatureIdentifier);
+
+ if (!ValidateInput(_sharedAccessPolicy, _sharedAccessKey, _sharedAccessSignature))
+ {
+ throw new ArgumentException("Specify either both the sharedAccessKey and sharedAccessKeyName, or only sharedAccessSignature");
+ }
+
+ HostName = _connectionString.GetRequired(HostNameIdentifier);
+ }
+
+ internal string HostName { get; }
+
+ internal ISasTokenProvider GetSasTokenProvider()
+ {
+ if (_sharedAccessSignature == null)
+ {
+ return new SasTokenProviderWithSharedAccessKey(HostName, _sharedAccessPolicy, _sharedAccessKey);
+ }
+ return new FixedSasTokenProvider(_sharedAccessSignature);
+ }
+
+ private static bool ValidateInput(string sharedAccessPolicy, string sharedAccessKey, string sharedAccessSignature)
+ {
+ if (sharedAccessSignature == null)
+ {
+ return sharedAccessKey != null && sharedAccessPolicy != null;
+ }
+ else
+ {
+ return sharedAccessKey == null && sharedAccessPolicy == null;
+ }
+ }
+ }
+}
diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenAuthenticationPolicy.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenAuthenticationPolicy.cs
new file mode 100644
index 000000000000..a657762f4b16
--- /dev/null
+++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenAuthenticationPolicy.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading.Tasks;
+using Azure.Core;
+using Azure.Core.Pipeline;
+
+namespace Azure.Iot.Hub.Service.Authentication
+{
+ ///
+ /// The shared access signature based HTTP pipeline policy.
+ /// This authentication policy injects the sas token into the HTTP authentication header for each HTTP request made.
+ ///
+ internal class SasTokenAuthenticationPolicy : HttpPipelinePolicy
+ {
+ private readonly ISasTokenProvider _sasTokenProvider;
+
+ internal SasTokenAuthenticationPolicy(ISasTokenProvider sasTokenProvider)
+ {
+ _sasTokenProvider = sasTokenProvider;
+ }
+
+ public override void Process(HttpMessage message, ReadOnlyMemory pipeline)
+ {
+ AddHeaders(message);
+ ProcessNext(message, pipeline);
+ }
+
+ public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline)
+ {
+ AddHeaders(message);
+ await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
+ }
+
+ private void AddHeaders(HttpMessage message)
+ {
+ message.Request.Headers.Add(HttpHeader.Names.Authorization, _sasTokenProvider.GetSasToken());
+ }
+ }
+}
diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenProviderWithSharedAccessKey.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenProviderWithSharedAccessKey.cs
new file mode 100644
index 000000000000..db6709ef0f76
--- /dev/null
+++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenProviderWithSharedAccessKey.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Azure.Iot.Hub.Service.Authentication
+{
+ ///
+ /// Implementation of a shared access signature provider with token caching and refresh.
+ ///
+ internal class SasTokenProviderWithSharedAccessKey : ISasTokenProvider
+ {
+ // The default time to live for a sas token is set to 30 minutes.
+ private static readonly TimeSpan s_defaultTimeToLive = TimeSpan.FromMinutes(30);
+
+ // Time buffer before expiry when the token should be renewed, expressed as a percentage of the time to live.
+ // The token will be renewed when it has 15% or less of the sas token's lifespan left.
+ private const int s_renewalTimeBufferPercentage = 15;
+
+ private readonly object _lock = new object();
+
+ private readonly string _hostName;
+ private readonly string _sharedAccessPolicy;
+ private readonly string _sharedAccessKey;
+ private readonly TimeSpan _timeToLive;
+
+ private string _cachedSasToken;
+ private DateTimeOffset _tokenExpiryTime;
+
+ // Protected constructor, to allow mocking
+ protected SasTokenProviderWithSharedAccessKey()
+ {
+ }
+
+ internal SasTokenProviderWithSharedAccessKey(string hostName, string sharedAccessPolicy, string sharedAccessKey, TimeSpan? timeToLive = null)
+ {
+ _hostName = hostName;
+ _sharedAccessPolicy = sharedAccessPolicy;
+ _sharedAccessKey = sharedAccessKey;
+ _timeToLive = timeToLive ?? s_defaultTimeToLive;
+
+ _cachedSasToken = null;
+ }
+
+ string ISasTokenProvider.GetSasToken()
+ {
+ lock (_lock)
+ {
+ if (IsTokenExpired())
+ {
+ var builder = new SharedAccessSignatureBuilder
+ {
+ HostName = _hostName,
+ SharedAccessPolicy = _sharedAccessPolicy,
+ SharedAccessKey = _sharedAccessKey,
+ TimeToLive = _timeToLive,
+ };
+
+ _tokenExpiryTime = DateTimeOffset.UtcNow.Add(_timeToLive);
+ _cachedSasToken = builder.ToSignature();
+ }
+
+ return _cachedSasToken;
+ }
+ }
+
+ private bool IsTokenExpired()
+ {
+ // The token is considered expired if this is the first time it is being accessed (not cached yet)
+ // or the current time is greater than or equal to the token expiry time, less 15% buffer.
+ if (_cachedSasToken == null)
+ {
+ return true;
+ }
+
+ var bufferTimeInMilliseconds = (double)s_renewalTimeBufferPercentage / 100 * _timeToLive.TotalMilliseconds;
+ DateTimeOffset tokenExpiryTimeWithBuffer = _tokenExpiryTime.AddMilliseconds(-bufferTimeInMilliseconds);
+ return DateTimeOffset.UtcNow.CompareTo(tokenExpiryTimeWithBuffer) >= 0;
+ }
+ }
+}
diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureBuilder.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureBuilder.cs
new file mode 100644
index 000000000000..2014958c25dc
--- /dev/null
+++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureBuilder.cs
@@ -0,0 +1,80 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Azure.Iot.Hub.Service.Authentication
+{
+ ///
+ /// Builds the shared access signature based on the access policy passed.
+ ///
+ internal class SharedAccessSignatureBuilder
+ {
+ internal string SharedAccessPolicy { get; set; }
+
+ internal string SharedAccessKey { get; set; }
+
+ internal string HostName { get; set; }
+
+ internal TimeSpan TimeToLive { get; set; }
+
+ internal string ToSignature()
+ {
+ return BuildSignature(SharedAccessPolicy, SharedAccessKey, HostName, TimeToLive);
+ }
+
+ private static string BuildSignature(string sharedAccessPolicy, string sharedAccessKey, string hostName, TimeSpan timeToLive)
+ {
+ string expiresOn = BuildExpiresOn(timeToLive);
+ string audience = WebUtility.UrlEncode(hostName);
+ var fields = new List
+ {
+ audience,
+ expiresOn
+ };
+
+ // Example string to be signed:
+ // dh://myiothub.azure-devices.net/a/b/c?myvalue1=a
+ //
+
+ string signature = Sign(string.Join("\n", fields), sharedAccessKey);
+
+ // Example returned string:
+ // SharedAccessSignature sr=ENCODED(dh://myiothub.azure-devices.net/a/b/c?myvalue1=a)&sig=&se=[&skn=]
+
+ var buffer = new StringBuilder();
+ buffer.Append($"{SharedAccessSignatureConstants.SharedAccessSignature} " +
+ $"{SharedAccessSignatureConstants.AudienceFieldName}={audience}" +
+ $"&{SharedAccessSignatureConstants.SignatureFieldName}={WebUtility.UrlEncode(signature)}" +
+ $"&{SharedAccessSignatureConstants.ExpiryFieldName}={WebUtility.UrlEncode(expiresOn)}");
+
+ if (!string.IsNullOrWhiteSpace(sharedAccessPolicy))
+ {
+ buffer.Append($"&{SharedAccessSignatureConstants.KeyNameFieldName}={WebUtility.UrlEncode(sharedAccessPolicy)}");
+ }
+
+ return buffer.ToString();
+ }
+
+ private static string BuildExpiresOn(TimeSpan timeToLive)
+ {
+ DateTimeOffset expiresOn = DateTimeOffset.UtcNow.Add(timeToLive);
+ TimeSpan secondsFromBaseTime = expiresOn.Subtract(SharedAccessSignatureConstants.EpochTime);
+ long seconds = Convert.ToInt64(secondsFromBaseTime.TotalSeconds, CultureInfo.InvariantCulture);
+ return Convert.ToString(seconds, CultureInfo.InvariantCulture);
+ }
+
+ private static string Sign(string requestString, string key)
+ {
+ using (var hmac = new HMACSHA256(Convert.FromBase64String(key)))
+ {
+ return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(requestString)));
+ }
+ }
+ }
+}
diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureConstants.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureConstants.cs
new file mode 100644
index 000000000000..327d6d7d800d
--- /dev/null
+++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureConstants.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Azure.Iot.Hub.Service.Authentication
+{
+ ///
+ /// The constants used for building the IoT hub service shared access signature token.
+ ///
+ internal static class SharedAccessSignatureConstants
+ {
+ internal const int MaxKeyNameLength = 256;
+ internal const int MaxKeyLength = 256;
+ internal const string SharedAccessSignature = "SharedAccessSignature";
+ internal const string AudienceFieldName = "sr";
+ internal const string SignatureFieldName = "sig";
+ internal const string KeyNameFieldName = "skn";
+ internal const string ExpiryFieldName = "se";
+ internal const string SignedResourceFullFieldName = SharedAccessSignature + " " + AudienceFieldName;
+ internal const string KeyValueSeparator = "=";
+ internal const string PairSeparator = "&";
+ internal static readonly DateTime EpochTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
+ internal static readonly TimeSpan MaxClockSkew = TimeSpan.FromMinutes(5);
+ }
+}
diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Azure.Iot.Hub.Service.csproj b/sdk/iot/Azure.Iot.Hub.Service/src/Azure.Iot.Hub.Service.csproj
index 8ea0f5882dff..0c15236760cf 100644
--- a/sdk/iot/Azure.Iot.Hub.Service/src/Azure.Iot.Hub.Service.csproj
+++ b/sdk/iot/Azure.Iot.Hub.Service/src/Azure.Iot.Hub.Service.csproj
@@ -47,6 +47,9 @@
Shared\Azure.Core
+
+ Shared\Azure.Core
+
diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/IoTHubServiceClient.cs b/sdk/iot/Azure.Iot.Hub.Service/src/IoTHubServiceClient.cs
index 749198543663..ef1e1f52dee1 100644
--- a/sdk/iot/Azure.Iot.Hub.Service/src/IoTHubServiceClient.cs
+++ b/sdk/iot/Azure.Iot.Hub.Service/src/IoTHubServiceClient.cs
@@ -4,16 +4,18 @@
using System;
using Azure.Core;
using Azure.Core.Pipeline;
+using Azure.Iot.Hub.Service.Authentication;
namespace Azure.Iot.Hub.Service
{
///
- /// The IoT Hub Service Client
+ /// The IoT Hub Service Client.
///
public class IoTHubServiceClient
{
private readonly HttpPipeline _httpPipeline;
private readonly ClientDiagnostics _clientDiagnostics;
+ private readonly Uri _endpoint;
private readonly RegistryManagerRestClient _registryManagerRestClient;
private readonly TwinRestClient _twinRestClient;
private readonly DeviceMethodRestClient _deviceMethodRestClient;
@@ -22,22 +24,27 @@ public class IoTHubServiceClient
/// place holder for Devices
///
public DevicesClient Devices { get; private set; }
+
///
/// place holder for Modules
///
public ModulesClient Modules { get; private set; }
+
///
/// place holder for Statistics
///
public StatisticsClient Statistics { get; private set; }
+
///
/// place holder for Messages
///
public CloudToDeviceMessagesClient Messages { get; private set; }
+
///
/// place holder for Files
///
public FilesClient Files { get; private set; }
+
///
/// place holder for Jobs
///
@@ -54,24 +61,91 @@ protected IoTHubServiceClient()
///
/// Initializes a new instance of the class.
///
- public IoTHubServiceClient(Uri endpoint)
- : this(endpoint, new IoTHubServiceClientOptions())
+ ///
+ /// The IoT Hub connection string, with either "iothubowner", "service", "registryRead" or "registryReadWrite" policy, as applicable.
+ /// For more information, see .
+ ///
+ public IoTHubServiceClient(string connectionString)
+ : this(connectionString, new IoTHubServiceClientOptions())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The IoT Hub connection string, with either "iothubowner", "service", "registryRead" or "registryReadWrite" policy, as applicable.
+ /// For more information, see .
+ ///
+ ///
+ /// Options that allow configuration of requests sent to the IoT Hub service.
+ ///
+ public IoTHubServiceClient(string connectionString, IoTHubServiceClientOptions options)
+ {
+ Argument.AssertNotNull(options, nameof(options));
+
+ var iotHubConnectionString = new IotHubConnectionString(connectionString);
+ ISasTokenProvider sasProvider = iotHubConnectionString.GetSasTokenProvider();
+
+ _endpoint = BuildEndpointUriFromHostName(iotHubConnectionString.HostName);
+
+ _clientDiagnostics = new ClientDiagnostics(options);
+
+ options.AddPolicy(new SasTokenAuthenticationPolicy(sasProvider), HttpPipelinePosition.PerCall);
+ _httpPipeline = HttpPipelineBuilder.Build(options);
+
+ _registryManagerRestClient = new RegistryManagerRestClient(_clientDiagnostics, _httpPipeline, _endpoint, options.GetVersionString());
+ _twinRestClient = new TwinRestClient(_clientDiagnostics, _httpPipeline, null, options.GetVersionString());
+ _deviceMethodRestClient = new DeviceMethodRestClient(_clientDiagnostics, _httpPipeline, _endpoint, options.GetVersionString());
+
+ Devices = new DevicesClient(_registryManagerRestClient, _twinRestClient, _deviceMethodRestClient);
+ Modules = new ModulesClient(_registryManagerRestClient, _twinRestClient, _deviceMethodRestClient);
+
+ Statistics = new StatisticsClient();
+ Messages = new CloudToDeviceMessagesClient();
+ Files = new FilesClient();
+ Jobs = new JobsClient();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The IoT Hub service instance URI to connect to.
+ ///
+ ///
+ /// The implementation which will be used to request for the authentication token.
+ ///
+ public IoTHubServiceClient(Uri endpoint, TokenCredential credential)
+ : this(endpoint, credential, new IoTHubServiceClientOptions())
{
}
///
/// Initializes a new instance of the class.
///
- public IoTHubServiceClient(Uri endpoint, IoTHubServiceClientOptions options)
+ ///
+ /// The IoT Hub service instance URI to connect to.
+ ///
+ ///
+ /// The implementation which will be used to request for the authentication token.
+ ///
+ ///
+ /// Options that allow configuration of requests sent to the IoT Hub service.
+ ///
+ public IoTHubServiceClient(Uri endpoint, TokenCredential credential, IoTHubServiceClientOptions options)
{
Argument.AssertNotNull(options, nameof(options));
+ _endpoint = endpoint;
_clientDiagnostics = new ClientDiagnostics(options);
+
+ options.AddPolicy(new BearerTokenAuthenticationPolicy(credential, GetAuthorizationScopes(_endpoint)), HttpPipelinePosition.PerCall);
_httpPipeline = HttpPipelineBuilder.Build(options);
- _registryManagerRestClient = new RegistryManagerRestClient(_clientDiagnostics, _httpPipeline, endpoint, options.GetVersionString());
+ _registryManagerRestClient = new RegistryManagerRestClient(_clientDiagnostics, _httpPipeline, _endpoint, options.GetVersionString());
_twinRestClient = new TwinRestClient(_clientDiagnostics, _httpPipeline, null, options.GetVersionString());
- _deviceMethodRestClient = new DeviceMethodRestClient(_clientDiagnostics, _httpPipeline, endpoint, options.GetVersionString());
+ _deviceMethodRestClient = new DeviceMethodRestClient(_clientDiagnostics, _httpPipeline, _endpoint, options.GetVersionString());
Devices = new DevicesClient(_registryManagerRestClient, _twinRestClient, _deviceMethodRestClient);
Modules = new ModulesClient(_registryManagerRestClient, _twinRestClient, _deviceMethodRestClient);
@@ -81,5 +155,24 @@ public IoTHubServiceClient(Uri endpoint, IoTHubServiceClientOptions options)
Files = new FilesClient();
Jobs = new JobsClient();
}
+
+ ///
+ /// Gets the scope for authentication/authorization policy.
+ ///
+ /// The IoT Hub service instance Uri.
+ /// List of scopes for the specified endpoint.
+ internal static string[] GetAuthorizationScopes(Uri endpoint)
+ {
+ Argument.AssertNotNull(endpoint, nameof(endpoint));
+ Argument.AssertNotNullOrEmpty(endpoint.AbsoluteUri, nameof(endpoint.AbsoluteUri));
+
+ // TODO: GetAuthorizationScopes for IoT Hub
+ return null;
+ }
+
+ private static Uri BuildEndpointUriFromHostName(string hostName)
+ {
+ return new UriBuilder { Scheme = "https", Host = hostName }.Uri;
+ }
}
}
diff --git a/sdk/iot/Azure.Iot.Hub.Service/tests/Azure.Iot.Hub.Service.Tests.csproj b/sdk/iot/Azure.Iot.Hub.Service/tests/Azure.Iot.Hub.Service.Tests.csproj
index 45ad4b09d71f..8e85dd564054 100644
--- a/sdk/iot/Azure.Iot.Hub.Service/tests/Azure.Iot.Hub.Service.Tests.csproj
+++ b/sdk/iot/Azure.Iot.Hub.Service/tests/Azure.Iot.Hub.Service.Tests.csproj
@@ -6,14 +6,28 @@
-
-
+
+
+
-
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
diff --git a/sdk/iot/Azure.Iot.Hub.Service/tests/E2eTestBase.cs b/sdk/iot/Azure.Iot.Hub.Service/tests/E2eTestBase.cs
new file mode 100644
index 000000000000..dbe4eee71b1d
--- /dev/null
+++ b/sdk/iot/Azure.Iot.Hub.Service/tests/E2eTestBase.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Net;
+using Azure.Core.TestFramework;
+using NUnit.Framework;
+
+namespace Azure.Iot.Hub.Service.Tests
+{
+ ///
+ /// This class will initialize all the settings and create and instance of the IoT Hub service client.
+ ///
+ [Parallelizable(ParallelScope.Self)]
+ public abstract class E2eTestBase : RecordedTestBase
+ {
+ public E2eTestBase(bool isAsync)
+ : base(isAsync, TestSettings.Instance.TestMode)
+ {
+ }
+
+ public E2eTestBase(bool isAsync, RecordedTestMode testMode)
+ : base(isAsync, testMode)
+ {
+ }
+
+ [SetUp]
+ public virtual void SetupE2eTestBase()
+ {
+ TestDiagnostics = false;
+
+ // TODO: set via client options and pipeline instead
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
+ }
+
+ protected IoTHubServiceClient GetClient()
+ {
+ return InstrumentClient(
+ new IoTHubServiceClient(TestSettings.Instance.IotHubConnectionString));
+ }
+
+ protected string GetRandom()
+ {
+ return Recording.GenerateId();
+ }
+ }
+}
diff --git a/sdk/iot/Azure.Iot.Hub.Service/tests/IotHubServiceTestEnvironment.cs b/sdk/iot/Azure.Iot.Hub.Service/tests/IotHubServiceTestEnvironment.cs
new file mode 100644
index 000000000000..c3a37af7d9e9
--- /dev/null
+++ b/sdk/iot/Azure.Iot.Hub.Service/tests/IotHubServiceTestEnvironment.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Azure.Core.TestFramework;
+
+namespace Azure.Iot.Hub.Service.Tests
+{
+ // This class contains the configurations required to be set to run tests against the CI pipeline.
+ public class IotHubServiceTestEnvironment : TestEnvironment
+ {
+ public IotHubServiceTestEnvironment()
+ : base("iot")
+ {
+ }
+ }
+}
diff --git a/sdk/iot/Azure.Iot.Hub.Service/tests/TestSettings.cs b/sdk/iot/Azure.Iot.Hub.Service/tests/TestSettings.cs
new file mode 100644
index 000000000000..753d80a04bf0
--- /dev/null
+++ b/sdk/iot/Azure.Iot.Hub.Service/tests/TestSettings.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Reflection;
+using Azure.Core.TestFramework;
+using Microsoft.Extensions.Configuration;
+
+namespace Azure.Iot.Hub.Service.Tests
+{
+ ///
+ /// These are the settings that will be used by the end-to-end tests tests.
+ /// The json files configured in the config will load the settings specific to a user.
+ ///
+ public class TestSettings
+ {
+ public const string IotHubServiceEnvironmentVariablesPrefix = "IOT";
+
+ // These environment variables are required to be set to run tests against the CI pipeline.
+ // If these environment variables exist in the environment, their values will replace (supersede) config.json values.
+ private static readonly string s_iotHubConnectionString = $"{IotHubServiceEnvironmentVariablesPrefix}_CONNECTION_STRING";
+
+ private static readonly string s_iotHubServiceTestMode = $"AZURE_IOT_TEST_MODE";
+
+ public static TestSettings Instance { get; private set; }
+
+ public RecordedTestMode TestMode { get; set; }
+
+ ///
+ /// The working directory of the tests.
+ ///
+ public string WorkingDirectory { get; private set; }
+
+ ///
+ /// The IoT Hub instance connection string.
+ ///
+ public string IotHubConnectionString { get; set; }
+
+ ///
+ /// The IoT Hub instance hostName.
+ ///
+ public string IotHubHostName { get; set; }
+
+ static TestSettings()
+ {
+ if (Instance != null)
+ {
+ return;
+ }
+
+ string codeBase = Assembly.GetExecutingAssembly().CodeBase;
+ var uri = new UriBuilder(codeBase);
+ string path = Uri.UnescapeDataString(uri.Path);
+ string workingDirectory = Path.GetDirectoryName(path);
+
+ string userName = Environment.UserName;
+
+ // Initialize the settings related to IoT Hub instance and auth
+ var testSettingsConfigBuilder = new ConfigurationBuilder();
+
+ string testSettingsCommonPath = Path.Combine(workingDirectory, "config", "common.config.json");
+ testSettingsConfigBuilder.AddJsonFile(testSettingsCommonPath);
+
+ string testSettingsUserPath = Path.Combine(workingDirectory, "config", $"{userName}.config.json");
+ if (File.Exists(testSettingsUserPath))
+ {
+ testSettingsConfigBuilder.AddJsonFile(testSettingsUserPath);
+ }
+
+ IConfiguration config = testSettingsConfigBuilder.Build();
+
+ // This will set the values from the above config files into the TestSettings Instance.
+ Instance = config.Get();
+ Instance.WorkingDirectory = workingDirectory;
+
+ // We will override settings if they can be found in the environment variables.
+ OverrideFromEnvVariables();
+ }
+
+ // These environment variables are required to be set to run tests against the CI pipeline.
+ private static void OverrideFromEnvVariables()
+ {
+ }
+ }
+}
diff --git a/sdk/iot/Azure.Iot.Hub.Service/tests/prerequisites/setup.ps1 b/sdk/iot/Azure.Iot.Hub.Service/tests/prerequisites/setup.ps1
index e331d2ad6fd2..30340a77e0af 100644
--- a/sdk/iot/Azure.Iot.Hub.Service/tests/prerequisites/setup.ps1
+++ b/sdk/iot/Azure.Iot.Hub.Service/tests/prerequisites/setup.ps1
@@ -115,8 +115,13 @@ Write-Host("Writing user config file - $fileName`n")
$appSecretJsonEscaped = ConvertTo-Json $appSecret
$config = @"
{
+<<<<<<< HEAD
"IotHubConnectionString": "$iotHubConnectionString",
"IotHubHostName": "$iotHubHostName",
+=======
+ "IotHubHostName": "$iotHubHostName",
+ "IotHubConnectionString": "$iotHubConnectionString",
+>>>>>>> 0223e524ec... feat(e2e-tests): Add initial setup for E2E tests
"ApplicationId": "$appId",
"ClientSecret": $appSecretJsonEscaped,
"TestMode": "Live"