Skip to content

Commit

Permalink
IoT hub service client authentication via connection string (#12731)
Browse files Browse the repository at this point in the history
* feat(e2e-tests): Add initial setup for E2E tests

* feat(iot-service): Add authentication via connection string
  • Loading branch information
abhipsaMisra authored Jun 16, 2020
1 parent a1d5605 commit 9db34d6
Show file tree
Hide file tree
Showing 16 changed files with 615 additions and 11 deletions.
4 changes: 4 additions & 0 deletions sdk/iot/Azure.Iot.Hub.Service/CodeMaid.config
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
<setting name="Progressing_ShowBuildProgressOnBuildStart" serializeAs="String">
<value>False</value>
</setting>
<setting name="Cleaning_SkipRemoveAndSortUsingStatementsDuringAutoCleanupOnSave"
serializeAs="String">
<value>False</value>
</setting>
</SteveCadwallader.CodeMaid.Properties.Settings>
</userSettings>
</configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -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; } }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Iot.Hub.Service.Authentication
{
/// <summary>
/// Implementation of a shared access signature provider with a fixed token.
/// </summary>
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Iot.Hub.Service.Authentication
{
/// <summary>
/// The token provider interface for shared access signature based authentication.
/// </summary>
internal interface ISasTokenProvider
{
/// <summary>
/// Retrieve the shared access signature to be used.
/// </summary>
/// <returns>The shared access signature to be used for authenticating HTTP requests to the service. It is called once per HTTP request.</returns>
public string GetSasToken();
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Implementation for creating the shared access signature token provider.
/// </summary>
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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
internal class SasTokenAuthenticationPolicy : HttpPipelinePolicy
{
private readonly ISasTokenProvider _sasTokenProvider;

internal SasTokenAuthenticationPolicy(ISasTokenProvider sasTokenProvider)
{
_sasTokenProvider = sasTokenProvider;
}

public override void Process(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
{
AddHeaders(message);
ProcessNext(message, pipeline);
}

public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
{
AddHeaders(message);
await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
}

private void AddHeaders(HttpMessage message)
{
message.Request.Headers.Add(HttpHeader.Names.Authorization, _sasTokenProvider.GetSasToken());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Azure.Iot.Hub.Service.Authentication
{
/// <summary>
/// Implementation of a shared access signature provider with token caching and refresh.
/// </summary>
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Builds the shared access signature based on the access policy passed.
/// </summary>
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<string>
{
audience,
expiresOn
};

// Example string to be signed:
// dh://myiothub.azure-devices.net/a/b/c?myvalue1=a
// <Value for ExpiresOn>

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=<Signature>&se=<ExpiresOnValue>[&skn=<KeyName>]

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)));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Azure.Iot.Hub.Service.Authentication
{
/// <summary>
/// The constants used for building the IoT hub service shared access signature token.
/// </summary>
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
<Compile Include="$(AzureCoreSharedSources)Argument.cs">
<LinkBase>Shared\Azure.Core</LinkBase>
</Compile>
<Compile Include="$(AzureCoreSharedSources)ConnectionString.cs">
<LinkBase>Shared\Azure.Core</LinkBase>
</Compile>
</ItemGroup>

<Import Project="$(MSBuildThisFileDirectory)..\..\..\core\Azure.Core\src\Azure.Core.props" />
Expand Down
Loading

0 comments on commit 9db34d6

Please sign in to comment.