Skip to content

Commit

Permalink
[Event Hubs Client] Connection String SAS Support (#14858)
Browse files Browse the repository at this point in the history
The focus of these changes is to add support for a precomputed shared access
signature token to be used as part of the connection string.
  • Loading branch information
jsquire committed Sep 4, 2020
1 parent 693071c commit 4aea0c3
Show file tree
Hide file tree
Showing 16 changed files with 306 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Azure Event Hubs is a highly scalable publish-subscribe service that can ingest millions of events per second and stream them to multiple consumers. This library extends its Event Processor with durable storage for checkpoint information using Azure Blob storage. For more information about Event Hubs, see https://azure.microsoft.com/en-us/services/event-hubs/</Description>
<Version>5.2.0-preview.3</Version>
<Version>5.2.0-preview.4</Version>
<ApiCompatVersion>5.1.0</ApiCompatVersion>
<PackageTags>Azure;Event Hubs;EventHubs;.NET;Event Processor;EventProcessor;$(PackageCommonTags)</PackageTags>
<TargetFrameworks>$(RequiredTargetFrameworks)</TargetFrameworks>
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ public SharedAccessSignatureCredential(SharedAccessSignature signature)
public override AccessToken GetToken(TokenRequestContext requestContext,
CancellationToken cancellationToken)
{
if (SharedAccessSignature.SignatureExpiration <= DateTimeOffset.UtcNow.Add(SignatureRefreshBuffer))
// If the signature was derived from a shared key rather than being provided externally,
// determine if the expiration is approaching and attempt to extend the token.

if ((!string.IsNullOrEmpty(SharedAccessSignature.SharedAccessKey))
&& (SharedAccessSignature.SignatureExpiration <= DateTimeOffset.UtcNow.Add(SignatureRefreshBuffer)))
{
lock (SignatureSyncRoot)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ internal static class ConnectionStringParser
/// <summary>The token that identifies the value of a shared access key.</summary>
private const string SharedAccessKeyValueToken = "SharedAccessKey";

/// <summary>The token that identifies the value of a shared access signature.</summary>
private const string SharedAccessSignatureToken = "SharedAccessSignature";

/// <summary>The character used to separate a token and its value in the connection string.</summary>
private const char TokenValueSeparator = '=';

Expand Down Expand Up @@ -64,7 +67,8 @@ public static ConnectionStringProperties Parse(string connectionString)
EndpointToken: default(UriBuilder),
EventHubNameToken: default(string),
SharedAccessKeyNameToken: default(string),
SharedAccessKeyValueToken: default(string)
SharedAccessKeyValueToken: default(string),
SharedAccessSignatureToken: default(string)
);

while (currentPosition != -1)
Expand Down Expand Up @@ -140,6 +144,10 @@ public static ConnectionStringProperties Parse(string connectionString)
{
parsedValues.SharedAccessKeyValueToken = value;
}
else if (string.Compare(SharedAccessSignatureToken, token, StringComparison.OrdinalIgnoreCase) == 0)
{
parsedValues.SharedAccessSignatureToken = value;
}
}
else if ((slice.Length != 1) || (slice[0] != TokenValuePairDelimiter))
{
Expand All @@ -158,7 +166,8 @@ public static ConnectionStringProperties Parse(string connectionString)
parsedValues.EndpointToken?.Uri,
parsedValues.EventHubNameToken,
parsedValues.SharedAccessKeyNameToken,
parsedValues.SharedAccessKeyValueToken
parsedValues.SharedAccessKeyValueToken,
parsedValues.SharedAccessSignatureToken
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ internal struct ConnectionStringProperties
///
public string SharedAccessKey { get; }

/// <summary>
/// The value of the fully-formed shared access signature, either for the Event Hubs
/// namespace or the Event Hub.
/// </summary>
///
public string SharedAccessSignature { get; }

/// <summary>
/// Initializes a new instance of the <see cref="ConnectionStringProperties"/> structure.
/// </summary>
Expand All @@ -48,16 +55,19 @@ internal struct ConnectionStringProperties
/// <param name="eventHubName">The name of the specific Event Hub under the namespace.</param>
/// <param name="sharedAccessKeyName">The name of the shared access key, to use authorization.</param>
/// <param name="sharedAccessKey">The shared access key to use for authorization.</param>
/// <param name="sharedAccessSignature">The precomputed shared access signature to use for authorization.</param>
///
public ConnectionStringProperties(Uri endpoint,
string eventHubName,
string sharedAccessKeyName,
string sharedAccessKey)
string sharedAccessKey,
string sharedAccessSignature)
{
Endpoint = endpoint;
EventHubName = eventHubName;
SharedAccessKeyName = sharedAccessKeyName;
SharedAccessKey = sharedAccessKey;
SharedAccessSignature = sharedAccessSignature;
}

/// <summary>
Expand All @@ -84,12 +94,23 @@ public void Validate(string explicitEventHubName,
throw new ArgumentException(Resources.OnlyOneEventHubNameMayBeSpecified, connectionStringArgumentName);
}

// The connection string may contain a precomputed shared access signature OR a shared key name and value,
// but not both.

if ((!string.IsNullOrEmpty(SharedAccessSignature))
&& ((!string.IsNullOrEmpty(SharedAccessKeyName)) || (!string.IsNullOrEmpty(SharedAccessKey))))
{
throw new ArgumentException(Resources.OnlyOneSharedAccessAuthorizationMayBeSpecified, connectionStringArgumentName);
}

// Ensure that each of the needed components are present for connecting.

if ((string.IsNullOrEmpty(explicitEventHubName)) && (string.IsNullOrEmpty(EventHubName))
|| (string.IsNullOrEmpty(Endpoint?.Host))
|| (string.IsNullOrEmpty(SharedAccessKeyName))
|| (string.IsNullOrEmpty(SharedAccessKey)))
var hasSharedKey = ((!string.IsNullOrEmpty(SharedAccessKeyName)) && (!string.IsNullOrEmpty(SharedAccessKey)));
var hasSharedSignature = (!string.IsNullOrEmpty(SharedAccessSignature));

if (string.IsNullOrEmpty(Endpoint?.Host)
|| ((string.IsNullOrEmpty(explicitEventHubName)) && (string.IsNullOrEmpty(EventHubName)))
|| ((!hasSharedKey) && (!hasSharedSignature)))
{
throw new ArgumentException(Resources.MissingConnectionInformation, connectionStringArgumentName);
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
<value>The connection string could not be parsed; either it was malformed or contains no well-known tokens.</value>
</data>
<data name="MissingConnectionInformation" xml:space="preserve">
<value>The connection string used for an Event Hub client must specify the Event Hubs namespace host, and a Shared Access Key (both the name and value) to be valid. The path to an Event Hub must be included in the connection string or specified separately.</value>
<value>The connection string used for an Event Hub client must specify the Event Hubs namespace host, and either a Shared Access Key (both the name and value) or Shared Access Signature to be valid. The path to an Event Hub must be included in the connection string or specified separately.</value>
</data>
<data name="OnlyOneEventHubNameMayBeSpecified" xml:space="preserve">
<value>The path to an Event Hub may be specified as part of the connection string or as a separate value, but not both. Please verify that your connection string does not have the `EntityPath` token if you are passing an explicit Event Hub name.</value>
Expand Down Expand Up @@ -288,4 +288,7 @@
<data name="AggregateEventProcessingExceptionMessage" xml:space="preserve">
<value>One or more exceptions occured during event processing. Please see the inner exceptions for more detail.</value>
</data>
<data name="OnlyOneSharedAccessAuthorizationMayBeSpecified" xml:space="preserve">
<value>The authorization for a connection string may specifiy a shared key or precomputed shared access signature, but not both. Please verify that your connection string does not have the `SharedAccessSignature` token if you are passing the `SharedKeyName` and `SharedKey`.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using Azure.Core.TestFramework;
using Azure.Messaging.EventHubs.Authorization;
using Azure.Messaging.EventHubs.Core;

namespace Azure.Messaging.EventHubs.Tests
Expand Down Expand Up @@ -160,10 +161,31 @@ private EventHubsTestEnvironment() : base("eventhub")
/// Live tests.
/// </summary>
///
/// <value>The namespace connection string is based on the dynamic Event Hubs scope.</value>
/// <param name="eventHubName">The name of the Event Hub to base the connection string on.</param>
///
/// <return>The Event Hub-level connection string.</return>
///
public string BuildConnectionStringForEventHub(string eventHubName) => $"{ EventHubsConnectionString };EntityPath={ eventHubName }";

/// <summary>
/// Builds a connection string for the Event Hubs namespace used for Live tests, creating a shared access signature
/// in place of the shared key.
/// </summary>
///
/// <param name="eventHubName">The name of the Event Hub to base the connection string on.</param>
/// <param name="signatureAudience">The audience to use for the shared access signature.</param>
/// <param name="validDurationMinutes">The duration, in minutes, that the signature should be considered valid for.</param>
///
/// <returns>The namespace connection string with a shared access signature based on the shared key of the current scope.</value>
///
public string BuildConnectionStringWithSharedAccessSignature(string eventHubName,
string signatureAudience,
int validDurationMinutes = 30)
{
var signature = new SharedAccessSignature(signatureAudience, SharedAccessKeyName, SharedAccessKey, TimeSpan.FromMinutes(validDurationMinutes));
return $"Endpoint={ ParsedConnectionString.Value.Endpoint };EntityPath={ eventHubName };SharedAccessSignature={ signature.Value }";
}

/// <summary>
/// Ensures that an Event Hubs namespace is available for the test run, using one if provided by the
/// <see cref="EventHubsNamespaceConnectionStringEnvironmentVariable" /> or creating a new Azure resource specific
Expand Down
67 changes: 59 additions & 8 deletions ...re.Messaging.EventHubs.Shared/tests/Authorization/SharedAccessSignatureCredentialTests.cs
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public void ConstructorInitializesProperties()
}

/// <summary>
/// Verifies functionality of the constructor.
/// Verifies functionality of the <see cref="SharedAccessSignatureCredential.GetToken" />
/// method.
/// </summary>
///
[Test]
Expand All @@ -58,7 +59,8 @@ public void GetTokenReturnsTheSignatureValue()
}

