Skip to content

Commit 596ab9b

Browse files
authored
[Service Bus Client] Connection String SAS Support (#14806)
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.
1 parent 0972863 commit 596ab9b

File tree

12 files changed

+222
-34
lines changed

12 files changed

+222
-34
lines changed

sdk/servicebus/Azure.Messaging.ServiceBus/src/Authorization/SharedAccessSignatureCredential.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ public override AccessToken GetToken(
5959
TokenRequestContext requestContext,
6060
CancellationToken cancellationToken)
6161
{
62-
if (SharedAccessSignature.SignatureExpiration <= DateTimeOffset.UtcNow.Add(SignatureRefreshBuffer))
62+
// If the signature was derived from a shared key rather than being provided externally,
63+
// determine if the expiration is approaching and attempt to extend the token.
64+
65+
if ((!string.IsNullOrEmpty(SharedAccessSignature.SharedAccessKey))
66+
&& (SharedAccessSignature.SignatureExpiration <= DateTimeOffset.UtcNow.Add(SignatureRefreshBuffer)))
6367
{
6468
lock (SignatureSyncRoot)
6569
{

sdk/servicebus/Azure.Messaging.ServiceBus/src/Core/ConnectionStringParser.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ internal static class ConnectionStringParser
3333
/// <summary>The token that identifies the value of a shared access key.</summary>
3434
private const string SharedAccessKeyValueToken = "SharedAccessKey";
3535

36+
/// <summary>The token that identifies the value of a shared access signature.</summary>
37+
private const string SharedAccessSignatureToken = "SharedAccessSignature";
38+
3639
/// <summary>
3740
/// Parses the specified Service Bus connection string into its component properties.
3841
/// </summary>
@@ -61,7 +64,8 @@ public static ConnectionStringProperties Parse(string connectionString)
6164
EndpointToken: default(UriBuilder),
6265
EntityNameToken: default(string),
6366
SharedAccessKeyNameToken: default(string),
64-
SharedAccessKeyValueToken: default(string)
67+
SharedAccessKeyValueToken: default(string),
68+
SharedAccessSignatureToken: default(string)
6569
);
6670

6771
while (currentPosition != -1)
@@ -131,6 +135,10 @@ public static ConnectionStringProperties Parse(string connectionString)
131135
{
132136
parsedValues.SharedAccessKeyValueToken = value;
133137
}
138+
else if (string.Compare(SharedAccessSignatureToken, token, StringComparison.OrdinalIgnoreCase) == 0)
139+
{
140+
parsedValues.SharedAccessSignatureToken = value;
141+
}
134142
}
135143
else if ((slice.Length != 1) || (slice[0] != TokenValuePairDelimiter))
136144
{
@@ -149,7 +157,8 @@ public static ConnectionStringProperties Parse(string connectionString)
149157
parsedValues.EndpointToken?.Uri,
150158
parsedValues.EntityNameToken,
151159
parsedValues.SharedAccessKeyNameToken,
152-
parsedValues.SharedAccessKeyValueToken
160+
parsedValues.SharedAccessKeyValueToken,
161+
parsedValues.SharedAccessSignatureToken
153162
);
154163
}
155164
}

sdk/servicebus/Azure.Messaging.ServiceBus/src/Core/ConnectionStringProperties.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ internal struct ConnectionStringProperties
4040
///
4141
public string SharedAccessKey { get; }
4242

43+
/// <summary>
44+
/// The value of the fully-formed shared access signature, either for the Service Bus
45+
/// namespace or the Service Bus entity.
46+
/// </summary>
47+
///
48+
public string SharedAccessSignature { get; }
49+
4350
/// <summary>
4451
/// Initializes a new instance of the <see cref="ConnectionStringProperties"/> structure.
4552
/// </summary>
@@ -48,17 +55,20 @@ internal struct ConnectionStringProperties
4855
/// <param name="entityName">The name of the specific Service Bus entity under the namespace.</param>
4956
/// <param name="sharedAccessKeyName">The name of the shared access key, to use authorization.</param>
5057
/// <param name="sharedAccessKey">The shared access key to use for authorization.</param>
58+
/// <param name="sharedAccessSignature">The precomputed shared access signature to use for authorization.</param>
5159
///
5260
public ConnectionStringProperties(
5361
Uri endpoint,
5462
string entityName,
5563
string sharedAccessKeyName,
56-
string sharedAccessKey)
64+
string sharedAccessKey,
65+
string sharedAccessSignature)
5766
{
5867
Endpoint = endpoint;
5968
EntityPath = entityName;
6069
SharedAccessKeyName = sharedAccessKeyName;
6170
SharedAccessKey = sharedAccessKey;
71+
SharedAccessSignature = sharedAccessSignature;
6272
}
6373
}
6474
}

