-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
a1d5605
commit 9db34d6
Showing
16 changed files
with
615 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
sdk/iot/Azure.Iot.Hub.Service/src/Authentication/FixedSasTokenProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
sdk/iot/Azure.Iot.Hub.Service/src/Authentication/ISasTokenProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
62 changes: 62 additions & 0 deletions
62
sdk/iot/Azure.Iot.Hub.Service/src/Authentication/IotHubConnectionString.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenAuthenticationPolicy.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} | ||
} |
81 changes: 81 additions & 0 deletions
81
sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenProviderWithSharedAccessKey.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
80 changes: 80 additions & 0 deletions
80
sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureBuilder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))); | ||
} | ||
} | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureConstants.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.