/// <summary>
/// Verifies functionality of the constructor.
/// Verifies functionality of the <see cref="SharedAccessSignatureCredential.GetToken" />
/// method.
/// </summary>
///
[Test]
Expand All @@ -72,7 +74,8 @@ public void GetTokenIgnoresScopeAndCancellationToken()
}

/// <summary>
/// Verifies functionality of the constructor.
/// Verifies functionality of the <see cref="SharedAccessSignatureCredential.GetToken" />
/// method.
/// </summary>
///
[Test]
Expand All @@ -88,7 +91,8 @@ public async Task GetTokenAsyncReturnsTheSignatureValue()
}

/// <summary>
/// Verifies functionality of the constructor.
/// Verifies functionality of the <see cref="SharedAccessSignatureCredential.GetToken" />
/// method.
/// </summary>
///
[Test]
Expand All @@ -104,11 +108,12 @@ public async Task GetTokenAsyncIgnoresScopeAndCancellationToken()
}

/// <summary>
/// Verifies functionality of the constructor.
/// Verifies functionality of the <see cref="SharedAccessSignatureCredential.GetToken" />
/// method.
/// </summary>
///
[Test]
public void GetTokenExtendsAnExpiredToken()
public void GetTokenExtendsAnExpiredTokenWhenCreatedWithTheSharedKey()
{
var value = "TOkEn!";
var signature = new SharedAccessSignature("hub-name", "keyName", "key", value, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(2)));
Expand All @@ -119,11 +124,12 @@ public void GetTokenExtendsAnExpiredToken()
}

