Skip to content

Commit 32ad222

Browse files
feat: add support for specifying aad token audience (Azure#49379)
* feat: add support for specifying aad token audience * pr feedback * more feedback * handle case where audience has scope
1 parent ce3228e commit 32ad222

File tree

9 files changed

+190
-6
lines changed

9 files changed

+190
-6
lines changed

sdk/tables/Azure.Data.Tables/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
- Added support for specifying the token credential's Microsoft Entra audience when creating a client.
8+
79
### Breaking Changes
810

911
### Bugs Fixed

sdk/tables/Azure.Data.Tables/api/Azure.Data.Tables.net8.0.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ public partial interface ITableEntity
77
string RowKey { get; set; }
88
System.DateTimeOffset? Timestamp { get; set; }
99
}
10+
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
11+
public readonly partial struct TableAudience : System.IEquatable<Azure.Data.Tables.TableAudience>
12+
{
13+
private readonly object _dummy;
14+
private readonly int _dummyPrimitive;
15+
public TableAudience(string value) { throw null; }
16+
public static Azure.Data.Tables.TableAudience AzureChina { get { throw null; } }
17+
public static Azure.Data.Tables.TableAudience AzureGovernment { get { throw null; } }
18+
public static Azure.Data.Tables.TableAudience AzurePublicCloud { get { throw null; } }
19+
public bool Equals(Azure.Data.Tables.TableAudience other) { throw null; }
20+
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
21+
public override bool Equals(object obj) { throw null; }
22+
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
23+
public override int GetHashCode() { throw null; }
24+
public static bool operator ==(Azure.Data.Tables.TableAudience left, Azure.Data.Tables.TableAudience right) { throw null; }
25+
public static implicit operator Azure.Data.Tables.TableAudience (string value) { throw null; }
26+
public static bool operator !=(Azure.Data.Tables.TableAudience left, Azure.Data.Tables.TableAudience right) { throw null; }
27+
public override string ToString() { throw null; }
28+
}
1029
public partial class TableClient
1130
{
1231
protected TableClient() { }
@@ -60,6 +79,7 @@ public TableClient(System.Uri endpoint, string tableName, Azure.Data.Tables.Tabl
6079
public partial class TableClientOptions : Azure.Core.ClientOptions
6180
{
6281
public TableClientOptions(Azure.Data.Tables.TableClientOptions.ServiceVersion serviceVersion = Azure.Data.Tables.TableClientOptions.ServiceVersion.V2020_12_06) { }
82+
public Azure.Data.Tables.TableAudience? Audience { get { throw null; } set { } }
6383
public bool EnableTenantDiscovery { get { throw null; } set { } }
6484
public enum ServiceVersion
6585
{

sdk/tables/Azure.Data.Tables/api/Azure.Data.Tables.netstandard2.0.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ public partial interface ITableEntity
77
string RowKey { get; set; }
88
System.DateTimeOffset? Timestamp { get; set; }
99
}
10+
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
11+
public readonly partial struct TableAudience : System.IEquatable<Azure.Data.Tables.TableAudience>
12+
{
13+
private readonly object _dummy;
14+
private readonly int _dummyPrimitive;
15+
public TableAudience(string value) { throw null; }
16+
public static Azure.Data.Tables.TableAudience AzureChina { get { throw null; } }
17+
public static Azure.Data.Tables.TableAudience AzureGovernment { get { throw null; } }
18+
public static Azure.Data.Tables.TableAudience AzurePublicCloud { get { throw null; } }
19+
public bool Equals(Azure.Data.Tables.TableAudience other) { throw null; }
20+
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
21+
public override bool Equals(object obj) { throw null; }
22+
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
23+
public override int GetHashCode() { throw null; }
24+
public static bool operator ==(Azure.Data.Tables.TableAudience left, Azure.Data.Tables.TableAudience right) { throw null; }
25+
public static implicit operator Azure.Data.Tables.TableAudience (string value) { throw null; }
26+
public static bool operator !=(Azure.Data.Tables.TableAudience left, Azure.Data.Tables.TableAudience right) { throw null; }
27+
public override string ToString() { throw null; }
28+
}
1029
public partial class TableClient
1130
{
1231
protected TableClient() { }
@@ -60,6 +79,7 @@ public TableClient(System.Uri endpoint, string tableName, Azure.Data.Tables.Tabl
6079
public partial class TableClientOptions : Azure.Core.ClientOptions
6180
{
6281
public TableClientOptions(Azure.Data.Tables.TableClientOptions.ServiceVersion serviceVersion = Azure.Data.Tables.TableClientOptions.ServiceVersion.V2020_12_06) { }
82+
public Azure.Data.Tables.TableAudience? Audience { get { throw null; } set { } }
6383
public bool EnableTenantDiscovery { get { throw null; } set { } }
6484
public enum ServiceVersion
6585
{
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.ComponentModel;
6+
7+
namespace Azure.Data.Tables
8+
{
9+
/// <summary> Cloud audiences available for Azure Tables. </summary>
10+
public readonly struct TableAudience : IEquatable<TableAudience>
11+
{
12+
private readonly string _value;
13+
14+
private const string AzureChinaValue = "AzureChina";
15+
private const string AzureGovernmentValue = "AzureGov";
16+
private const string AzurePublicCloudValue = "AzurePublic";
17+
18+
private const string AzureStorageChinaValue = "https://storage.azure.cn";
19+
private const string AzureStorageGovernmentValue = "https://storage.azure.us";
20+
private const string AzureStoragePublicCloudValue = "https://storage.azure.com";
21+
private const string AzureCosmosChinaValue = "https://cosmos.azure.cn";
22+
private const string AzureCosmosGovernmentValue = "https://cosmos.azure.us";
23+
private const string AzureCosmosPublicCloudValue = "https://cosmos.azure.com";
24+
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="TableAudience"/> object.
27+
/// </summary>
28+
/// <param name="value">The Microsoft Entra audience to use when forming authorization scopes.
29+
/// For the Azure Tables service, this value corresponds to a URL that identifies the Azure cloud where the resource is located.</param>
30+
/// <exception cref="ArgumentNullException"> <paramref name="value"/> is null. </exception>
31+
/// <remarks>Please use one of the constant members over creating a custom value unless you have special needs for doing so.</remarks>
32+
public TableAudience(string value)
33+
{
34+
Argument.AssertNotNullOrEmpty(value, nameof(value));
35+
_value = value;
36+
}
37+
38+
/// <summary> The authorization audience used to authenticate with the Azure China cloud. </summary>
39+
public static TableAudience AzureChina { get; } = new TableAudience(AzureChinaValue);
40+
41+
/// <summary> The authorization audience used to authenticate with the Azure Government cloud. </summary>
42+
public static TableAudience AzureGovernment { get; } = new TableAudience(AzureGovernmentValue);
43+
44+
/// <summary> The authorization audience used to authenticate with the Azure Public cloud. </summary>
45+
public static TableAudience AzurePublicCloud { get; } = new TableAudience(AzurePublicCloudValue);
46+
47+
/// <summary> Determines if two <see cref="TableAudience"/> values are the same. </summary>
48+
public static bool operator ==(TableAudience left, TableAudience right) => left.Equals(right);
49+
/// <summary> Determines if two <see cref="TableAudience"/> values are not the same. </summary>
50+
public static bool operator !=(TableAudience left, TableAudience right) => !left.Equals(right);
51+
/// <summary> Converts a string to a <see cref="TableAudience"/>. </summary>
52+
public static implicit operator TableAudience(string value) => new TableAudience(value);
53+
54+
/// <inheritdoc />
55+
[EditorBrowsable(EditorBrowsableState.Never)]
56+
public override bool Equals(object obj) => obj is TableAudience other && Equals(other);
57+
/// <inheritdoc />
58+
public bool Equals(TableAudience other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase);
59+
60+
/// <inheritdoc />
61+
[EditorBrowsable(EditorBrowsableState.Never)]
62+
public override int GetHashCode() => _value?.GetHashCode() ?? 0;
63+
/// <inheritdoc />
64+
public override string ToString() => _value;
65+
66+
internal string GetDefaultScope(bool isCosmosEndpoint)
67+
{
68+
var audience = _value switch
69+
{
70+
AzureChinaValue when isCosmosEndpoint => AzureCosmosChinaValue,
71+
AzureGovernmentValue when isCosmosEndpoint => AzureCosmosGovernmentValue,
72+
AzurePublicCloudValue when isCosmosEndpoint => AzureCosmosPublicCloudValue,
73+
AzureChinaValue => AzureStorageChinaValue,
74+
AzureGovernmentValue => AzureStorageGovernmentValue,
75+
AzurePublicCloudValue => AzureStoragePublicCloudValue,
76+
_ => _value
77+
};
78+
79+
if (audience.EndsWith($"/{TableConstants.DefaultScope}", StringComparison.InvariantCultureIgnoreCase))
80+
{
81+
return audience;
82+
}
83+
84+
return audience.EndsWith("/", StringComparison.InvariantCultureIgnoreCase)
85+
? $"{audience}{TableConstants.DefaultScope}"
86+
: $"{audience}/{TableConstants.DefaultScope}";
87+
}
88+
}
89+
}

sdk/tables/Azure.Data.Tables/src/TableClient.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,10 +272,11 @@ public TableClient(Uri endpoint, string tableName, TokenCredential tokenCredenti
272272
options ??= TableClientOptions.DefaultOptions;
273273

274274
var perCallPolicies = _isCosmosEndpoint ? new[] { new CosmosPatchTransformPolicy() } : Array.Empty<HttpPipelinePolicy>();
275-
275+
var audienceScope = (options?.Audience ?? TableAudience.AzurePublicCloud)
276+
.GetDefaultScope(_isCosmosEndpoint);
276277
var pipelineOptions = new HttpPipelineOptions(options)
277278
{
278-
PerRetryPolicies = { new TableBearerTokenChallengeAuthorizationPolicy(tokenCredential, _isCosmosEndpoint ? TableConstants.CosmosScope : TableConstants.StorageScope, options.EnableTenantDiscovery) },
279+
PerRetryPolicies = { new TableBearerTokenChallengeAuthorizationPolicy(tokenCredential, audienceScope, options.EnableTenantDiscovery) },
279280
ResponseClassifier = new ResponseClassifier(),
280281
RequestFailedDetailsParser = new TablesRequestFailedDetailsParser()
281282
};

sdk/tables/Azure.Data.Tables/src/TableClientOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ public enum ServiceVersion
6161
#pragma warning restore CA1707 // Identifiers should not contain underscores
6262
}
6363

64+
/// <summary>
65+
/// Gets or sets the Audience to use for authentication with Microsoft Entra.
66+
/// The audience is not considered when using a shared key or connection string.
67+
/// </summary>
68+
public TableAudience? Audience { get; set; }
69+
6470
internal static TableClientOptions DefaultOptions => new()
6571
{
6672
Diagnostics =

sdk/tables/Azure.Data.Tables/src/TableConstants.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ internal static class TableConstants
77
{
88
internal const string LegacyCosmosTableDomain = ".table.cosmosdb.";
99
internal const string CosmosTableDomain = ".table.cosmos.";
10-
internal const string StorageScope = "https://storage.azure.com/.default";
11-
internal const string CosmosScope = "https://cosmos.azure.com/.default";
1210
internal const string ReturnNoContent = "return-no-content";
11+
internal const string DefaultScope = ".default";
1312

1413
internal static class CompatSwitches
1514
{

sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,11 @@ public TableServiceClient(Uri endpoint, TokenCredential tokenCredential, TableCl
273273
var perCallPolicies = _isCosmosEndpoint ? new[] { new CosmosPatchTransformPolicy() } : Array.Empty<HttpPipelinePolicy>();
274274
var endpointString = _endpoint.AbsoluteUri;
275275
string secondaryEndpoint = TableConnectionString.GetSecondaryUriFromPrimary(_endpoint)?.AbsoluteUri;
276-
276+
var audienceScope = (options?.Audience ?? TableAudience.AzurePublicCloud)
277+
.GetDefaultScope(_isCosmosEndpoint);
277278
var pipelineOptions = new HttpPipelineOptions(options)
278279
{
279-
PerRetryPolicies = { new TableBearerTokenChallengeAuthorizationPolicy(tokenCredential, _isCosmosEndpoint ? TableConstants.CosmosScope : TableConstants.StorageScope, options.EnableTenantDiscovery) },
280+
PerRetryPolicies = { new TableBearerTokenChallengeAuthorizationPolicy(tokenCredential, audienceScope, options.EnableTenantDiscovery) },
280281
ResponseClassifier = new ResponseClassifier(),
281282
RequestFailedDetailsParser = new TablesRequestFailedDetailsParser()
282283
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using NUnit.Framework;
5+
using System.Collections.Generic;
6+
7+
namespace Azure.Data.Tables.Tests
8+
{
9+
public class TableAudienceTests
10+
{
11+
// This test validates that the token audience scope is correctly parsed
12+
[TestCaseSource(nameof(GetDefaultScopeTestCases))]
13+
public void TestGetDefaultScope(
14+
TableAudience? audience,
15+
bool isCosmosEndpoint,
16+
string expectedScope)
17+
{
18+
audience ??= TableAudience.AzurePublicCloud;
19+
var defaultScope = audience.Value.GetDefaultScope(isCosmosEndpoint);
20+
Assert.AreEqual(expectedScope, defaultScope);
21+
}
22+
23+
public static IEnumerable<TestCaseData> GetDefaultScopeTestCases
24+
{
25+
get
26+
{
27+
// cosmos audience
28+
yield return new TestCaseData(TableAudience.AzurePublicCloud, true, "https://cosmos.azure.com/.default");
29+
yield return new TestCaseData(TableAudience.AzureChina, true, "https://cosmos.azure.cn/.default");
30+
yield return new TestCaseData(TableAudience.AzureGovernment, true, "https://cosmos.azure.us/.default");
31+
// storage audience
32+
yield return new TestCaseData(TableAudience.AzurePublicCloud, false, "https://storage.azure.com/.default");
33+
yield return new TestCaseData(TableAudience.AzureChina, false, "https://storage.azure.cn/.default");
34+
yield return new TestCaseData(TableAudience.AzureGovernment, false, "https://storage.azure.us/.default");
35+
// default audience with cosmos endpoint should be public cloud
36+
yield return new TestCaseData(null, true, "https://cosmos.azure.com/.default");
37+
// default audience with storage endpoint should be public cloud
38+
yield return new TestCaseData(null, false, "https://storage.azure.com/.default");
39+
// custom audience
40+
yield return new TestCaseData(new TableAudience("my.custom.audience"), false, "my.custom.audience/.default");
41+
yield return new TestCaseData(new TableAudience("my.custom.audience/"), false, "my.custom.audience/.default");
42+
yield return new TestCaseData(new TableAudience("my.custom.audience/.default"), false, "my.custom.audience/.default");
43+
}
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)