Skip to content

Commit f120fdc

Browse files
Enable mTLS Proof‑of‑Possession for Client‑Assertion Delegates (#5409)
* initial * pr comments * build break for public api change * IsMtlsPopEnabled * pr comments * name change --------- Co-authored-by: Gladwin Johnson <[email protected]>
1 parent e813211 commit f120fdc

23 files changed

+840
-153
lines changed

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

44
using System;
@@ -15,6 +15,7 @@
1515
using Microsoft.Identity.Client.Utils;
1616
using Microsoft.Identity.Client.Extensibility;
1717
using Microsoft.Identity.Client.OAuth2;
18+
using System.Security.Cryptography.X509Certificates;
1819
using System.Security.Cryptography;
1920
using System.Text;
2021

@@ -98,18 +99,20 @@ public AcquireTokenForClientParameterBuilder WithSendX5C(bool withSendX5C)
9899
/// <returns>The current instance of <see cref="AcquireTokenForClientParameterBuilder"/> to enable method chaining.</returns>
99100
public AcquireTokenForClientParameterBuilder WithMtlsProofOfPossession()
100101
{
101-
if (ServiceBundle.Config.ClientCredential is not CertificateClientCredential certificateCredential)
102+
if (ServiceBundle.Config.ClientCredential is CertificateClientCredential certificateCredential)
102103
{
103-
throw new MsalClientException(
104+
if (certificateCredential.Certificate == null)
105+
{
106+
throw new MsalClientException(
104107
MsalError.MtlsCertificateNotProvided,
105108
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
106-
}
107-
else
108-
{
109+
}
110+
109111
CommonParameters.AuthenticationOperation = new MtlsPopAuthenticationOperation(certificateCredential.Certificate);
110-
CommonParameters.MtlsCertificate = certificateCredential.Certificate;
112+
CommonParameters.MtlsCertificate = certificateCredential.Certificate;
111113
}
112114

115+
CommonParameters.IsMtlsPopRequested = true;
113116
return this;
114117
}
115118

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;
@@ -56,6 +59,9 @@ public async Task<AuthenticationResult> ExecuteAsync(
5659
AcquireTokenForClientParameters clientParameters,
5760
CancellationToken cancellationToken)
5861
{
62+
await commonParameters.InitMtlsPopParametersAsync(ServiceBundle, cancellationToken)
63+
.ConfigureAwait(false);
64+
5965
RequestContext requestContext = CreateRequestContextAndLogVersionInfo(commonParameters.CorrelationId, commonParameters.MtlsCertificate, cancellationToken);
6066

6167
AuthenticationRequestParameters requestParams = await _confidentialClientApplication.CreateRequestParametersAsync(

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99
using Microsoft.Identity.Client.AppConfig;
1010
using Microsoft.Identity.Client.AuthScheme;
1111
using Microsoft.Identity.Client.AuthScheme.Bearer;
12+
using Microsoft.Identity.Client.AuthScheme.PoP;
1213
using Microsoft.Identity.Client.Extensibility;
14+
using Microsoft.Identity.Client.Internal;
15+
using Microsoft.Identity.Client.Internal.ClientCredential;
1316
using Microsoft.Identity.Client.TelemetryCore.Internal.Events;
17+
using Microsoft.Identity.Client.Utils;
1418
using static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquireTokenParameterBuilderExtension;
1519

1620
namespace Microsoft.Identity.Client.ApiConfig.Parameters
@@ -34,5 +38,75 @@ internal class AcquireTokenCommonParameters
3438
public SortedList<string, Func<CancellationToken, Task<string>>> CacheKeyComponents { get; internal set; }
3539
public string FmiPathSuffix { get; internal set; }
3640
public string ClientAssertionFmiPath { get; internal set; }
41+
public bool IsMtlsPopRequested { get; set; }
42+
43+
internal async Task InitMtlsPopParametersAsync(IServiceBundle serviceBundle, CancellationToken ct)
44+
{
45+
if (!IsMtlsPopRequested)
46+
{
47+
return; // PoP not requested
48+
}
49+
50+
// ────────────────────────────────────
51+
// Case 1 – Certificate credential
52+
// ────────────────────────────────────
53+
if (serviceBundle.Config.ClientCredential is CertificateClientCredential certCred)
54+
{
55+
if (certCred.Certificate == null)
56+
{
57+
throw new MsalClientException(
58+
MsalError.MtlsCertificateNotProvided,
59+
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
60+
}
61+
62+
return;
63+
}
64+
65+
// ────────────────────────────────────
66+
// Case 2 – Client‑assertion delegate
67+
// ────────────────────────────────────
68+
if (serviceBundle.Config.ClientCredential is ClientAssertionDelegateCredential cadc)
69+
{
70+
var opts = new AssertionRequestOptions
71+
{
72+
ClientID = serviceBundle.Config.ClientId,
73+
ClientCapabilities = serviceBundle.Config.ClientCapabilities,
74+
Claims = Claims,
75+
CancellationToken = ct
76+
};
77+
78+
ClientAssertion ar = await cadc.GetAssertionAsync(opts, ct).ConfigureAwait(false);
79+
80+
if (ar.TokenBindingCertificate == null)
81+
{
82+
throw new MsalClientException(
83+
MsalError.MtlsCertificateNotProvided,
84+
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
85+
}
86+
87+
InitMtlsPopParameters(ar.TokenBindingCertificate, serviceBundle);
88+
return;
89+
}
90+
91+
// ────────────────────────────────────
92+
// Case 3 – Any other credential (client‑secret etc.)
93+
// ────────────────────────────────────
94+
throw new MsalClientException(
95+
MsalError.MtlsCertificateNotProvided,
96+
MsalErrorMessage.MtlsCertificateNotProvidedMessage);
97+
}
98+
99+
private void InitMtlsPopParameters(X509Certificate2 cert, IServiceBundle serviceBundle)
100+
{
101+
// region check (AAD only)
102+
if (serviceBundle.Config.Authority.AuthorityInfo.AuthorityType == AuthorityType.Aad &&
103+
serviceBundle.Config.AzureRegion == null)
104+
{
105+
throw new MsalClientException(MsalError.MtlsPopWithoutRegion, MsalErrorMessage.MtlsPopWithoutRegion);
106+
}
107+
108+
AuthenticationOperation = new MtlsPopAuthenticationOperation(cert);
109+
MtlsCertificate = cert;
110+
}
37111
}
38112
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Security.Cryptography.X509Certificates;
5+
6+
namespace Microsoft.Identity.Client
7+
{
8+
/// <summary>
9+
/// Container returned from <c>WithClientAssertion</c>.
10+
/// </summary>
11+
public class ClientAssertion
12+
{
13+
/// <summary>
14+
/// Represents the client assertion (JWT) and optional mutual‑TLS binding certificate returned
15+
/// by the <c>clientAssertionProvider</c> callback supplied to
16+
/// <see cref="ConfidentialClientApplicationBuilder.WithClientAssertion(System.Func{AssertionRequestOptions, System.Threading.CancellationToken, System.Threading.Tasks.Task{ClientAssertion}})"/>.
17+
/// </summary>
18+
/// <remarks>
19+
/// MSAL forwards <see cref="Assertion"/> to the token endpoint as the <c>client_assertion</c> parameter.
20+
/// When mutual‑TLS Proof‑of‑Possession (PoP) is enabled on the application and a
21+
/// <see cref="TokenBindingCertificate"/> is provided, MSAL sets <c>client_assertion_type</c> to
22+
/// <c>urn:ietf:params:oauth:client-assertion-type:jwt-pop</c>; otherwise it uses <c>jwt-bearer</c>.
23+
/// <br/><br/>
24+
/// Guidance on constructing the client assertion (required claims, audience, and lifetime) is available at
25+
/// <see href="https://aka.ms/msal-net-client-assertion">aka.ms/msal-net-client-assertion</see>.
26+
/// The assertion is created by your callback; MSAL does not modify or re‑sign it.
27+
/// **Note:** It is up to the caller to cache the assertion and certificate if reuse is desired.
28+
/// </remarks>
29+
public string Assertion { get; set; }
30+
31+
/// <summary>
32+
/// Optional. Certificate used to bind the client assertion for mutual‑TLS Proof‑of‑Possession (PoP).
33+
/// </summary>
34+
/// <remarks>
35+
/// Provide a value only when PoP is enabled on the application. The certificate should include an
36+
/// accessible private key. If <c>null</c>, MSAL treats the assertion as a bearer assertion and uses
37+
/// <c>client_assertion_type=jwt-bearer</c>.
38+
/// </remarks>
39+
public X509Certificate2 TokenBindingCertificate { get; set; }
40+
}
41+
}

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

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -228,13 +228,12 @@ public ConfidentialClientApplicationBuilder WithClientAssertion(Func<string> cli
228228
throw new ArgumentNullException(nameof(clientAssertionDelegate));
229229
}
230230

231-
Func<CancellationToken, Task<string>> clientAssertionAsyncDelegate = (_) =>
232-
{
233-
return Task.FromResult(clientAssertionDelegate());
234-
};
235-
236-
Config.ClientCredential = new SignedAssertionDelegateClientCredential(clientAssertionAsyncDelegate);
237-
return this;
231+
return WithClientAssertion(
232+
(opts, ct) =>
233+
Task.FromResult(new ClientAssertion
234+
{
235+
Assertion = clientAssertionDelegate() // bearer
236+
}));
238237
}
239238

240239
/// <summary>
@@ -252,8 +251,12 @@ public ConfidentialClientApplicationBuilder WithClientAssertion(Func<Cancellatio
252251
throw new ArgumentNullException(nameof(clientAssertionAsyncDelegate));
253252
}
254253

255-
Config.ClientCredential = new SignedAssertionDelegateClientCredential(clientAssertionAsyncDelegate);
256-
return this;
254+
return WithClientAssertion(
255+
async (opts, ct) =>
256+
{
257+
string jwt = await clientAssertionAsyncDelegate(ct).ConfigureAwait(false);
258+
return new ClientAssertion { Assertion = jwt }; // bearer
259+
});
257260
}
258261

259262
/// <summary>
@@ -270,7 +273,30 @@ public ConfidentialClientApplicationBuilder WithClientAssertion(Func<AssertionRe
270273
throw new ArgumentNullException(nameof(clientAssertionAsyncDelegate));
271274
}
272275

273-
Config.ClientCredential = new SignedAssertionDelegateClientCredential(clientAssertionAsyncDelegate);
276+
return WithClientAssertion(
277+
async (opts, _) =>
278+
{
279+
string jwt = await clientAssertionAsyncDelegate(opts).ConfigureAwait(false);
280+
return new ClientAssertion { Assertion = jwt }; // bearer
281+
});
282+
}
283+
284+
/// <summary>
285+
/// Configures the client application to use a client assertion for authentication.
286+
/// </summary>
287+
/// <remarks>This method allows the client application to authenticate using a custom client
288+
/// assertion, which can be useful in scenarios where the assertion needs to be dynamically generated or
289+
/// retrieved.</remarks>
290+
/// <param name="clientAssertionProvider">A delegate that asynchronously provides an <see cref="ClientAssertion"/> based on the given <see
291+
/// cref="AssertionRequestOptions"/> and <see cref="CancellationToken"/>. This delegate must not be <see
292+
/// langword="null"/>.</param>
293+
/// <returns>The <see cref="ConfidentialClientApplicationBuilder"/> instance configured with the specified client
294+
/// assertion.</returns>
295+
/// <exception cref="MsalClientException">Thrown if <paramref name="clientAssertionProvider"/> is <see langword="null"/>.</exception>
296+
public ConfidentialClientApplicationBuilder WithClientAssertion(Func<AssertionRequestOptions,
297+
CancellationToken, Task<ClientAssertion>> clientAssertionProvider)
298+
{
299+
Config.ClientCredential = new ClientAssertionDelegateCredential(clientAssertionProvider);
274300
return this;
275301
}
276302

src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal class MtlsPopAuthenticationOperation : IAuthenticationOperation
1717
public MtlsPopAuthenticationOperation(X509Certificate2 mtlsCert)
1818
{
1919
_mtlsCert = mtlsCert;
20-
KeyId = ComputeX5tS256KeyId(_mtlsCert);
20+
KeyId = CoreHelpers.ComputeX5tS256KeyId(_mtlsCert);
2121
}
2222

2323
public int TelemetryTokenType => TelemetryTokenTypeConstants.MtlsPop;
@@ -40,20 +40,5 @@ public void FormatResult(AuthenticationResult authenticationResult)
4040
{
4141
authenticationResult.BindingCertificate = _mtlsCert;
4242
}
43-
44-
private static string ComputeX5tS256KeyId(X509Certificate2 certificate)
45-
{
46-
// Extract the raw bytes of the certificate’s public key.
47-
var publicKey = certificate.GetPublicKey();
48-
49-
// Compute the SHA-256 hash of the public key.
50-
using (var sha256 = SHA256.Create())
51-
{
52-
byte[] hash = sha256.ComputeHash(publicKey);
53-
54-
// Return the hash encoded in Base64 URL format.
55-
return Base64UrlHelpers.Encode(hash);
56-
}
57-
}
5843
}
5944
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Security.Cryptography.X509Certificates;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.Identity.Client;
9+
using Microsoft.Identity.Client.AuthScheme.PoP;
10+
using Microsoft.Identity.Client.Core;
11+
using Microsoft.Identity.Client.Internal.Requests;
12+
using Microsoft.Identity.Client.OAuth2;
13+
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
14+
using Microsoft.Identity.Client.TelemetryCore;
15+
16+
namespace Microsoft.Identity.Client.Internal.ClientCredential
17+
{
18+
/// <summary>
19+
/// Handles client assertions supplied via a delegate that returns an
20+
/// <see cref="ClientAssertion"/> (JWT + optional certificate bound for mTLS‑PoP).
21+
/// </summary>
22+
internal sealed class ClientAssertionDelegateCredential : IClientCredential
23+
{
24+
private readonly Func<AssertionRequestOptions, CancellationToken, Task<ClientAssertion>> _provider;
25+
26+
internal Task<ClientAssertion> GetAssertionAsync(
27+
AssertionRequestOptions options,
28+
CancellationToken cancellationToken) =>
29+
_provider(options, cancellationToken);
30+
31+
public ClientAssertionDelegateCredential(
32+
Func<AssertionRequestOptions, CancellationToken, Task<ClientAssertion>> provider)
33+
{
34+
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
35+
}
36+
37+
public AssertionType AssertionType => AssertionType.ClientAssertion;
38+
39+
// ──────────────────────────────────
40+
// Main hook for token requests
41+
// ──────────────────────────────────
42+
public async Task AddConfidentialClientParametersAsync(
43+
OAuth2Client oAuth2Client,
44+
AuthenticationRequestParameters p,
45+
ICryptographyManager _,
46+
string tokenEndpoint,
47+
CancellationToken ct)
48+
{
49+
var opts = new AssertionRequestOptions
50+
{
51+
CancellationToken = ct,
52+
ClientID = p.AppConfig.ClientId,
53+
TokenEndpoint = tokenEndpoint,
54+
ClientCapabilities = p.RequestContext.ServiceBundle.Config.ClientCapabilities,
55+
Claims = p.Claims,
56+
ClientAssertionFmiPath = p.ClientAssertionFmiPath
57+
};
58+
59+
ClientAssertion resp = await _provider(opts, ct).ConfigureAwait(false);
60+
61+
if (string.IsNullOrWhiteSpace(resp?.Assertion))
62+
{
63+
throw new MsalClientException(MsalError.InvalidClientAssertion,
64+
MsalErrorMessage.InvalidClientAssertionEmpty);
65+
}
66+
67+
// Decide bearer vs mTLS PoP
68+
bool IsMtlsPopRequested = p.IsMtlsPopRequested;
69+
70+
if (IsMtlsPopRequested && resp.TokenBindingCertificate != null)
71+
{
72+
oAuth2Client.AddBodyParameter(
73+
OAuth2Parameter.ClientAssertionType,
74+
OAuth2AssertionType.JwtPop /* constant added in OAuth2AssertionType */);
75+
}
76+
else
77+
{
78+
oAuth2Client.AddBodyParameter(
79+
OAuth2Parameter.ClientAssertionType,
80+
OAuth2AssertionType.JwtBearer);
81+
}
82+
83+
oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertion, resp.Assertion);
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)