Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6dfcb13
Add support for MTLS-only protocol to MicrosoftIdentityMessageHandler
tlupes May 10, 2026
72c15de
fix
tlupes May 10, 2026
55dd9d6
Use Constants
tlupes May 10, 2026
5448ac6
Fix for resending sent request
tlupes May 13, 2026
7abfb27
Update tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs
tlupes May 19, 2026
3410127
Update src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityM…
tlupes May 19, 2026
e0a0cc9
Update src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs
tlupes May 19, 2026
d89038d
First batch of PR review comments
tlupes May 19, 2026
5119874
Merge branch 'MicrosoftIdentityMessageHandlerMtls' of https://github.…
tlupes May 19, 2026
07135ee
Justification for suppression
tlupes May 25, 2026
3e8b65f
Split auth calls into 3
tlupes May 26, 2026
8a2031a
Fix build break and align mTLS token-binding tests with throw-on-null…
tlupes May 26, 2026
913b3cb
Gate outer claims-challenge retry off for pure mTLS to avoid composin…
tlupes May 26, 2026
970f398
Various comment fixes
tlupes May 26, 2026
ec96938
Merge branch 'master' into MicrosoftIdentityMessageHandlerMtls
tlupes May 26, 2026
2f9f9c9
Potential fix for pull request finding
tlupes May 26, 2026
0fbed67
AI fixes
tlupes May 26, 2026
9daab6d
Merge branch 'MicrosoftIdentityMessageHandlerMtls' of https://github.…
tlupes May 26, 2026
33527e2
Merge branch 'master' into MicrosoftIdentityMessageHandlerMtls
tlupes Jun 11, 2026
82b6789
Fix XML doc summary to say "with mTLS and mTLS_Pop support"
Copilot Jun 11, 2026
816cfec
Make HpptClientFactory optional
tlupes Jun 11, 2026
b82ca62
Merge branch 'MicrosoftIdentityMessageHandlerMtls' of https://github.…
tlupes Jun 11, 2026
a726b2c
Remove test that is impossible to mock
tlupes Jun 11, 2026
702ae0c
Merge branch 'master' into MicrosoftIdentityMessageHandlerMtls
tlupes Jun 12, 2026
7d5304d
UT fixes
tlupes Jun 12, 2026
fd74741
Merge branch 'MicrosoftIdentityMessageHandlerMtls' of https://github.…
tlupes Jun 12, 2026
2256f35
Merge branch 'master' into MicrosoftIdentityMessageHandlerMtls
tlupes Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Microsoft.Identity.Web.Diagnostics
{
/// <summary>
/// Exception for a failed HTTP call. This is exclusively used by reporting and never thrown.
/// </summary>
internal class UnauthorizedHttpRequestException : Exception
Comment thread
tlupes marked this conversation as resolved.
Comment thread
tlupes marked this conversation as resolved.
{
public UnauthorizedHttpRequestException()
{
}

public UnauthorizedHttpRequestException(string message)
: base(message)
{
}

public UnauthorizedHttpRequestException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}
57 changes: 6 additions & 51 deletions src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using Microsoft.Extensions.Options;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web.Diagnostics;
Comment thread
tlupes marked this conversation as resolved.

namespace Microsoft.Identity.Web
{
Expand All @@ -41,31 +42,6 @@ internal partial class DownstreamApi : IDownstreamApi
private const string Authorization = "Authorization";
private const string AuthSchemeDstsSamlBearer = "http://schemas.microsoft.com/dsts/saml2-bearer";

/// <summary>
/// The name of the MTLS_PoP Protocol (Token, along with certificate proof-of-possession).
/// </summary>
private const string TokenBindingProtocolScheme = "MTLS_POP";

/// <summary>
/// The name of the MTLS-only protocol (no tokens)
/// </summary>
private const string MtlsProtocolScheme = "MTLS";

/// <summary>
/// HTTP Status Codes which indicate an issue with the certificate.
/// This should be carefully curated to be accurate and balance false positives with false negatives.
/// If a non-certificate failure is captured here, then the certificate may needlessly change and a pointless retry will occur. This can impact the ability for certificate rotations to occur.
/// If a certificate failure is not captured here, then the certificate will not be refreshed when it should be, which may lead to prolonged outages until a manual refresh occurs.
/// </summary>
private static readonly HashSet<HttpStatusCode> AuthFailureHttpStatusCodes =
[
HttpStatusCode.BadRequest,
HttpStatusCode.Unauthorized,
HttpStatusCode.Forbidden,
(HttpStatusCode)495, // nginx "SSL Certificate Error"
(HttpStatusCode)496, // nginx "SSL Certificate Required"
];

protected readonly ILogger<DownstreamApi> _logger;

/// <summary>
Expand Down Expand Up @@ -632,7 +608,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
{
CredentialSourceLoaderParameters loaderParameters = new CredentialSourceLoaderParameters(string.Empty, string.Empty)
{
Protocol = MtlsProtocolScheme,
Protocol = Constants.MtlsProtocolScheme,
ApiUrl = effectiveOptions.GetApiUrl(),
};

Expand All @@ -646,7 +622,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
true,
null);
}
else if (AuthFailureHttpStatusCodes.Contains(downstreamApiResult.StatusCode))
else if (Constants.AuthFailureHttpStatusCodes.Contains(downstreamApiResult.StatusCode))
{
// Only alert if the failure is potentially due to the certificate.
// This to to avoid needlessly refreshing the certificate on non-certificate related failures.
Expand Down Expand Up @@ -705,7 +681,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(

// Obtention of the authorization header (except when calling an anonymous endpoint)
// which is done by not specifying any scopes or mTLS scheme.
if (string.Equals(effectiveOptions.ProtocolScheme, MtlsProtocolScheme, StringComparison.OrdinalIgnoreCase))
if (string.Equals(effectiveOptions.ProtocolScheme, Constants.MtlsProtocolScheme, StringComparison.OrdinalIgnoreCase))
{
if (_credentialsProvider == null)
{
Expand All @@ -716,7 +692,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
new CredentialSourceLoaderParameters(string.Empty, string.Empty)
{
ApiUrl = effectiveOptions.GetApiUrl(),
Protocol = MtlsProtocolScheme,
Protocol = Constants.MtlsProtocolScheme,
},
cancellationToken);

Expand All @@ -738,7 +714,7 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
// Firstly check if it's token binding scenario so authorization header provider returns
// a binding certificate along with acquired authorization header.
if (_authorizationHeaderProvider is IBoundAuthorizationHeaderProvider boundAuthorizationHeaderBoundProvider
&& string.Equals(effectiveOptions.ProtocolScheme, TokenBindingProtocolScheme, StringComparison.OrdinalIgnoreCase))
&& string.Equals(effectiveOptions.ProtocolScheme, Constants.TokenBindingProtocolScheme, StringComparison.OrdinalIgnoreCase))
{
var authorizationHeaderResult = await boundAuthorizationHeaderBoundProvider.CreateBoundAuthorizationHeaderAsync(
effectiveOptions,
Expand Down Expand Up @@ -899,26 +875,5 @@ internal static async Task<string> ReadErrorResponseContentAsync(HttpResponseMes

return errorResponseContent;
}

/// <summary>
/// Exception for a failed HTTP call. This is exclusively used by reporting and never thrown.
/// </summary>
private class UnauthorizedHttpRequestException : Exception
{
public UnauthorizedHttpRequestException()
{
}

public UnauthorizedHttpRequestException(string message)
: base(message)
{
}

public UnauthorizedHttpRequestException(string message, Exception innerException)
: base(message, innerException)
{
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<!-- Include AOT attribute polyfills for older TFMs -->
<ItemGroup Condition="'$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'net472' or '$(TargetFramework)' == 'netstandard2.0'">
<Compile Include="..\Shared\CodeAnalysisAttributes.cs" Link="Polyfills\CodeAnalysisAttributes.cs" />
</ItemGroup>
<ItemGroup>
Comment thread
neha-bhargava marked this conversation as resolved.
<None Include="..\..\README.md">
<Pack>True</Pack>
Expand Down
32 changes: 30 additions & 2 deletions src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Net;

namespace Microsoft.Identity.Web
{
Expand Down Expand Up @@ -240,7 +241,7 @@ public static class Constants
/// <summary>
/// Error codes indicating certificate or signed assertion issues that warrant retry with a new certificate.
/// </summary>
internal static readonly HashSet<string> s_certificateRelatedErrorCodes = new (StringComparer.OrdinalIgnoreCase)
internal static readonly IReadOnlyCollection<string> CertificateAuthFailureStsErrorCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
InvalidKeyError, // AADSTS700027 - Client assertion contains an invalid signature
SignedAssertionInvalidTimeRange, // AADSTS700024 - Signed assertion invalid time range
Expand All @@ -250,10 +251,27 @@ public static class Constants
CertificateWasRevoked, // AADSTS7000277 - Certificate was revoked
};

/// <summary>
/// HTTP status codes which may indicate that a certificate based authentication failure occurred.
/// </summary>
/*
* Used by Microsoft.Identity.Web.DownstreamApi
* Any changes to this member (including removal) can cause runtime failures.
* Treat as a public member.
*/
internal static readonly IReadOnlyCollection<HttpStatusCode> AuthFailureHttpStatusCodes = new HashSet<HttpStatusCode>()
{
HttpStatusCode.BadRequest,
HttpStatusCode.Unauthorized,
HttpStatusCode.Forbidden,
(HttpStatusCode)495, // nginx "SSL Certificate Error"
(HttpStatusCode)496, // nginx "SSL Certificate Required"
};

/// <summary>
/// Error codes indicating permanent configuration errors that should not trigger retry.
/// </summary>
internal static readonly HashSet<string> s_nonRetryableConfigErrorCodes = new (StringComparer.OrdinalIgnoreCase)
internal static readonly IReadOnlyCollection<string> s_nonRetryableConfigErrorCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
InvalidClientSecret, // AADSTS7000215 - Wrong client secret
ApplicationNotFound, // AADSTS700016 - Application with identifier not found
Expand Down Expand Up @@ -345,5 +363,15 @@ public static class Constants
* Treat as a public member.
*/
internal const string Upn = "upn";

/// <summary>
/// The name of the MTLS_PoP Protocol (Token, along with certificate proof-of-possession).
/// </summary>
internal const string TokenBindingProtocolScheme = "MTLS_POP";

/// <summary>
/// The name of the MTLS-only protocol (no tokens)
/// </summary>
internal const string MtlsProtocolScheme = "MTLS";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
[assembly: SuppressMessage("ApiDesign", "RS0016:Symbol is not part of the declared API", Justification = "Protected serialization constructor for .NET Framework/Standard 2.0 compatibility", Scope = "member", Target = "~M:Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext)")]
[assembly: SuppressMessage("ApiDesign", "RS0027:API with optional parameter(s) should have the most parameters amongst its public overloads", Justification = "Existing shipped API; new overload adds mTLS PoP support", Scope = "member", Target = "~M:Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.#ctor(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider,Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions,Microsoft.Extensions.Logging.ILogger{Microsoft.Identity.Web.MicrosoftIdentityMessageHandler})")]
[assembly: SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "New overload adds mTLS PoP support alongside existing shipped constructor", Scope = "member", Target = "~M:Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.#ctor(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider,Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions,Microsoft.Identity.Client.IMsalMtlsHttpClientFactory,Microsoft.Extensions.Logging.ILogger{Microsoft.Identity.Web.MicrosoftIdentityMessageHandler})")]
[assembly: SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "New overload adds mTLS only support alongside existing shipped constructors", Scope = "member", Target = "~M:Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.#ctor(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider,Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions,Microsoft.Identity.Client.IMsalMtlsHttpClientFactory,Microsoft.Identity.Web.ICredentialsProvider,Microsoft.Extensions.Logging.ILogger{Microsoft.Identity.Web.MicrosoftIdentityMessageHandler})")]
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
using System.Net.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web.Experimental;

namespace Microsoft.Identity.Web
{
Expand Down Expand Up @@ -136,10 +140,12 @@ public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler(

return builder.AddHttpMessageHandler(sp =>
{
var headerProvider = sp.GetRequiredService<IAuthorizationHeaderProvider>();
var httpClientFactory = sp.GetService<IHttpClientFactory>();
var mtlsHttpClientFactory = httpClientFactory != null ? new MsalMtlsHttpClientFactory(httpClientFactory) : null;
return new MicrosoftIdentityMessageHandler(headerProvider, defaultOptions: null, mtlsHttpClientFactory, logger: null);
return new MicrosoftIdentityMessageHandler(
sp.GetRequiredService<IAuthorizationHeaderProvider>(),
defaultOptions: null,
GetMsalMtlsHttpClientFactory(sp),
GetCredentialsProvider(sp),
GetLogger<MicrosoftIdentityMessageHandler>(sp));
});
}

Expand Down Expand Up @@ -214,10 +220,12 @@ public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler(

return builder.AddHttpMessageHandler(sp =>
{
var headerProvider = sp.GetRequiredService<IAuthorizationHeaderProvider>();
var httpClientFactory = sp.GetService<IHttpClientFactory>();
var mtlsHttpClientFactory = httpClientFactory != null ? new MsalMtlsHttpClientFactory(httpClientFactory) : null;
return new MicrosoftIdentityMessageHandler(headerProvider, options, mtlsHttpClientFactory, logger: null);
return new MicrosoftIdentityMessageHandler(
sp.GetRequiredService<IAuthorizationHeaderProvider>(),
options,
GetMsalMtlsHttpClientFactory(sp),
GetCredentialsProvider(sp),
GetLogger<MicrosoftIdentityMessageHandler>(sp));
});
}

Expand Down Expand Up @@ -303,10 +311,12 @@ public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler(
var options = new MicrosoftIdentityMessageHandlerOptions();
configureOptions(options);

var headerProvider = sp.GetRequiredService<IAuthorizationHeaderProvider>();
var httpClientFactory = sp.GetService<IHttpClientFactory>();
var mtlsHttpClientFactory = httpClientFactory != null ? new MsalMtlsHttpClientFactory(httpClientFactory) : null;
return new MicrosoftIdentityMessageHandler(headerProvider, options, mtlsHttpClientFactory, logger: null);
return new MicrosoftIdentityMessageHandler(
sp.GetRequiredService<IAuthorizationHeaderProvider>(),
options,
GetMsalMtlsHttpClientFactory(sp),
GetCredentialsProvider(sp),
GetLogger<MicrosoftIdentityMessageHandler>(sp));
});
}

Expand Down Expand Up @@ -421,11 +431,62 @@ public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler(
var options = new MicrosoftIdentityMessageHandlerOptions();
configuration.Bind(options);

var headerProvider = sp.GetRequiredService<IAuthorizationHeaderProvider>();
var httpClientFactory = sp.GetService<IHttpClientFactory>();
var mtlsHttpClientFactory = httpClientFactory != null ? new MsalMtlsHttpClientFactory(httpClientFactory) : null;
return new MicrosoftIdentityMessageHandler(headerProvider, options, mtlsHttpClientFactory, logger: null);
return new MicrosoftIdentityMessageHandler(
sp.GetRequiredService<IAuthorizationHeaderProvider>(),
options,
GetMsalMtlsHttpClientFactory(sp),
GetCredentialsProvider(sp),
GetLogger<MicrosoftIdentityMessageHandler>(sp));
});
}

private static IMsalMtlsHttpClientFactory? GetMsalMtlsHttpClientFactory(IServiceProvider services)
{
var msalMtlsHttpClientFactory = services.GetService<IMsalMtlsHttpClientFactory>();
if (msalMtlsHttpClientFactory is not null)
{
return msalMtlsHttpClientFactory;
}

var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory is not null)
{
return new MsalMtlsHttpClientFactory(httpClientFactory);
Comment thread
tlupes marked this conversation as resolved.
}

return null;
}

private static ICredentialsProvider? GetCredentialsProvider(IServiceProvider services)
{
var credentialsProvider = services.GetService<ICredentialsProvider>();
if (credentialsProvider is not null)
{
return credentialsProvider;
}

var loader = services.GetService<ICredentialsLoader>();
if (loader is not null)
{
return new CredentialsProvider(
Comment thread
tlupes marked this conversation as resolved.
GetLogger<CredentialsProvider>(services),
loader,
services.GetServices<ICertificatesObserver>() ?? [],
services.GetService<ITokenAcquisitionHost>());
}

return null;
}

private static ILogger<T> GetLogger<T>(IServiceProvider services)
{
var loggerFactory = services.GetService<ILoggerFactory>();
if (loggerFactory is not null)
{
return loggerFactory.CreateLogger<T>();
}

return NullLogger<T>.Instance;
}
}
}
Loading
Loading