Skip to content

Commit 5759809

Browse files
authored
Handle client certificate rotation for token binding (#53916)
1 parent 203dab4 commit 5759809

19 files changed

+909
-61
lines changed

sdk/core/Azure.Core.TestFramework/src/Azure.Core.TestFramework.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
<IncludeOperationsSharedSource>true</IncludeOperationsSharedSource>
55
</PropertyGroup>
66
<ItemGroup>
7-
<PackageReference Include="Azure.Core" />
7+
<!-- https://github.com/Azure/azure-sdk-for-net/issues/53975 -->
8+
<!-- <PackageReference Include="Azure.Core" /> -->
9+
<ProjectReference Include="..\..\Azure.Core\src\Azure.Core.csproj" />
810
<PackageReference Include="Azure.Identity" />
911
<PackageReference Include="Newtonsoft.Json" />
1012
<PackageReference Include="NUnit" />

sdk/core/Azure.Core.TestFramework/src/MockTransport.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public class MockTransport : HttpPipelineTransport
2121

2222
public bool? ExpectSyncPipeline { get; set; }
2323

24+
public List<HttpPipelineTransportOptions> TransportUpdates { get; } = [];
25+
2426
public MockTransport()
2527
{
2628
RequestGate = new AsyncGate<MockRequest, MockResponse>();
@@ -38,7 +40,7 @@ public MockTransport(params MockResponse[] responses)
3840
};
3941
}
4042

41-
public MockTransport(Func<MockRequest, MockResponse> responseFactory): this(req => responseFactory((MockRequest)req.Request))
43+
public MockTransport(Func<MockRequest, MockResponse> responseFactory) : this(req => responseFactory((MockRequest)req.Request))
4244
{
4345
}
4446

@@ -72,6 +74,11 @@ public override async ValueTask ProcessAsync(HttpMessage message)
7274
await ProcessCore(message);
7375
}
7476

