Skip to content

Commit 1703b49

Browse files
pr comments
1 parent 479ab37 commit 1703b49

19 files changed

+272
-155
lines changed

src/client/Microsoft.Identity.Client/ApiConfig/AcquireTokenForClientParameterBuilder.cs

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -97,35 +97,19 @@ public AcquireTokenForClientParameterBuilder WithSendX5C(bool withSendX5C)
9797
/// <returns>The current instance of <see cref="AcquireTokenForClientParameterBuilder"/> to enable method chaining.</returns>
9898
public AcquireTokenForClientParameterBuilder WithMtlsProofOfPossession()
9999
{
100-
if (ServiceBundle.Config.ClientCredential is CertificateClientCredential certCred)
100+
if (ServiceBundle.Config.ClientCredential is CertificateClientCredential certificateCredential)
101101
{
102-
CommonParameters.AuthenticationOperation =
103-
new MtlsPopAuthenticationOperation(certCred.Certificate);
104-
CommonParameters.MtlsCertificate = certCred.Certificate;
105-
}
106-
else if (ServiceBundle.Config.ClientCredential is ClientAssertionDelegateCredential assertCred)
107-
{
108-
X509Certificate2 cert = assertCred.PeekCertificate(ServiceBundle.Config.ClientId);
109-
110-
if (cert == null)
102+
if (certificateCredential.Certificate == null)
111103
{
112-
// Delegate did not supply a certificate ➜ cannot proceed with mTLS‑PoP
113104
throw new MsalClientException(
114-
MsalError.MtlsCertificateNotProvided,
115-
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
116-
}
117-
118-
CommonParameters.AuthenticationOperation =
119-
new MtlsPopAuthenticationOperation(cert);
120-
CommonParameters.MtlsCertificate = cert;
121-
}
122-
else
123-
{
124-
throw new MsalClientException(
125105
MsalError.MtlsCertificateNotProvided,
126106
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
127-
}
107+
}
128108

109+
CommonParameters.AuthenticationOperation = new MtlsPopAuthenticationOperation(certificateCredential.Certificate);
110+
CommonParameters.MtlsCertificate = certificateCredential.Certificate;
111+
}
112+
CommonParameters.IsPopEnabled = true;
129113
return this;
130114
}
131115

src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Security.Cryptography.X509Certificates;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Microsoft.Identity.Client.ApiConfig.Parameters;
89
using Microsoft.Identity.Client.AuthScheme.PoP;
910
using Microsoft.Identity.Client.Instance.Discovery;
11+
using Microsoft.Identity.Client.Instance.Validation;
1012
using Microsoft.Identity.Client.Internal;
13+
using Microsoft.Identity.Client.Internal.ClientCredential;
1114
using Microsoft.Identity.Client.Internal.Requests;
1215
using Microsoft.Identity.Client.ManagedIdentity;
1316
using Microsoft.Identity.Client.Utils;
@@ -55,6 +58,9 @@ public async Task<AuthenticationResult> ExecuteAsync(
5558
AcquireTokenForClientParameters clientParameters,
5659
CancellationToken cancellationToken)
5760
{
61+
await PopBindingResolver.ValidateAndWireAsync(ServiceBundle, commonParameters,cancellationToken)
62+
.ConfigureAwait(false);
63+
5864
RequestContext requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken);
5965

6066
AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync(

src/client/Microsoft.Identity.Client/ApiConfig/Parameters/AcquireTokenCommonParameters.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ internal class AcquireTokenCommonParameters
3333
public SortedList<string, string> CacheKeyComponents { get; internal set; }
3434
public string FmiPathSuffix { get; internal set; }
3535
public string ClientAssertionFmiPath { get; internal set; }
36+
public bool IsPopEnabled { get; set; }
3637
}
3738
}