/// <summary>
/// Verifies functionality of the constructor.
/// Verifies functionality of the <see cref="SharedAccessSignatureCredential.GetToken" />
/// method.
/// </summary>
///
[Test]
public void GetTokenExtendsATokenCloseToExpiring()
public void GetTokenExtendsATokenCloseToExpiringWhenCreatedWithTheSharedKey()
{
var value = "TOkEn!";
var tokenExpiration = DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(GetSignatureRefreshBuffer().TotalSeconds / 2));
Expand All @@ -134,6 +140,40 @@ public void GetTokenExtendsATokenCloseToExpiring()
Assert.That(credential.GetToken(new TokenRequestContext(), default).ExpiresOn, Is.EqualTo(expectedExpiration).Within(TimeSpan.FromMinutes(1)));
}

/// <summary>
/// Verifies functionality of the <see cref="SharedAccessSignatureCredential.GetToken" />
/// method.
/// </summary>
///
[Test]
public void GetTokenDoesNotExtendAnExpiredTokenWhenCreatedWithoutTheKey()
{
var expectedExpiration = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromHours(2));
var value = $"SharedAccessSignature sr=https%3A%2F%2Ffake-test.servicebus.windows.net%2F&sig=nNBNavJfBiHuXUzWOLhSvI3bVgqbQUzA7Po8%2F4wQQng%3D&se={ ToUnixTime(expectedExpiration) }&skn=fakeKey";
var sourceSignature = new SharedAccessSignature("fake-test", "fakeKey", "ABC123", value, expectedExpiration).Value;
var signature = new SharedAccessSignature(sourceSignature);
var credential = new SharedAccessSignatureCredential(signature);