77+
public override void Update(HttpPipelineTransportOptions options)
78+
{
79+
TransportUpdates.Add(options);
80+
}
81+
7582
private async Task ProcessCore(HttpMessage message)
7683
{
7784
if (!(message.Request is MockRequest request))

sdk/core/Azure.Core/api/Azure.Core.net462.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ public partial struct AccessToken
306306
public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; }
307307
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; }
308308
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; }
309+
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; }
310+
public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } }
309311
public System.DateTimeOffset ExpiresOn { get { throw null; } }
310312
public System.DateTimeOffset? RefreshOn { get { throw null; } }
311313
public string Token { get { throw null; } }
@@ -1042,12 +1044,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans
10421044
{
10431045
public static readonly Azure.Core.Pipeline.HttpClientTransport Shared;
10441046
public HttpClientTransport() { }
1047+
public HttpClientTransport(System.Func<Azure.Core.Pipeline.HttpPipelineTransportOptions, System.Net.Http.HttpClient> clientFactory) { }
1048+
public HttpClientTransport(System.Func<Azure.Core.Pipeline.HttpPipelineTransportOptions, System.Net.Http.HttpMessageHandler> handlerFactory) { }
10451049
public HttpClientTransport(System.Net.Http.HttpClient client) { }
10461050
public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { }
10471051
public sealed override Azure.Core.Request CreateRequest() { throw null; }
10481052
public void Dispose() { }
10491053
public override void Process(Azure.Core.HttpMessage message) { }
10501054
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
1055+
public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
10511056
}
10521057
public partial class HttpPipeline
10531058
{
@@ -1103,6 +1108,7 @@ protected HttpPipelineTransport() { }
11031108
public abstract Azure.Core.Request CreateRequest();
11041109
public abstract void Process(Azure.Core.HttpMessage message);
11051110
public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message);
1111+
public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
11061112
}
11071113
public partial class HttpPipelineTransportOptions
11081114
{

sdk/core/Azure.Core/api/Azure.Core.net472.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ public partial struct AccessToken
306306
public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; }
307307
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; }
308308
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; }
309+
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; }
310+
public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } }
309311
public System.DateTimeOffset ExpiresOn { get { throw null; } }
310312
public System.DateTimeOffset? RefreshOn { get { throw null; } }
311313
public string Token { get { throw null; } }
@@ -1042,12 +1044,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans
10421044
{
10431045
public static readonly Azure.Core.Pipeline.HttpClientTransport Shared;
10441046
public HttpClientTransport() { }
1047+
public HttpClientTransport(System.Func<Azure.Core.Pipeline.HttpPipelineTransportOptions, System.Net.Http.HttpClient> clientFactory) { }
1048+
public HttpClientTransport(System.Func<Azure.Core.Pipeline.HttpPipelineTransportOptions, System.Net.Http.HttpMessageHandler> handlerFactory) { }
10451049
public HttpClientTransport(System.Net.Http.HttpClient client) { }
10461050
public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { }
10471051
public sealed override Azure.Core.Request CreateRequest() { throw null; }
10481052
public void Dispose() { }
10491053
public override void Process(Azure.Core.HttpMessage message) { }
10501054
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
1055+
public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
10511056
}
10521057
public partial class HttpPipeline
10531058
{
@@ -1103,6 +1108,7 @@ protected HttpPipelineTransport() { }
11031108
public abstract Azure.Core.Request CreateRequest();
11041109
public abstract void Process(Azure.Core.HttpMessage message);
11051110
public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message);
1111+
public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
11061112
}
11071113
public partial class HttpPipelineTransportOptions
11081114
{

sdk/core/Azure.Core/api/Azure.Core.net8.0.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ public partial struct AccessToken
315315
public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; }
316316
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; }
317317
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; }
318+
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; }
319+
public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } }
318320
public System.DateTimeOffset ExpiresOn { get { throw null; } }
319321
public System.DateTimeOffset? RefreshOn { get { throw null; } }
320322
public string Token { get { throw null; } }
@@ -1055,12 +1057,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans
10551057
{
10561058
public static readonly Azure.Core.Pipeline.HttpClientTransport Shared;
10571059
public HttpClientTransport() { }
1060+
public HttpClientTransport(System.Func<Azure.Core.Pipeline.HttpPipelineTransportOptions, System.Net.Http.HttpClient> clientFactory) { }
1061+
public HttpClientTransport(System.Func<Azure.Core.Pipeline.HttpPipelineTransportOptions, System.Net.Http.HttpMessageHandler> handlerFactory) { }
10581062
public HttpClientTransport(System.Net.Http.HttpClient client) { }
10591063
public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { }
10601064
public sealed override Azure.Core.Request CreateRequest() { throw null; }
10611065
public void Dispose() { }
10621066
public override void Process(Azure.Core.HttpMessage message) { }
10631067
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
1068+
public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
10641069
}
10651070
public partial class HttpPipeline
10661071
{
@@ -1117,6 +1122,7 @@ protected HttpPipelineTransport() { }
11171122
public abstract Azure.Core.Request CreateRequest();
11181123
public abstract void Process(Azure.Core.HttpMessage message);
11191124
public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message);
1125+
public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
11201126
}
11211127
public partial class HttpPipelineTransportOptions
11221128
{

sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ public partial struct AccessToken
306306
public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; }
307307
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; }
308308
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; }
309+
public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; }
310+
public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } }
309311
public System.DateTimeOffset ExpiresOn { get { throw null; } }
310312
public System.DateTimeOffset? RefreshOn { get { throw null; } }
311313
public string Token { get { throw null; } }
@@ -1042,12 +1044,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans
10421044
{
10431045
public static readonly Azure.Core.Pipeline.HttpClientTransport Shared;
10441046
public HttpClientTransport() { }
1047+
public HttpClientTransport(System.Func<Azure.Core.Pipeline.HttpPipelineTransportOptions, System.Net.Http.HttpClient> clientFactory) { }
1048+
public HttpClientTransport(System.Func<Azure.Core.Pipeline.HttpPipelineTransportOptions, System.Net.Http.HttpMessageHandler> handlerFactory) { }
10451049
public HttpClientTransport(System.Net.Http.HttpClient client) { }
10461050
public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { }
10471051
public sealed override Azure.Core.Request CreateRequest() { throw null; }
10481052
public void Dispose() { }
10491053
public override void Process(Azure.Core.HttpMessage message) { }
10501054
public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; }
1055+
public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
10511056
}
10521057
public partial class HttpPipeline
10531058
{
@@ -1103,6 +1108,7 @@ protected HttpPipelineTransport() { }
11031108
public abstract Azure.Core.Request CreateRequest();
11041109
public abstract void Process(Azure.Core.HttpMessage message);
11051110
public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message);
1111+
public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { }
11061112
}
11071113
public partial class HttpPipelineTransportOptions
11081114
{

sdk/core/Azure.Core/src/AccessToken.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.ClientModel.Primitives;
6+
using System.Security.Cryptography.X509Certificates;
67

78
namespace Azure.Core
89
{
@@ -52,6 +53,23 @@ public AccessToken(string accessToken, DateTimeOffset expiresOn, DateTimeOffset?
5253
TokenType = tokenType;
5354
}
5455

56+
/// <summary>
57+
/// Creates a new instance of <see cref="AccessToken"/> using the provided <paramref name="accessToken"/> and <paramref name="expiresOn"/>.
58+
/// </summary>
59+
/// <param name="accessToken">The access token value.</param>
60+
/// <param name="expiresOn">The access token expiry date.</param>
61+
/// <param name="refreshOn">Specifies the time when the cached token should be proactively refreshed.</param>
62+
/// <param name="tokenType">The access token type.</param>
63+
/// <param name="bindingCertificate">The binding certificate for the access token.</param>
64+
public AccessToken(string accessToken, DateTimeOffset expiresOn, DateTimeOffset? refreshOn, string tokenType, X509Certificate2 bindingCertificate)
65+
{
66+
Token = accessToken;
67+
ExpiresOn = expiresOn;
68+
RefreshOn = refreshOn;
69+
TokenType = tokenType;
70+
BindingCertificate = bindingCertificate;
71+
}
72+
5573
/// <summary>
5674
/// Get the access token value.
5775
/// </summary>
@@ -72,6 +90,13 @@ public AccessToken(string accessToken, DateTimeOffset expiresOn, DateTimeOffset?
7290
/// </summary>
7391
public string TokenType { get; }
7492

93+
/// <summary>
94+
/// Gets or sets the binding certificate for the access token.
95+
/// This is used when authenticating via Proof of Possession (PoP).
96+
/// </summary>
97+
/// <seealso href="https://learn.microsoft.com/entra/msal/dotnet/advanced/proof-of-possession-tokens"/>
98+
public X509Certificate2? BindingCertificate { get; }
99+
75100
/// <inheritdoc />
76101
public override bool Equals(object? obj)
77102
{

sdk/core/Azure.Core/src/Diagnostics/AzureCoreEventSource.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ internal sealed class AzureCoreEventSource : AzureEventSource
3636
private const int RequestRedirectCountExceededEvent = 22;
3737
private const int PipelineTransportOptionsNotAppliedEvent = 23;
3838
private const int FailedToDecodeCaeChallengeClaimsEvent = 24;
39+
private const int FailedToUpdateTransportEvent = 25;
3940

4041
private AzureCoreEventSource() : base(EventSourceName) { }
4142

@@ -333,5 +334,14 @@ public void FailedToDecodeCaeChallengeClaims(string? encodedClaims, string excep
333334
{
334335
WriteEvent(FailedToDecodeCaeChallengeClaimsEvent, encodedClaims, exception);
335336
}
337+
338+
[Event(FailedToUpdateTransportEvent, Level = EventLevel.Error, Message = "Failed to update transport: {0}")]
339+
public void FailedToUpdateTransport(string reason)
340+
{
341+
if (IsEnabled(EventLevel.Error, EventKeywords.None))
342+
{
343+
WriteEvent(FailedToUpdateTransportEvent, reason);
344+
}
345+
}
336346
}
337347
}

0 commit comments

Comments
 (0)