sdk/servicebus/Azure.Messaging.ServiceBus/src/Primitives/ServiceBusConnection.cs

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -83,33 +83,34 @@ internal ServiceBusConnection(
8383
ServiceBusClientOptions options)
8484
{
8585
Argument.AssertNotNullOrEmpty(connectionString, nameof(connectionString));
86-
8786
ValidateConnectionOptions(options);
88-
ConnectionStringProperties connectionStringProperties = ConnectionStringParser.Parse(connectionString);
8987

90-
if (string.IsNullOrEmpty(connectionStringProperties.Endpoint?.Host)
91-
|| string.IsNullOrEmpty(connectionStringProperties.SharedAccessKeyName)
92-
|| string.IsNullOrEmpty(connectionStringProperties.SharedAccessKey))
93-
{
94-
throw new ArgumentException(Resources.MissingConnectionInformation, nameof(connectionString));
95-
}
88+
var connectionStringProperties = ConnectionStringParser.Parse(connectionString);
89+
ValidateConnectionStringProperties(connectionStringProperties, nameof(connectionString));
9690

9791
FullyQualifiedNamespace = connectionStringProperties.Endpoint.Host;
9892
TransportType = options.TransportType;
9993
EntityPath = connectionStringProperties.EntityPath;
10094
RetryOptions = options.RetryOptions;
10195

102-
var sharedAccessSignature = new SharedAccessSignature
103-
(
104-
BuildAudienceResource(options.TransportType, FullyQualifiedNamespace, EntityPath),
105-
connectionStringProperties.SharedAccessKeyName,
106-
connectionStringProperties.SharedAccessKey
107-
);
96+
SharedAccessSignature sharedAccessSignature;
97+
98+
if (string.IsNullOrEmpty(connectionStringProperties.SharedAccessSignature))
99+
{
100+
sharedAccessSignature = new SharedAccessSignature(
101+
BuildConnectionResource(options.TransportType, FullyQualifiedNamespace, EntityPath),
102+
connectionStringProperties.SharedAccessKeyName,
103+
connectionStringProperties.SharedAccessKey);
104+
}
105+
else
106+
{
107+
sharedAccessSignature = new SharedAccessSignature(connectionStringProperties.SharedAccessSignature);
108+
}
108109

109110
var sharedCredential = new SharedAccessSignatureCredential(sharedAccessSignature);
110111
var tokenCredential = new ServiceBusTokenCredential(
111112
sharedCredential,
112-
BuildAudienceResource(TransportType, FullyQualifiedNamespace, EntityPath));
113+
BuildConnectionResource(TransportType, FullyQualifiedNamespace, EntityPath));
113114
#pragma warning disable CA2214 // Do not call overridable methods in constructors. This internal method is virtual for testing purposes.
114115
_innerClient = CreateTransportClient(tokenCredential, options);
115116
#pragma warning restore CA2214 // Do not call overridable methods in constructors
@@ -137,11 +138,11 @@ internal ServiceBusConnection(
137138
break;
138139

139140
case ServiceBusSharedKeyCredential sharedKeyCredential:
140-
credential = sharedKeyCredential.AsSharedAccessSignatureCredential(BuildAudienceResource(options.TransportType, fullyQualifiedNamespace, EntityPath));
141+
credential = sharedKeyCredential.AsSharedAccessSignatureCredential(BuildConnectionResource(options.TransportType, fullyQualifiedNamespace, EntityPath));
141142
break;
142143
}
143144

144-
var tokenCredential = new ServiceBusTokenCredential(credential, BuildAudienceResource(options.TransportType, fullyQualifiedNamespace, EntityPath));
145+
var tokenCredential = new ServiceBusTokenCredential(credential, BuildConnectionResource(options.TransportType, fullyQualifiedNamespace, EntityPath));
145146

146147
FullyQualifiedNamespace = fullyQualifiedNamespace;
147148
TransportType = options.TransportType;
@@ -265,7 +266,7 @@ internal virtual TransportClient CreateTransportClient(
265266
}
266267