src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -287,21 +287,16 @@ public ConfidentialClientApplicationBuilder WithClientAssertion(Func<AssertionRe
287287
/// <remarks>This method allows the client application to authenticate using a custom client
288288
/// assertion, which can be useful in scenarios where the assertion needs to be dynamically generated or
289289
/// retrieved.</remarks>
290-
/// <param name="boundAssertionAsync">A delegate that asynchronously provides an <see cref="AssertionResponse"/> based on the given <see
290+
/// <param name="clientAssertionProvider">A delegate that asynchronously provides an <see cref="AssertionResponse"/> based on the given <see
291291
/// cref="AssertionRequestOptions"/> and <see cref="CancellationToken"/>. This delegate must not be <see
292292
/// langword="null"/>.</param>
293293
/// <returns>The <see cref="ConfidentialClientApplicationBuilder"/> instance configured with the specified client
294294
/// assertion.</returns>
295-
/// <exception cref="ArgumentNullException">Thrown if <paramref name="boundAssertionAsync"/> is <see langword="null"/>.</exception>
295+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="clientAssertionProvider"/> is <see langword="null"/>.</exception>
296296
public ConfidentialClientApplicationBuilder WithClientAssertion(Func<AssertionRequestOptions,
297-
CancellationToken, Task<AssertionResponse>> boundAssertionAsync)
297+
CancellationToken, Task<AssertionResponse>> clientAssertionProvider)
298298
{
299-
if (boundAssertionAsync == null)
300-
{
301-
throw new ArgumentNullException(nameof(boundAssertionAsync));
302-
}
303-
304-
Config.ClientCredential = new ClientAssertionDelegateCredential(boundAssertionAsync);
299+
Config.ClientCredential = new ClientAssertionDelegateCredential(clientAssertionProvider);
305300
return this;
306301
}
307302

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Security.Cryptography.X509Certificates;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.Identity.Client.ApiConfig.Parameters;
10+
using Microsoft.Identity.Client.AuthScheme.PoP;
11+
using Microsoft.Identity.Client.Internal;
12+
using Microsoft.Identity.Client.Internal.ClientCredential;
13+
using Microsoft.Identity.Client.Internal.Requests;
14+
15+
namespace Microsoft.Identity.Client.AuthScheme.PoP
16+
{
17+
/// <summary>
18+
/// Central place for validating mTLS Proof‑of‑Possession pre‑conditions
19+
/// and wiring up <see cref="MtlsPopAuthenticationOperation"/>.
20+
/// </summary>
21+
internal static class PopBindingResolver
22+
{
23+
/// <summary>
24+
/// Ensures a certificate is available, region settings are correct,
25+
/// and populates <see cref="AcquireTokenCommonParameters.AuthenticationOperation"/>
26+
/// and <see cref="AcquireTokenCommonParameters.MtlsCertificate"/>.
27+
/// </summary>
28+
internal static async Task ValidateAndWireAsync(IServiceBundle serviceBundle,
29+
AcquireTokenCommonParameters commonParameters,
30+
CancellationToken ct)
31+
{
32+
if (!commonParameters.IsPopEnabled)
33+
{
34+
return; // PoP not requested
35+
}
36+
37+
// ────────────────────────────────────
38+
// Case 1 – Certificate credential
39+
// ────────────────────────────────────
40+
if (serviceBundle.Config.ClientCredential is CertificateClientCredential certCred)
41+
{
42+
if (certCred.Certificate == null)
43+
{
44+
throw new MsalClientException(
45+
MsalError.MtlsCertificateNotProvided,
46+
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
47+
}
48+
49+
return;
50+
}
51+
52+
// ────────────────────────────────────
53+
// Case 2 – Client‑assertion delegate
54+
// ────────────────────────────────────
55+
if (serviceBundle.Config.ClientCredential is ClientAssertionDelegateCredential cadc)
56+
{
57+
var opts = new AssertionRequestOptions
58+
{
59+
ClientID = serviceBundle.Config.ClientId,
60+
ClientCapabilities = serviceBundle.Config.ClientCapabilities,
61+
Claims = commonParameters.Claims,
62+
CancellationToken = ct
63+
};
64+
65+
AssertionResponse ar = await cadc.GetAssertionAsync(opts, ct).ConfigureAwait(false);
66+
67+
if (ar.TokenBindingCertificate == null)
68+
{
69+
throw new MsalClientException(
70+
MsalError.MtlsCertificateNotProvided,
71+
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
72+
}
73+
74+
Wire(commonParameters, ar.TokenBindingCertificate, serviceBundle);
75+
return;
76+
}
77+
78+
// ────────────────────────────────────
79+
// Case 3 – Any other credential (client‑secret etc.)
80+
// ────────────────────────────────────
81+
throw new MsalClientException(
82+
MsalError.MtlsCertificateNotProvided,
83+
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
84+
}
85+
86+
/// <summary>
87+
/// Common wiring + region check.
88+
/// </summary>
89+
private static void Wire(
90+
AcquireTokenCommonParameters commonParameters,
91+
X509Certificate2 cert,
92+
IServiceBundle serviceBundle)
93+
{
94+
// Region requirement (AAD only)
95+
if (serviceBundle.Config.Authority.AuthorityInfo.AuthorityType == AuthorityType.Aad &&
96+
serviceBundle.Config.AzureRegion == null)
97+
{
98+
throw new MsalClientException(
99+
MsalError.MtlsPopWithoutRegion,
100+
MsalErrorMessage.MtlsPopWithoutRegion);
101+
}
102+
103+
commonParameters.AuthenticationOperation = new MtlsPopAuthenticationOperation(cert);
104+
commonParameters.MtlsCertificate = cert;
105+
106+
commonParameters.CacheKeyComponents ??= new SortedList<string, string>(StringComparer.Ordinal);
107+
108+
commonParameters.CacheKeyComponents[Constants.CertSerialNumber] = cert.SerialNumber;
109+
110+
serviceBundle.Config.CertificateIdToAssociateWithToken = cert.Thumbprint;
111+
}
112+
}
113+
}

src/client/Microsoft.Identity.Client/Internal/ClientCredential/ClientAssertionDelegateCredential.cs

Lines changed: 42 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
using System.Security.Cryptography.X509Certificates;
66
using System.Threading;
77
using System.Threading.Tasks;
8+
using Microsoft.Identity.Client;
9+
using Microsoft.Identity.Client.AuthScheme.PoP;
10+
using Microsoft.Identity.Client.Core;
811
using Microsoft.Identity.Client.Internal.Requests;
912
using Microsoft.Identity.Client.OAuth2;
1013
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
@@ -13,94 +16,78 @@
1316
namespace Microsoft.Identity.Client.Internal.ClientCredential
1417
{
1518
/// <summary>
16-
/// Handles client assertions supplied via a delegate that returns
17-
/// <see cref="AssertionResponse"/> (JWT + optional certificate).
19+
/// Handles client assertions supplied via a delegate that returns an
20+
/// <see cref="AssertionResponse"/> (JWT + optional certificate bound for mTLS‑PoP).
1821
/// </summary>
1922
internal sealed class ClientAssertionDelegateCredential : IClientCredential
2023
{
21-
private readonly Func<AssertionRequestOptions, CancellationToken, Task<AssertionResponse>> _assertionDelegate;
24+
private readonly Func<AssertionRequestOptions, CancellationToken, Task<AssertionResponse>> _provider;
25+
26+
internal Task<AssertionResponse> GetAssertionAsync(
27+
AssertionRequestOptions options,
28+
CancellationToken cancellationToken) =>
29+
_provider(options, cancellationToken);
2230

2331
public ClientAssertionDelegateCredential(
24-
Func<AssertionRequestOptions, CancellationToken, Task<AssertionResponse>> assertionDelegate)
32+
Func<AssertionRequestOptions, CancellationToken, Task<AssertionResponse>> provider)
2533
{
26-
_assertionDelegate = assertionDelegate ?? throw new ArgumentNullException(nameof(assertionDelegate));
34+
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
2735
}
2836

2937
public AssertionType AssertionType => AssertionType.ClientAssertion;
3038

31-
private X509Certificate2 _cachedCertificate;
32-
33-
internal X509Certificate2 PeekCertificate(string clientId)
34-
{
35-
// Return the cached value if we already probed once
36-
if (_cachedCertificate != null)
37-
{
38-
return _cachedCertificate;
39-
}
40-
41-
try
42-
{
43-
var probeOpts = new AssertionRequestOptions
44-
{
45-
ClientID = clientId,
46-
};
47-
48-
var resp = _assertionDelegate(probeOpts, CancellationToken.None)
49-
.ConfigureAwait(false)
50-
.GetAwaiter()
51-
.GetResult();
52-
53-
_cachedCertificate = resp?.TokenBindingCertificate;
54-
}
55-
catch
56-
{
57-
}
58-
59-
return _cachedCertificate;
60-
}
39+
// ──────────────────────────────────
40+
// Expose the certificate we used in the *last* call
41+
// ──────────────────────────────────
42+
private X509Certificate2 _lastCertificate;
43+
internal X509Certificate2 LastCertificate => _lastCertificate;
6144

45+
// ──────────────────────────────────
46+
// Main hook for token requests
47+
// ──────────────────────────────────
6248
public async Task AddConfidentialClientParametersAsync(
6349
OAuth2Client oAuth2Client,
64-
AuthenticationRequestParameters requestParameters,
65-
ICryptographyManager cryptographyManager,
50+
AuthenticationRequestParameters p,
51+
ICryptographyManager _,
6652
string tokenEndpoint,
67-
CancellationToken cancellationToken)
53+
CancellationToken ct)
6854
{
69-
// Build the same AssertionRequestOptions old code produced
7055
var opts = new AssertionRequestOptions
7156
{
72-
CancellationToken = cancellationToken,
73-
ClientID = requestParameters.AppConfig.ClientId,
57+
CancellationToken = ct,
58+
ClientID = p.AppConfig.ClientId,
7459
TokenEndpoint = tokenEndpoint,
75-
ClientCapabilities = requestParameters.RequestContext.ServiceBundle.Config.ClientCapabilities,
76-
Claims = requestParameters.Claims,
77-
ClientAssertionFmiPath = requestParameters.ClientAssertionFmiPath
60+
ClientCapabilities = p.RequestContext.ServiceBundle.Config.ClientCapabilities,
61+
Claims = p.Claims,
62+
ClientAssertionFmiPath = p.ClientAssertionFmiPath
7863
};
7964

80-
// Execute delegate
81-
AssertionResponse resp = await _assertionDelegate(opts, cancellationToken)
82-
.ConfigureAwait(false);
65+
AssertionResponse resp = await _provider(opts, ct).ConfigureAwait(false);
8366

84-
// Empty JWT is not allowed
85-
if (string.IsNullOrWhiteSpace(resp.Assertion))
67+
if (string.IsNullOrWhiteSpace(resp?.Assertion))
8668
{
87-
throw new ArgumentException(
88-
"The assertion delegate returned an empty JWT.",
89-
nameof(_assertionDelegate));
69+
throw new MsalClientException(MsalError.InvalidClientAssertion,
70+
MsalErrorMessage.InvalidClientAssertionEmpty);
9071
}
9172

92-
// Set assertion type
93-
if (resp.TokenBindingCertificate != null)
73+
// Decide bearer vs PoP
74+
bool popEnabled = p.IsPopEnabled;
75+
76+
if (popEnabled && resp.TokenBindingCertificate != null)
9477
{
9578
oAuth2Client.AddBodyParameter(
9679
OAuth2Parameter.ClientAssertionType,
97-
OAuth2AssertionType.JwtPop);
80+
OAuth2AssertionType.JwtPop /* constant added in OAuth2AssertionType */);
81+
82+
_lastCertificate = resp.TokenBindingCertificate;
9883
}
9984
else
10085
{
10186
oAuth2Client.AddBodyParameter(
10287
OAuth2Parameter.ClientAssertionType,
10388
OAuth2AssertionType.JwtBearer);
89+
90+
_lastCertificate = null;
10491
}
10592

10693
oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, resp.Assertion);

src/client/Microsoft.Identity.Client/Internal/Requests/AuthenticationRequestParameters.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ public AuthenticationRequestParameters(
111111

112112
public X509Certificate2 MtlsCertificate => _commonParameters.MtlsCertificate;
113113

114+
public bool IsPopEnabled => _commonParameters.IsPopEnabled;
115+
114116
/// <summary>
115117
/// Indicates if the user configured claims via .WithClaims. Not affected by Client Capabilities
116118
/// </summary>

0 commit comments

Comments
 (0)