Assert.That(credential.GetToken(new TokenRequestContext(), default).ExpiresOn, Is.EqualTo(expectedExpiration).Within(TimeSpan.FromMinutes(1)));
}

/// <summary>
/// Verifies functionality of the <see cref="SharedAccessSignatureCredential.GetToken" />
/// method.
/// </summary>
///
[Test]
public void GetTokenDoesNotExtendATokenCloseToExpiringWhenCreatedWithoutTheKey()
{
var tokenExpiration = DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(GetSignatureRefreshBuffer().TotalSeconds / 2));
var value = $"SharedAccessSignature sr=https%3A%2F%2Ffake-test.servicebus.windows.net%2F&sig=nNBNavJfBiHuXUzWOLhSvI3bVgqbQUzA7Po8%2F4wQQng%3D&se={ ToUnixTime(tokenExpiration) }&skn=fakeKey";
var sourceSignature = new SharedAccessSignature("fake-test", "fakeKey", "ABC123", value, tokenExpiration).Value;
var signature = new SharedAccessSignature(sourceSignature);
var credential = new SharedAccessSignatureCredential(signature);

Assert.That(credential.GetToken(new TokenRequestContext(), default).ExpiresOn, Is.EqualTo(tokenExpiration).Within(TimeSpan.FromMinutes(1)));
}

/// <summary>
/// Verifies that a signature can be rotated without refreshing its validity.
/// </summary>
Expand All @@ -155,6 +195,17 @@ public void ShouldUpdateSharedAccessKey()
Assert.That(newSignature.SignatureExpiration, Is.EqualTo(signature.SignatureExpiration));
}

/// <summary>
/// Converts a <see cref="DateTimeOffset" /> value to the corresponding Unix-style time stamp.
/// </summary>
///
/// <param name="timestamp">The date/time to convert.</param>
///
/// <returns>The Unix-style times tamp which corresponds to the specified date/time.</returns>
///
private static long ToUnixTime(DateTimeOffset timestamp) =>
Convert.ToInt64((timestamp - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);

/// <summary>
/// Retrieves the shared access signature from the credential using its private accessor.
/// </summary>
Expand Down
Loading

0 comments on commit 4aea0c3

Please sign in to comment.