267268
/// <summary>
268-
/// Builds the audience for use in the signature.
269+
/// Builds the audience of the connection for use in the signature.
269270
/// </summary>
270271
///
271272
/// <param name="transportType">The type of protocol and transport that will be used for communicating with the Service Bus service.</param>
@@ -274,7 +275,7 @@ internal virtual TransportClient CreateTransportClient(
274275
///
275276
/// <returns>The value to use as the audience of the signature.</returns>
276277
///
277-
private static string BuildAudienceResource(
278+
internal static string BuildConnectionResource(
278279
ServiceBusTransportType transportType,
279280
string fullyQualifiedNamespace,
280281
string entityName)
@@ -297,6 +298,12 @@ private static string BuildAudienceResource(
297298
return builder.Uri.AbsoluteUri.ToLowerInvariant();
298299
}
299300

301+
/// <summary>
302+
/// Throw an ObjectDisposedException if the object is Closing.
303+
/// </summary>
304+
internal virtual void ThrowIfClosed() =>
305+
Argument.AssertNotDisposed(IsClosed, nameof(ServiceBusConnection));
306+
300307
/// <summary>
301308
/// Performs the actions needed to validate the <see cref="ServiceBusClientOptions" /> associated
302309
/// with this client.
@@ -327,13 +334,36 @@ private static void ValidateConnectionOptions(ServiceBusClientOptions connection
327334
}
328335

329336
/// <summary>
330-
/// Throw an ObjectDisposedException if the object is Closing.
337+
/// Performs the actions needed to validate the set of connection string properties for connecting to the
338+
/// Service Bus service.
331339
/// </summary>
332-
internal virtual void ThrowIfClosed()
340+
///
341+
/// <param name="connectionStringProperties">The set of connection string properties to validate.</param>
342+
/// <param name="connectionStringArgumentName">The name of the argument associated with the connection string; to be used when raising <see cref="ArgumentException" /> variants.</param>
343+
///
344+
/// <exception cref="ArgumentException">In the case that the properties violate an invariant or otherwise represent a combination that is not permissible, an appropriate exception will be thrown.</exception>
345+
///
346+
private static void ValidateConnectionStringProperties(
347+
ConnectionStringProperties connectionStringProperties,
348+
string connectionStringArgumentName)
333349
{
334-
if (IsClosed)
350+
var hasSharedKey = ((!string.IsNullOrEmpty(connectionStringProperties.SharedAccessKeyName)) && (!string.IsNullOrEmpty(connectionStringProperties.SharedAccessKey)));
351+
var hasSharedSignature = (!string.IsNullOrEmpty(connectionStringProperties.SharedAccessSignature));
352+
353+
// Ensure that each of the needed components are present for connecting.
354+
355+
if ((string.IsNullOrEmpty(connectionStringProperties.Endpoint?.Host))
356+
|| ((!hasSharedKey) && (!hasSharedSignature)))
357+
{
358+
throw new ArgumentException(Resources.MissingConnectionInformation, connectionStringArgumentName);
359+
}
360+
361+
// The connection string may contain a precomputed shared access signature OR a shared key name and value,
362+
// but not both.
363+
364+
if (hasSharedKey && hasSharedSignature)
335365
{
336-
throw new ObjectDisposedException($"{nameof(ServiceBusConnection)} has already been closed. Please create a new instance");
366+
throw new ArgumentException(Resources.OnlyOneSharedAccessAuthorizationMayBeSpecified, connectionStringArgumentName);
337367
}
338368
}
339369
}

sdk/servicebus/Azure.Messaging.ServiceBus/src/Resources.Designer.cs

100755100644
Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/servicebus/Azure.Messaging.ServiceBus/src/Resources.resx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@
256256
<value>The message (id:{0}, size:{1} bytes) is larger than is currently allowed ({2} bytes).</value>
257257
</data>
258258
<data name="MissingConnectionInformation" xml:space="preserve">
259-
<value>The connection string used for an Service Bus entity client must specify the Service Bus namespace host, and a Shared Access Signature (both the name and value) to be valid. The path to an Service Bus entity must be included in the connection string or specified separately.</value>
259+
<value>The connection string used for an Service Bus client must specify the Service Bus namespace host and either a Shared Access Key (both the name and value) OR a Shard Access Signature to be valid.</value>
260260
</data>
261261
<data name="OnlyOneEntityNameMayBeSpecified" xml:space="preserve">
262262
<value>The path to an Service Bus entity may be specified as part of the connection string or as a separate value, but not both.</value>
@@ -297,4 +297,7 @@
297297
<data name="MessageProcessorIsNotRunning" xml:space="preserve">
298298
<value>The message processor is not currently running. It needs to be started before it can be stopped.</value>
299299
</data>
300-
</root>
300+
<data name="OnlyOneSharedAccessAuthorizationMayBeSpecified" xml:space="preserve">
301+
<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>
302+
</data>
303+
</root>

sdk/servicebus/Azure.Messaging.ServiceBus/tests/Client/ServiceBusClientLiveTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,40 @@ namespace Azure.Messaging.ServiceBus.Tests.Client
1212
{
1313
public class ServiceBusClientLiveTests : ServiceBusLiveTestBase
1414
{
15+
/// <summary>
16+
/// Verifies that the <see cref="EventHubConnection" /> is able to
17+
/// connect to the Event Hubs service.
18+
/// </summary>
19+
///
20+
[Test]
21+
public async Task ClientCanConnectUsingSharedAccessSignatureConnectionString()
22+
{
23+
await using (var scope = await ServiceBusScope.CreateWithQueue(enablePartitioning: true, enableSession: false))
24+
{
25+
var options = new ServiceBusClientOptions();
26+
var audience = ServiceBusConnection.BuildConnectionResource(options.TransportType, TestEnvironment.FullyQualifiedNamespace, scope.QueueName);
27+
var connectionString = TestEnvironment.BuildConnectionStringWithSharedAccessSignature(scope.QueueName, audience);
28+
29+
await using (var client = new ServiceBusClient(connectionString, options))
30+
{
31+
Assert.That(async () =>
32+
{
33+
ServiceBusReceiver receiver = null;
34+
35+
try
36+
{
37+
receiver = client.CreateReceiver(scope.QueueName);
38+
}
39+
finally
40+
{
41+
await (receiver?.DisposeAsync() ?? new ValueTask());
42+
}
43+
44+
}, Throws.Nothing);
45+
}
46+
}
47+
}
48+
1549
[Test]
1650
[TestCase(true)]
1751
[TestCase(false)]

sdk/servicebus/Azure.Messaging.ServiceBus/tests/Client/ServiceBusClientTests.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,26 @@ public void ConstructorDoesNotRequireEntityNameInConnectionString()
119119
[TestCase("SharedAccessKeyName=[value];SharedAccessKey=[value];EntityPath=[value]")]
120120
[TestCase("Endpoint=value.com;SharedAccessKey=[value];EntityPath=[value]")]
121121
[TestCase("Endpoint=value.com;SharedAccessKeyName=[value];EntityPath=[value]")]
122-
public void ConstructorValidatesConnectionString(string connectionString)
122+
[TestCase("HostName=value.azure-devices.net;SharedAccessKeyName=[value];SharedAccessKey=[value]")]
123+
[TestCase("HostName=value.azure-devices.net;SharedAccessKeyName=[value];SharedAccessKey=[value];EntityPath=[value]")]
124+
[TestCase("HostName=value.azure-devices.net;SharedAccessKeyName=[value];SharedAccessSignature=[sas];EntityPath=[value]")]
125+
[TestCase("HostName=value.azure-devices.net;SharedAccessKey=[value];SharedAccessSignature=[sas];EntityPath=[value]")]
126+
public void ConstructorValidatesConnectionStringForMissingInformation(string connectionString)
123127
{
124-
Assert.That(() => new ServiceBusClient(connectionString), Throws.InstanceOf<ArgumentException>());
128+
Assert.That(() => new ServiceBusClient(connectionString), Throws.ArgumentException.And.Message.StartsWith(Resources.MissingConnectionInformation));
129+
}
130+
131+
/// <summary>
132+
/// Verifies functionality of the <see cref="ServiceBusClient" />
133+
/// constructor.
134+
/// </summary>
135+
///
136+
[Test]
137+
138+
[TestCase("Endpoint=value.azure-devices.net;SharedAccessKeyName=[value];SharedAccessKey=[value];SharedAccessSignature=[sas]")]
139+
public void ConstructorValidatesConnectionStringForDuplicateAuthorization(string connectionString)
140+
{
141+
Assert.That(() => new ServiceBusClient(connectionString), Throws.ArgumentException.And.Message.StartsWith(Resources.OnlyOneSharedAccessAuthorizationMayBeSpecified));
125142
}
126143

127144
/// <summary>

sdk/servicebus/Azure.Messaging.ServiceBus/tests/Client/ConnectionStringParserTests.cs renamed to sdk/servicebus/Azure.Messaging.ServiceBus/tests/Core/ConnectionStringParserTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using Azure.Messaging.ServiceBus.Core;
77
using NUnit.Framework;
88

9-
namespace Azure.Messaging.ServiceBus.Tests.Client
9+
namespace Azure.Messaging.ServiceBus.Tests.Core
1010
{
1111
public class ConnectionStringParserTests
1212
{

0 commit comments

Comments
 (0)