Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pakrym Is there support for Connection String auth in Azure.Core? Is this something we should add just for our client library or will there be support in Azure.Core? We can contribute to the codebase.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pakrym We have taken a dependency on the Azure.Core.ConnectionString parser for parsing the connection string. Do we have an Azure.Core authentication policy that will inject the generated sas token (from the connection string shared access key and policy) into the HTTP pipeline?

The app configuration SDK, that we've used for reference, also uses connection strings, but they've defined their own http pipeline policy for adding the auth header to the HTTP requests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we don't have a shared policy like that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pakrym Do you think it would make sense to have a common Policy that all client libraries can share? Just like the Bearer token policy?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the policy just adds the authentication header so it could be common right? If there is a difference in getting the value of the header, each client can have their own implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abhipsaMisra - We should be able to re-use the AzureKeyCredentialPolicy and just pass the necessary value to it instead of creating a new policy. Also do you see that the code will be same for ours and other libraries? We should try to get to a common solution in that case. My minimal knowledge of connection strings suggests that it should be same but I might be wrong.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes @pakrym, we should be good to reuse the AzureKeyCredentialPolicy to pass the token to the http pipeline. For the implementation for actually generating the sas token, our code is here: https://github.com/Azure/azure-sdk-for-net/blob/feature/abmisr/devicesE2e/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureBuilder.cs#L36
It uses sha256 on the access key to sign the hostname and token expiry time. App configuration uses a similar mechanism to compute their token, but their request string has a few more parameters.

@vinagesh : I would think so, yes, but I don't think that's the case. Looking at app configuration SDK, their implementation for generating tokens wouldn't work for us, so I am not sure exactly how much of this we can generalize. This will need deeper investigation and input from service teams.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
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)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to allow negative timespan values? I don't see a check anywhere

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll add a check for that.
I actually don't have a way for the user to set the sas token TimeToLive right now; maybe it should be customizable via IotHubServiceClientOptions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep I can see users wanting to provide that as input.

Copy link
Member

@vinagesh vinagesh Jun 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just fyi @prmathur-microsoft - This is internal and users will not be calling it. We are using it to get SAS token from the connection string provided by users.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes Sindhu. But data is flowing from CS which is input from customer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a very basic question :) Do connection strings have the timeToLive on them or do we calculate it somehow? I will go and investigate one for IotHub as I've not paid a lot of attention to the values in a connection string earlier.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_hostName = hostName;
_sharedAccessPolicy = sharedAccessPolicy;
_sharedAccessKey = sharedAccessKey;
_timeToLive = timeToLive ?? s_defaultTimeToLive;

_cachedSasToken = null;
}

string ISasTokenProvider.GetSasToken()
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can a user provide sastoken?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the current implementation, no. User only provides a connection string, and the sdk evaluates the sas token based on that. The Azure SDK guidelines say: YOU MAY offer a way to create credentials from a connection string only if the service offers a connection string via the Azure portal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the Azure SDK guidelines say about users wanting to provide sastoken?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any specific reference to sas tokens, they say: Typically, binding parameters would include a URI to the service endpoint and authorization credentials.
This is something we are hoping to get some clarity on from Krzyszstof (I'll reach out to him after our meeting with Andrei on Monday).

Copy link
Member Author

@abhipsaMisra abhipsaMisra Jun 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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