diff --git a/Microsoft.Identity.Web.sln b/Microsoft.Identity.Web.sln index 638896dac..08f93676e 100644 --- a/Microsoft.Identity.Web.sln +++ b/Microsoft.Identity.Web.sln @@ -215,6 +215,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireBlazorCallsWebApi.Web EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireBlazorCallsWebApi.AppHost", "tests\DevApps\AspireBlazorCallsWebApi\AspireBlazorCallsWebApi.AppHost\AspireBlazorCallsWebApi.AppHost.csproj", "{CF9A0AD1-477A-AC6E-E94E-3E6D5D8F07BE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mtls", "Mtls", "{D70AD3EC-52EE-495D-BA5E-C9671601C699}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MtlsClient", "tests\DevApps\Mtls\MtlsClient\MtlsClient.csproj", "{CF703B6E-6F28-8E41-484C-FFDA485A1293}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MtlsWebApi", "tests\DevApps\Mtls\MtlsWebApi\MtlsWebApi.csproj", "{270F5686-0E0B-74E6-E66F-87C54BEF24DE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -482,6 +488,14 @@ Global {CF9A0AD1-477A-AC6E-E94E-3E6D5D8F07BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {CF9A0AD1-477A-AC6E-E94E-3E6D5D8F07BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {CF9A0AD1-477A-AC6E-E94E-3E6D5D8F07BE}.Release|Any CPU.Build.0 = Release|Any CPU + {CF703B6E-6F28-8E41-484C-FFDA485A1293}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF703B6E-6F28-8E41-484C-FFDA485A1293}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF703B6E-6F28-8E41-484C-FFDA485A1293}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF703B6E-6F28-8E41-484C-FFDA485A1293}.Release|Any CPU.Build.0 = Release|Any CPU + {270F5686-0E0B-74E6-E66F-87C54BEF24DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {270F5686-0E0B-74E6-E66F-87C54BEF24DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {270F5686-0E0B-74E6-E66F-87C54BEF24DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {270F5686-0E0B-74E6-E66F-87C54BEF24DE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -569,6 +583,9 @@ Global {106919C1-549A-AEAE-9925-E9E50B81BD23} = {688187B8-8AEF-4C80-948B-92BCCDE86D76} {5373EB99-4B37-B3B3-8280-2A1EDB79F198} = {688187B8-8AEF-4C80-948B-92BCCDE86D76} {CF9A0AD1-477A-AC6E-E94E-3E6D5D8F07BE} = {688187B8-8AEF-4C80-948B-92BCCDE86D76} + {D70AD3EC-52EE-495D-BA5E-C9671601C699} = {7786D2DD-9EE4-42E1-B587-740A2E15C41D} + {CF703B6E-6F28-8E41-484C-FFDA485A1293} = {D70AD3EC-52EE-495D-BA5E-C9671601C699} + {270F5686-0E0B-74E6-E66F-87C54BEF24DE} = {D70AD3EC-52EE-495D-BA5E-C9671601C699} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {104367F1-CE75-4F40-B32F-F14853973187} diff --git a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs index 75ef5a4b3..cac8b46a4 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs +++ b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs @@ -6,14 +6,17 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Runtime.CompilerServices; using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Identity.Abstractions; @@ -25,19 +28,44 @@ namespace Microsoft.Identity.Web internal partial class DownstreamApi : IDownstreamApi { private readonly IAuthorizationHeaderProvider _authorizationHeaderProvider; + private readonly ICredentialsProvider? _credentialsProvider; private readonly IHttpClientFactory _httpClientFactory; // This MSAL HTTP client factory is used to create HTTP clients with mTLS binding certificate. // Note, that it doesn't replace _httpClientFactory to keep backward compatibility and ability // to create named HTTP clients for non-mTLS scenarios. - private readonly IMsalHttpClientFactory? _msalHttpClientFactory; + private readonly IMsalHttpClientFactory _msalHttpClientFactory; private readonly IOptionsMonitor _namedDownstreamApiOptions; private const string Authorization = "Authorization"; - private const string TokenBindingProtocolScheme = "MTLS_POP"; private const string AuthSchemeDstsSamlBearer = "http://schemas.microsoft.com/dsts/saml2-bearer"; + /// + /// The name of the MTLS_PoP Protocol (Token, along with certificate proof-of-possession). + /// + private const string TokenBindingProtocolScheme = "MTLS_POP"; + + /// + /// The name of the MTLS-only protocol (no tokens) + /// + private const string MtlsProtocolScheme = "MTLS"; + + /// + /// 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. + /// + private static readonly HashSet AuthFailureHttpStatusCodes = + [ + HttpStatusCode.BadRequest, + HttpStatusCode.Unauthorized, + HttpStatusCode.Forbidden, + (HttpStatusCode)495, // nginx "SSL Certificate Error" + (HttpStatusCode)496, // nginx "SSL Certificate Required" + ]; + protected readonly ILogger _logger; /// @@ -56,6 +84,7 @@ public DownstreamApi( namedDownstreamApiOptions, httpClientFactory, logger, + credentialsProvider: null!, msalHttpClientFactory: null) { } @@ -74,10 +103,36 @@ public DownstreamApi( IHttpClientFactory httpClientFactory, ILogger logger, IMsalHttpClientFactory? msalHttpClientFactory) + : this(authorizationHeaderProvider, + namedDownstreamApiOptions, + httpClientFactory, + logger, + credentialsProvider: null!, + msalHttpClientFactory: msalHttpClientFactory) + { + } + + /// + /// Constructor which accepts optional MSAL HTTP client factory. + /// + /// Authorization header provider. + /// Named options provider. + /// HTTP client factory. + /// Logger. + /// Certificate provider for mTLS auth scenarios. + /// The MSAL HTTP client factory for mTLS PoP scenarios. + public DownstreamApi( + IAuthorizationHeaderProvider authorizationHeaderProvider, + IOptionsMonitor namedDownstreamApiOptions, + IHttpClientFactory httpClientFactory, + ILogger logger, + ICredentialsProvider credentialsProvider, + IMsalHttpClientFactory? msalHttpClientFactory) { _authorizationHeaderProvider = authorizationHeaderProvider; _namedDownstreamApiOptions = namedDownstreamApiOptions; _httpClientFactory = httpClientFactory; + _credentialsProvider = credentialsProvider; _msalHttpClientFactory = msalHttpClientFactory ?? new MsalMtlsHttpClientFactory(httpClientFactory); _logger = logger; } @@ -528,45 +583,95 @@ public Task CallApiForAppAsync( { // Downstream API URI string apiUrl = effectiveOptions.GetApiUrl(); + HttpResponseMessage downstreamApiResult; + bool retry; + bool hasRetried = false; - // Create an HTTP request message - using HttpRequestMessage httpRequestMessage = new( - new HttpMethod(effectiveOptions.HttpMethod), - apiUrl); + do + { + retry = false; - // Request result will contain authorization header and potentially binding certificate for mTLS - var requestResult = await UpdateRequestAsync(httpRequestMessage, content, effectiveOptions, appToken, user, cancellationToken); + // Create an HTTP request message + using HttpRequestMessage httpRequestMessage = new( + new HttpMethod(effectiveOptions.HttpMethod), + apiUrl); - // If a binding certificate is specified (which means mTLS is required) and MSAL mTLS HTTP factory is present - // then create an HttpClient with the certificate by using IMsalMtlsHttpClientFactory. - // Otherwise use the default HttpClientFactory with optional named client. - HttpClient client = requestResult?.BindingCertificate != null && _msalHttpClientFactory != null && _msalHttpClientFactory is IMsalMtlsHttpClientFactory msalMtlsHttpClientFactory - ? msalMtlsHttpClientFactory.GetHttpClient(requestResult.BindingCertificate) - : (string.IsNullOrEmpty(serviceName) ? _httpClientFactory.CreateClient() : _httpClientFactory.CreateClient(serviceName)); + // Request result will contain authorization header and potentially binding certificate for mTLS + (AuthorizationHeaderInformation? requestResult, CredentialDescription? mtlsCred) = await UpdateRequestWithCertificateAsync(httpRequestMessage, content, effectiveOptions, appToken, user, cancellationToken); - // Send the HTTP message - var downstreamApiResult = await client.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); + // If a binding certificate is specified (which means mTLS is required) and MSAL mTLS HTTP factory is present + // then create an HttpClient with the certificate by using IMsalMtlsHttpClientFactory. + // Otherwise use the default HttpClientFactory with optional named client. + HttpClient client = requestResult?.BindingCertificate != null && _msalHttpClientFactory is IMsalMtlsHttpClientFactory msalMtlsHttpClientFactory + ? msalMtlsHttpClientFactory.GetHttpClient(requestResult.BindingCertificate) + : (string.IsNullOrEmpty(serviceName) ? _httpClientFactory.CreateClient() : _httpClientFactory.CreateClient(serviceName)); - // Retry only if the resource sent 401 Unauthorized with WWW-Authenticate header and claims - if (downstreamApiResult.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - effectiveOptions.AcquireTokenOptions.Claims = WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(downstreamApiResult.Headers); + // Send the HTTP message + downstreamApiResult = await client.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrEmpty(effectiveOptions.AcquireTokenOptions.Claims)) + // Retry only if the resource sent 401 Unauthorized with WWW-Authenticate header and claims + if (downstreamApiResult.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - using HttpRequestMessage retryHttpRequestMessage = new( - new HttpMethod(effectiveOptions.HttpMethod), - apiUrl); + effectiveOptions.AcquireTokenOptions.Claims = WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(downstreamApiResult.Headers); + + if (!string.IsNullOrEmpty(effectiveOptions.AcquireTokenOptions.Claims)) + { + using HttpRequestMessage retryHttpRequestMessage = new( + new HttpMethod(effectiveOptions.HttpMethod), + apiUrl); - await UpdateRequestAsync(retryHttpRequestMessage, content, effectiveOptions, appToken, user, cancellationToken); + await UpdateRequestWithCertificateAsync(retryHttpRequestMessage, content, effectiveOptions, appToken, user, cancellationToken); + + return await client.SendAsync(retryHttpRequestMessage, cancellationToken).ConfigureAwait(false); + } + } + + // Track mTLS certificate result, if applicable. + // This may also trigger a retry if a certificate failure occurred. + if (mtlsCred != null && requestResult?.BindingCertificate != null && _credentialsProvider != null) + { + CredentialSourceLoaderParameters loaderParameters = new CredentialSourceLoaderParameters(string.Empty, string.Empty) + { + Protocol = MtlsProtocolScheme, + ApiUrl = effectiveOptions.GetApiUrl(), + }; - return await client.SendAsync(retryHttpRequestMessage, cancellationToken).ConfigureAwait(false); + // Always fire on success. + if (downstreamApiResult.IsSuccessStatusCode) + { + _credentialsProvider.NotifyCertificateUsed( + loaderParameters, + mtlsCred, + requestResult.BindingCertificate, + true, + null); + } + else if (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. + _credentialsProvider.NotifyCertificateUsed( + loaderParameters, + mtlsCred, + requestResult.BindingCertificate, + false, + new UnauthorizedHttpRequestException($"Response has status {downstreamApiResult.StatusCode} - {downstreamApiResult.ReasonPhrase}")); + + // Retry certificate failures once + if (!hasRetried) + { + retry = true; + hasRetried = true; + } + } } } + while (retry); return downstreamApiResult; } + [Obsolete("Replaced by UpdateRequestWithCertificateAsync")] internal /* internal for test */ async Task UpdateRequestAsync( HttpRequestMessage httpRequestMessage, HttpContent? content, @@ -574,6 +679,17 @@ public Task CallApiForAppAsync( bool appToken, ClaimsPrincipal? user, CancellationToken cancellationToken) + { + return (await UpdateRequestWithCertificateAsync(httpRequestMessage, content, effectiveOptions, appToken, user, cancellationToken)).HeaderInfo; + } + + internal /* internal for test */ async Task<(AuthorizationHeaderInformation? HeaderInfo, CredentialDescription? MtlsCredential)> UpdateRequestWithCertificateAsync( + HttpRequestMessage httpRequestMessage, + HttpContent? content, + DownstreamApiOptions effectiveOptions, + bool appToken, + ClaimsPrincipal? user, + CancellationToken cancellationToken) { AddCallerSDKTelemetry(effectiveOptions); @@ -585,10 +701,37 @@ public Task CallApiForAppAsync( effectiveOptions.RequestAppToken = appToken; AuthorizationHeaderInformation? authorizationHeaderInformation = null; + CredentialDescription? credential = null; - // Obtention of the authorization header (except when calling an anonymous endpoint - // which is done by not specifying any scopes - if (effectiveOptions.Scopes != null && effectiveOptions.Scopes.Any()) + // 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 (_credentialsProvider == null) + { + throw new InvalidOperationException("mTLS authentication requires a Credentials Provider object to be registered, but no such service was found."); + } + + credential = await _credentialsProvider.GetCredentialAsync( + new CredentialSourceLoaderParameters(string.Empty, string.Empty) + { + ApiUrl = effectiveOptions.GetApiUrl(), + Protocol = MtlsProtocolScheme, + }, + cancellationToken); + + if (credential == null || credential.Certificate == null) + { + throw new InvalidOperationException("mTLS authentication requires a certificate, but no certificate was found."); + } + + authorizationHeaderInformation = new AuthorizationHeaderInformation() + { + AuthorizationHeaderValue = null, + BindingCertificate = credential.Certificate, + }; + } + else if (effectiveOptions.Scopes != null && effectiveOptions.Scopes.Any()) { string authorizationHeader = string.Empty; @@ -635,6 +778,7 @@ public Task CallApiForAppAsync( { Logger.UnauthenticatedApiCall(_logger, null); } + if (!string.IsNullOrEmpty(effectiveOptions.AcceptHeader)) { httpRequestMessage.Headers.Accept.ParseAdd(effectiveOptions.AcceptHeader); @@ -679,7 +823,7 @@ public Task CallApiForAppAsync( // Opportunity to change the request message effectiveOptions.CustomizeHttpRequestMessage?.Invoke(httpRequestMessage); - return authorizationHeaderInformation; + return (authorizationHeaderInformation, credential); } internal /* for test */ static Dictionary CallerSDKDetails { get; } = new() @@ -741,5 +885,26 @@ internal static async Task ReadErrorResponseContentAsync(HttpResponseMes return errorResponseContent; } + + /// + /// Exception for a failed HTTP call. This is exclusively used by reporting and never thrown. + /// + private class UnauthorizedHttpRequestException : Exception + { + public UnauthorizedHttpRequestException() + { + } + + public UnauthorizedHttpRequestException(string message) + : base(message) + { + } + + public UnauthorizedHttpRequestException(string message, Exception innerException) + : base(message, innerException) + { + } + } + } } diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Unshipped.txt index 7dc5c5811..08a25e3a5 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void +Microsoft.Identity.Web.DownstreamApi.UpdateRequestWithCertificateAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(Microsoft.Identity.Abstractions.AuthorizationHeaderInformation? HeaderInfo, Microsoft.Identity.Abstractions.CredentialDescription? MtlsCredential)>! diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net462/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net462/InternalAPI.Unshipped.txt index 7dc5c5811..08a25e3a5 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net462/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net462/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void +Microsoft.Identity.Web.DownstreamApi.UpdateRequestWithCertificateAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(Microsoft.Identity.Abstractions.AuthorizationHeaderInformation? HeaderInfo, Microsoft.Identity.Abstractions.CredentialDescription? MtlsCredential)>! diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net472/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net472/InternalAPI.Unshipped.txt index 7dc5c5811..08a25e3a5 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net472/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net472/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void +Microsoft.Identity.Web.DownstreamApi.UpdateRequestWithCertificateAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(Microsoft.Identity.Abstractions.AuthorizationHeaderInformation? HeaderInfo, Microsoft.Identity.Abstractions.CredentialDescription? MtlsCredential)>! diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net8.0/InternalAPI.Unshipped.txt index 7dc5c5811..08a25e3a5 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void +Microsoft.Identity.Web.DownstreamApi.UpdateRequestWithCertificateAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(Microsoft.Identity.Abstractions.AuthorizationHeaderInformation? HeaderInfo, Microsoft.Identity.Abstractions.CredentialDescription? MtlsCredential)>! diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net9.0/InternalAPI.Unshipped.txt index 7dc5c5811..08a25e3a5 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net9.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net9.0/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void +Microsoft.Identity.Web.DownstreamApi.UpdateRequestWithCertificateAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(Microsoft.Identity.Abstractions.AuthorizationHeaderInformation? HeaderInfo, Microsoft.Identity.Abstractions.CredentialDescription? MtlsCredential)>! diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt index 7dc5c5811..08a25e3a5 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void +Microsoft.Identity.Web.DownstreamApi.UpdateRequestWithCertificateAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(Microsoft.Identity.Abstractions.AuthorizationHeaderInformation? HeaderInfo, Microsoft.Identity.Abstractions.CredentialDescription? MtlsCredential)>! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisition-AspnetCore.cs b/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisition-AspnetCore.cs index a8a2d868b..eb62ab699 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisition-AspnetCore.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisition-AspnetCore.cs @@ -32,15 +32,13 @@ internal class TokenAcquisitionAspNetCore : TokenAcquisition, ITokenAcquisitionI /// HTTP client factory. /// Logger. /// Service provider. - /// Credential loader service. public TokenAcquisitionAspNetCore( IMsalTokenCacheProvider tokenCacheProvider, IHttpClientFactory httpClientFactory, ILogger logger, ITokenAcquisitionHost tokenAcquisitionHost, - IServiceProvider serviceProvider, - ICredentialsLoader credentialsLoader) : - base(tokenCacheProvider, tokenAcquisitionHost, httpClientFactory, logger, serviceProvider, credentialsLoader) + IServiceProvider serviceProvider) : + base(tokenCacheProvider, tokenAcquisitionHost, httpClientFactory, logger, serviceProvider) { } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs index a03d6d508..a32d070ba 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs @@ -4,45 +4,29 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Client; +using static Microsoft.Identity.Web.TokenAcquisition; namespace Microsoft.Identity.Web { internal static partial class ConfidentialClientApplicationBuilderExtension { - [Obsolete(IDWebErrorMessage.WithClientCredentialsIsObsolete, false)] - public static ConfidentialClientApplicationBuilder WithClientCredentials( - this ConfidentialClientApplicationBuilder builder, - IEnumerable clientCredentials, - ILogger logger, - ICredentialsLoader credentialsLoader, - CredentialSourceLoaderParameters credentialSourceLoaderParameters) - { - return WithClientCredentialsAsync( - builder, - clientCredentials, - logger, - credentialsLoader, - credentialSourceLoaderParameters, - isTokenBinding: false).GetAwaiter().GetResult(); - } - public static async Task WithClientCredentialsAsync( this ConfidentialClientApplicationBuilder builder, - IEnumerable clientCredentials, - ILogger logger, - ICredentialsLoader credentialsLoader, + MergedOptions mergedOptions, + ICredentialsProvider credentialsProvider, CredentialSourceLoaderParameters? credentialSourceLoaderParameters, - bool isTokenBinding) + bool isTokenBinding, + CancellationToken cancellationToken = default) { - var credential = await LoadCredentialForMsalOrFailAsync( - clientCredentials, - logger, - credentialsLoader, - credentialSourceLoaderParameters) + var credential = await credentialsProvider.GetCredentialAsync( + mergedOptions, + credentialSourceLoaderParameters, + cancellationToken) .ConfigureAwait(false); if (isTokenBinding) @@ -52,7 +36,6 @@ public static async Task WithClientCredent return builder.WithCertificate(credential.Certificate); } - logger.LogError("A certificate, which is required for token binding, is missing in loaded credentials."); throw new InvalidOperationException(IDWebErrorMessage.MissingTokenBindingCertificate); } @@ -74,101 +57,5 @@ public static async Task WithClientCredent } } - - internal /* for test */ async static Task LoadCredentialForMsalOrFailAsync( - IEnumerable clientCredentials, - ILogger logger, - ICredentialsLoader credentialsLoader, - CredentialSourceLoaderParameters? credentialSourceLoaderParameters) - { - string errorMessage = "\n"; - - foreach (CredentialDescription credential in clientCredentials) - { - Logger.AttemptToLoadCredentials(logger, credential); - - if (!credential.Skip) - { - // Load the credentials and record error messages in case we need to fail at the end - try - { - - await credentialsLoader.LoadCredentialsIfNeededAsync(credential, credentialSourceLoaderParameters); - } - catch (Exception ex) - { - Logger.AttemptToLoadCredentialsFailed(logger, credential, ex); - errorMessage += $"Credential {credential.Id} failed because: {ex} \n"; - } - - - if (credential.CredentialType == CredentialType.SignedAssertion) - { - if (credential.SourceType == CredentialSource.SignedAssertionFromManagedIdentity) - { - if (credential.Skip) - { - Logger.NotUsingManagedIdentity(logger, errorMessage); - } - else - { - Logger.UsingManagedIdentity(logger); - return credential; - } - } - if (credential.SourceType == CredentialSource.SignedAssertionFilePath) - { - if (!credential.Skip) - { - Logger.UsingPodIdentityFile(logger, credential.SignedAssertionFileDiskPath ?? "not found"); - return credential; - } - } - if (credential.SourceType == CredentialSource.SignedAssertionFromVault) - { - if (!credential.Skip) - { - Logger.UsingSignedAssertionFromVault(logger, credential.KeyVaultUrl ?? "undefined"); - return credential; - } - } - if (credential.SourceType == CredentialSource.CustomSignedAssertion) - { - if (!credential.Skip) - { - Logger.UsingSignedAssertionFromCustomProvider(logger, credential.CustomSignedAssertionProviderName ?? "undefined"); - return credential; - } - } - } - - if (credential.CredentialType == CredentialType.Certificate) - { - if (credential.Certificate != null) - { - Logger.UsingCertThumbprint(logger, credential.Certificate?.Thumbprint); - return credential; - } - } - - if (credential.CredentialType == CredentialType.Secret) - { - return credential; - } - } - } - - if (clientCredentials.Any(c => c.CredentialType == CredentialType.Certificate || c.CredentialType == CredentialType.SignedAssertion)) - { - throw new ArgumentException( - IDWebErrorMessage.ClientCertificatesHaveExpiredOrCannotBeLoaded + errorMessage, - nameof(clientCredentials)); - } - - logger.LogInformation($"No client credential could be used. Secret may have been defined elsewhere. " + - $"Count {(clientCredentials != null ? clientCredentials.Count() : 0)} "); - - return null; - } } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.Logger.cs b/src/Microsoft.Identity.Web.TokenAcquisition/CredentialsProvider.LogMessages.cs similarity index 98% rename from src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.Logger.cs rename to src/Microsoft.Identity.Web.TokenAcquisition/CredentialsProvider.LogMessages.cs index 9b954ac61..4b0b0e3dd 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.Logger.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/CredentialsProvider.LogMessages.cs @@ -7,9 +7,9 @@ namespace Microsoft.Identity.Web { - internal partial class ConfidentialClientApplicationBuilderExtension + internal partial class CredentialsProvider { - internal static class Logger + private class LogMessages { private static readonly Action s_notManagedIdentity = LoggerMessage.Define( diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/CredentialsProvider.cs b/src/Microsoft.Identity.Web.TokenAcquisition/CredentialsProvider.cs new file mode 100644 index 000000000..e9f1f9d0a --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/CredentialsProvider.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web.Experimental; + +namespace Microsoft.Identity.Web +{ + internal partial class CredentialsProvider : ICredentialsProvider + { + private readonly ILogger _logger; + private readonly ITokenAcquisitionHost? _tokenHost; + private readonly ICredentialsLoader _credentialsLoader; + private readonly IReadOnlyList _certificatesObservers; + + public CredentialsProvider( + ILogger logger, + ICredentialsLoader credentialsLoader, + IEnumerable certificatesObservers, + ITokenAcquisitionHost? tokenHost = null) + { + _logger = logger; + _tokenHost = tokenHost; + _credentialsLoader = credentialsLoader; + _certificatesObservers = [.. certificatesObservers]; + } + + public Task GetCredentialAsync( + CredentialSourceLoaderParameters? credentialSourceLoaderParameters, + CancellationToken cancellationToken = default) + { + if (_tokenHost == null) + { + throw new InvalidOperationException("Token acquisition host is not available."); + } + + return GetCredentialAsync( + _tokenHost.GetOptions(null, out string effectiveScheme) ?? throw new InvalidOperationException($"Unable to load client credentials for scheme '{effectiveScheme}'."), + credentialSourceLoaderParameters, + cancellationToken); + } + + public async Task GetCredentialAsync( + MergedOptions options, + CredentialSourceLoaderParameters? credentialSourceLoaderParameters, + CancellationToken cancellationToken = default) + { + IEnumerable clientCredentials = options.ClientCredentials ?? []; + + string errorMessage = "\n"; + + foreach (CredentialDescription credential in clientCredentials) + { + LogMessages.AttemptToLoadCredentials(_logger, credential); + + if (!credential.Skip) + { + // Load the credentials and record error messages in case we need to fail at the end + try + { + + await _credentialsLoader.LoadCredentialsIfNeededAsync(credential, credentialSourceLoaderParameters); + } + catch (Exception ex) + { + LogMessages.AttemptToLoadCredentialsFailed(_logger, credential, ex); + errorMessage += $"Credential {credential.Id} failed because: {ex} \n"; + } + + if (credential.CredentialType == CredentialType.SignedAssertion) + { + if (credential.SourceType == CredentialSource.SignedAssertionFromManagedIdentity) + { + if (credential.Skip) + { + LogMessages.NotUsingManagedIdentity(_logger, errorMessage); + } + else + { + LogMessages.UsingManagedIdentity(_logger); + return credential; + } + } + if (credential.SourceType == CredentialSource.SignedAssertionFilePath) + { + if (!credential.Skip) + { + LogMessages.UsingPodIdentityFile(_logger, credential.SignedAssertionFileDiskPath ?? "not found"); + return credential; + } + } + if (credential.SourceType == CredentialSource.SignedAssertionFromVault) + { + if (!credential.Skip) + { + LogMessages.UsingSignedAssertionFromVault(_logger, credential.KeyVaultUrl ?? "undefined"); + return credential; + } + } + if (credential.SourceType == CredentialSource.CustomSignedAssertion) + { + if (!credential.Skip) + { + LogMessages.UsingSignedAssertionFromCustomProvider(_logger, credential.CustomSignedAssertionProviderName ?? "undefined"); + return credential; + } + } + } + + if (credential.CredentialType == CredentialType.Certificate) + { + var certificate = credential.Certificate; + if (certificate != null) + { + LogMessages.UsingCertThumbprint(_logger, certificate.Thumbprint); + NotifyCertificateAction( + credentialSourceLoaderParameters, + credential, + certificate, + CerticateObserverAction.Selected, + null); + return credential; + } + } + + if (credential.CredentialType == CredentialType.Secret) + { + return credential; + } + } + } + + if (clientCredentials.Any(c => c.CredentialType == CredentialType.Certificate || c.CredentialType == CredentialType.SignedAssertion)) + { + throw new ArgumentException( + IDWebErrorMessage.ClientCertificatesHaveExpiredOrCannotBeLoaded + errorMessage, + nameof(clientCredentials)); + } + + _logger.LogInformation($"No client credential could be used. Secret may have been defined elsewhere. " + + $"Count {(clientCredentials != null ? clientCredentials.Count() : 0)} "); + + return null; + } + + public void NotifyCertificateUsed( + CredentialSourceLoaderParameters? credentialSourceLoaderParameters, + CredentialDescription certificateDescription, + X509Certificate2 certificate, + bool successful, + Exception? exception) + { + CerticateObserverAction action = successful ? CerticateObserverAction.SuccessfullyUsed : CerticateObserverAction.Deselected; + NotifyCertificateAction(credentialSourceLoaderParameters, certificateDescription, certificate, action, exception); + } + + private void NotifyCertificateAction( + CredentialSourceLoaderParameters? sourceLoaderParameters, + CredentialDescription certificateDescription, + X509Certificate2 certificate, + CerticateObserverAction action, + Exception? exception) + { + for (int i = 0; i < _certificatesObservers.Count; i++) + { + _certificatesObservers[i].OnClientCertificateChanged( + new CertificateChangeEventArg() + { + Action = action, + Certificate = certificate, + CredentialDescription = certificateDescription, + CredentialSourceLoaderParameters = sourceLoaderParameters, + ThrownException = exception, + }); + } + + // If deselected, clear the values so they can be reloaded. + if (action == CerticateObserverAction.Deselected) + { + certificateDescription.Certificate = null; + certificateDescription.CachedValue = null; + } + } + } +} diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/GlobalSuppressions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/GlobalSuppressions.cs index 5fee106fa..b4cf3d7dc 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/GlobalSuppressions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/GlobalSuppressions.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "This method has an async counterpart.", Scope = "member", Target = "~M:Microsoft.Identity.Web.TokenAcquisitionAspNetCore.ReplyForbiddenWithWwwAuthenticateHeader(System.Collections.Generic.IEnumerable{System.String},Microsoft.Identity.Client.MsalUiRequiredException,System.String,Microsoft.AspNetCore.Http.HttpResponse)")] -[assembly: SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "This method has an async counterpart.", Scope = "member", Target = "~M:Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentials(Microsoft.Identity.Client.ConfidentialClientApplicationBuilder,System.Collections.Generic.IEnumerable{Microsoft.Identity.Abstractions.CredentialDescription},Microsoft.Extensions.Logging.ILogger,Microsoft.Identity.Abstractions.ICredentialsLoader,Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters)~Microsoft.Identity.Client.ConfidentialClientApplicationBuilder")] [assembly: SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Existing public API", Scope = "member", Target = "~M:Microsoft.Identity.Web.ITokenAcquisition.GetAccessTokenForAppAsync(System.String,System.String,Microsoft.Identity.Web.TokenAcquisitionOptions)~System.Threading.Tasks.Task{System.String}")] [assembly: SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Existing public API", Scope = "member", Target = "~M:Microsoft.Identity.Web.ITokenAcquisition.GetAccessTokenForAppAsync(System.String,System.String,System.String,Microsoft.Identity.Web.TokenAcquisitionOptions)~System.Threading.Tasks.Task{System.String}")] [assembly: SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Existing public API", Scope = "member", Target = "~M:Microsoft.Identity.Web.ITokenAcquisition.GetAccessTokenForUserAsync(System.Collections.Generic.IEnumerable{System.String},System.String,System.String,System.Security.Claims.ClaimsPrincipal,Microsoft.Identity.Web.TokenAcquisitionOptions)~System.Threading.Tasks.Task{System.String}")] diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ICertificatesObserver.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ICertificatesObserver.cs index 616537bcd..fd08e2380 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ICertificatesObserver.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ICertificatesObserver.cs @@ -53,6 +53,11 @@ public class CertificateChangeEventArg /// public CredentialDescription? CredentialDescription { get; set; } + /// + /// If provided, the used to load the certificate. + /// + public CredentialSourceLoaderParameters? CredentialSourceLoaderParameters { get; set; } + /// /// Gets the exception thrown during the certificate selection or deselection. /// diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ICredentialsProvider.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ICredentialsProvider.cs new file mode 100644 index 000000000..73c9cf92d --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ICredentialsProvider.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Abstractions; + +namespace Microsoft.Identity.Web +{ + /// + /// Credential provider class. Provides access to configured credentials and observers. + /// + public interface ICredentialsProvider + { + /// + /// Gets a credential to be used for authentication, based on the provided credential descriptions. + /// The provider may choose to return a credential based on any of the provided descriptions, and is not required to return a credential for each description. + /// The provider may also choose to return null, in which case the system will attempt to authenticate without client credentials, if applicable. + /// + /// Parameters to use for credential selection. + /// The cancellation token. + /// A matching and loaded credential, if any are applicable. + public Task GetCredentialAsync( + CredentialSourceLoaderParameters? credentialSourceLoaderParameters, + CancellationToken cancellationToken); + + /// + /// Gets a credential to be used for authentication, based on the provided credential descriptions. + /// The provider may choose to return a credential based on any of the provided descriptions, and is not required to return a credential for each description. + /// The provider may also choose to return null, in which case the system will attempt to authenticate without client credentials, if applicable. + /// + /// The merged options to use to select a certificate from. + /// Parameters to use for credential selection. + /// The cancellation token. + /// A matching and loaded credential, if any are applicable. + internal Task GetCredentialAsync( + MergedOptions mergedOptions, + CredentialSourceLoaderParameters? credentialSourceLoaderParameters, + CancellationToken cancellationToken); + + /// + /// Notifies that a certificate was used. + /// + /// The source loader parameters. + /// The description of the certificate. + /// The certificate, distinct from the description in case the certificate value has changed. + /// Whether he usage was successful or a failure. + /// The exception, if applicable. + public void NotifyCertificateUsed( + CredentialSourceLoaderParameters? sourceLoaderParameters, + CredentialDescription certificateDescription, + X509Certificate2 certificate, + bool successful, + Exception? exception); + } +} diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs b/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs index 56ee6fb43..de6328687 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs @@ -76,7 +76,5 @@ internal static class IDWebErrorMessage public const string InitializeAsyncIsObsolete = "IDW10801: Use Initialize instead. See https://aka.ms/ms-id-web/1.9.0. "; public const string FromStoreWithThumprintIsObsolete = "IDW10803: Use FromStoreWithThumbprint instead, due to spelling error. "; public const string AadIssuerValidatorIsObsolete = "IDW10804: Use MicrosoftIdentityIssuerValidator. "; - - public const string WithClientCredentialsIsObsolete = "Use WithClientCredentialsAsync instead."; } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ProtocolNames.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ProtocolNames.cs new file mode 100644 index 000000000..4dff7289e --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ProtocolNames.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Identity.Web +{ + internal static class ProtocolNames + { + public const string Bearer = "Bearer"; + + public const string MtlsPop = "MTLS_POP"; + + public const string Mtls = "MTLS"; + } +} diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Shipped.txt index 284c86210..ffaf258d1 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Shipped.txt @@ -111,7 +111,6 @@ const Microsoft.Identity.Web.IDWebErrorMessage.TenantIdClaimNotPresentInToken = const Microsoft.Identity.Web.IDWebErrorMessage.TokenBindingRequiresEnabledAppTokenAcquisition = "IDW10116: Token binding requires enabled app token acquisition." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TokenIsNotJwtToken = "IDW10403: Token is not a JWT token. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.UnauthenticatedUser = "IDW10204: The user is unauthenticated. The HttpContext does not contain any claims. " -> string! -const Microsoft.Identity.Web.IDWebErrorMessage.WithClientCredentialsIsObsolete = "Use WithClientCredentialsAsync instead." -> string! const Microsoft.Identity.Web.LogMessages.ErrorAcquiringTokenForDownstreamWebApi = "Error acquiring a token for a downstream web API - MsalUiRequiredException message is: " -> string! const Microsoft.Identity.Web.LogMessages.ExceptionOccurredWhenAddingAnAccountToTheCacheFromAuthCode = "Exception occurred while adding an account to the cache from the auth code. " -> string! const Microsoft.Identity.Web.LogMessages.MethodBegin = "Begin {0}. " -> string! @@ -312,9 +311,6 @@ Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcqui Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUserAsync(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.Util.Base64UrlHelpers -readonly Microsoft.Identity.Web.TokenAcquisition._certificatesObserver -> Microsoft.Identity.Web.Experimental.ICertificatesObserver? -readonly Microsoft.Identity.Web.TokenAcquisition._certificatesObservers -> System.Collections.Generic.IReadOnlyList! -readonly Microsoft.Identity.Web.TokenAcquisition._credentialsLoader -> Microsoft.Identity.Abstractions.ICredentialsLoader! readonly Microsoft.Identity.Web.TokenAcquisition._httpClientFactory -> Microsoft.Identity.Client.IMsalHttpClientFactory! readonly Microsoft.Identity.Web.TokenAcquisition._logger -> Microsoft.Extensions.Logging.ILogger! readonly Microsoft.Identity.Web.TokenAcquisition._serviceProvider -> System.IServiceProvider! @@ -335,8 +331,6 @@ static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logg static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingPodIdentityFile(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionFileDiskPath) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromVault(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentials(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters! credentialSourceLoaderParameters) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.GetKey(string? authority, string? clientId, string? region) -> string! static Microsoft.Identity.Web.HttpContextExtensions.GetTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.HttpContextExtensions.StoreTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.IdentityModel.Tokens.SecurityToken? token) -> void diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt index 7dc5c5811..30035ea1d 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt @@ -1 +1,15 @@ #nullable enable +Microsoft.Identity.Web.CredentialsProvider +Microsoft.Identity.Web.CredentialsProvider.CredentialsProvider(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, System.Collections.Generic.IEnumerable! certificatesObservers, Microsoft.Identity.Web.ITokenAcquisitionHost? tokenHost = null) -> void +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! options, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.TokenAcquisition(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.TokenAcquisitionAspNetCore.TokenAcquisitionAspNetCore(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.IServiceProvider! serviceProvider) -> void +readonly Microsoft.Identity.Web.TokenAcquisition._credentialsProvider -> Microsoft.Identity.Web.ICredentialsProvider! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ProtocolNames +const Microsoft.Identity.Web.ProtocolNames.Bearer = "Bearer" -> string! +const Microsoft.Identity.Web.ProtocolNames.MtlsPop = "MTLS_POP" -> string! +const Microsoft.Identity.Web.ProtocolNames.Mtls = "MTLS" -> string! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt index c12c7d6e4..9897c9040 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1,3 +1,8 @@ #nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt index 7dc5c5811..30035ea1d 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt @@ -1 +1,15 @@ #nullable enable +Microsoft.Identity.Web.CredentialsProvider +Microsoft.Identity.Web.CredentialsProvider.CredentialsProvider(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, System.Collections.Generic.IEnumerable! certificatesObservers, Microsoft.Identity.Web.ITokenAcquisitionHost? tokenHost = null) -> void +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! options, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.TokenAcquisition(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.TokenAcquisitionAspNetCore.TokenAcquisitionAspNetCore(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.IServiceProvider! serviceProvider) -> void +readonly Microsoft.Identity.Web.TokenAcquisition._credentialsProvider -> Microsoft.Identity.Web.ICredentialsProvider! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ProtocolNames +const Microsoft.Identity.Web.ProtocolNames.Bearer = "Bearer" -> string! +const Microsoft.Identity.Web.ProtocolNames.MtlsPop = "MTLS_POP" -> string! +const Microsoft.Identity.Web.ProtocolNames.Mtls = "MTLS" -> string! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt index c12c7d6e4..9897c9040 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1,3 +1,8 @@ #nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt index 7dc5c5811..30035ea1d 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt @@ -1 +1,15 @@ #nullable enable +Microsoft.Identity.Web.CredentialsProvider +Microsoft.Identity.Web.CredentialsProvider.CredentialsProvider(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, System.Collections.Generic.IEnumerable! certificatesObservers, Microsoft.Identity.Web.ITokenAcquisitionHost? tokenHost = null) -> void +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! options, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.TokenAcquisition(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.TokenAcquisitionAspNetCore.TokenAcquisitionAspNetCore(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.IServiceProvider! serviceProvider) -> void +readonly Microsoft.Identity.Web.TokenAcquisition._credentialsProvider -> Microsoft.Identity.Web.ICredentialsProvider! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ProtocolNames +const Microsoft.Identity.Web.ProtocolNames.Bearer = "Bearer" -> string! +const Microsoft.Identity.Web.ProtocolNames.MtlsPop = "MTLS_POP" -> string! +const Microsoft.Identity.Web.ProtocolNames.Mtls = "MTLS" -> string! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt index c12c7d6e4..9897c9040 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1,3 +1,8 @@ #nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Shipped.txt index 1edc5a028..ffaf258d1 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Shipped.txt @@ -111,7 +111,6 @@ const Microsoft.Identity.Web.IDWebErrorMessage.TenantIdClaimNotPresentInToken = const Microsoft.Identity.Web.IDWebErrorMessage.TokenBindingRequiresEnabledAppTokenAcquisition = "IDW10116: Token binding requires enabled app token acquisition." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TokenIsNotJwtToken = "IDW10403: Token is not a JWT token. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.UnauthenticatedUser = "IDW10204: The user is unauthenticated. The HttpContext does not contain any claims. " -> string! -const Microsoft.Identity.Web.IDWebErrorMessage.WithClientCredentialsIsObsolete = "Use WithClientCredentialsAsync instead." -> string! const Microsoft.Identity.Web.LogMessages.ErrorAcquiringTokenForDownstreamWebApi = "Error acquiring a token for a downstream web API - MsalUiRequiredException message is: " -> string! const Microsoft.Identity.Web.LogMessages.ExceptionOccurredWhenAddingAnAccountToTheCacheFromAuthCode = "Exception occurred while adding an account to the cache from the auth code. " -> string! const Microsoft.Identity.Web.LogMessages.MethodBegin = "Begin {0}. " -> string! @@ -258,6 +257,9 @@ Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.Micr Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger.MicrosoftIdentityApplicationOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptions) -> void Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger.PostConfigure(string? name, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> void +Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger +Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptionsStore) -> void +Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger.PostConfigure(string? name, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> void Microsoft.Identity.Web.MicrosoftIdentityOptions.HasClientCredentials.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptions.IsB2C.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptionsMerger @@ -303,13 +305,12 @@ Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.TokenAcquisitionAspnetCore Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeOnBehalfOfInitializedAsync(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUserAsync(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.Util.Base64UrlHelpers -readonly Microsoft.Identity.Web.TokenAcquisition._certificatesObserver -> Microsoft.Identity.Web.Experimental.ICertificatesObserver? -readonly Microsoft.Identity.Web.TokenAcquisition._certificatesObservers -> System.Collections.Generic.IReadOnlyList! -readonly Microsoft.Identity.Web.TokenAcquisition._credentialsLoader -> Microsoft.Identity.Abstractions.ICredentialsLoader! readonly Microsoft.Identity.Web.TokenAcquisition._httpClientFactory -> Microsoft.Identity.Client.IMsalHttpClientFactory! readonly Microsoft.Identity.Web.TokenAcquisition._logger -> Microsoft.Extensions.Logging.ILogger! readonly Microsoft.Identity.Web.TokenAcquisition._serviceProvider -> System.IServiceProvider! @@ -330,8 +331,6 @@ static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logg static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingPodIdentityFile(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionFileDiskPath) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromVault(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentials(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters! credentialSourceLoaderParameters) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.GetKey(string? authority, string? clientId, string? region) -> string! static Microsoft.Identity.Web.HttpContextExtensions.GetTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.HttpContextExtensions.StoreTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.IdentityModel.Tokens.SecurityToken? token) -> void diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt index 7dc5c5811..30035ea1d 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -1 +1,15 @@ #nullable enable +Microsoft.Identity.Web.CredentialsProvider +Microsoft.Identity.Web.CredentialsProvider.CredentialsProvider(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, System.Collections.Generic.IEnumerable! certificatesObservers, Microsoft.Identity.Web.ITokenAcquisitionHost? tokenHost = null) -> void +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! options, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.TokenAcquisition(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.TokenAcquisitionAspNetCore.TokenAcquisitionAspNetCore(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.IServiceProvider! serviceProvider) -> void +readonly Microsoft.Identity.Web.TokenAcquisition._credentialsProvider -> Microsoft.Identity.Web.ICredentialsProvider! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ProtocolNames +const Microsoft.Identity.Web.ProtocolNames.Bearer = "Bearer" -> string! +const Microsoft.Identity.Web.ProtocolNames.MtlsPop = "MTLS_POP" -> string! +const Microsoft.Identity.Web.ProtocolNames.Mtls = "MTLS" -> string! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt index c12c7d6e4..9897c9040 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,3 +1,8 @@ #nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Shipped.txt index ec23fff4b..ffaf258d1 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Shipped.txt @@ -111,7 +111,6 @@ const Microsoft.Identity.Web.IDWebErrorMessage.TenantIdClaimNotPresentInToken = const Microsoft.Identity.Web.IDWebErrorMessage.TokenBindingRequiresEnabledAppTokenAcquisition = "IDW10116: Token binding requires enabled app token acquisition." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TokenIsNotJwtToken = "IDW10403: Token is not a JWT token. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.UnauthenticatedUser = "IDW10204: The user is unauthenticated. The HttpContext does not contain any claims. " -> string! -const Microsoft.Identity.Web.IDWebErrorMessage.WithClientCredentialsIsObsolete = "Use WithClientCredentialsAsync instead." -> string! const Microsoft.Identity.Web.LogMessages.ErrorAcquiringTokenForDownstreamWebApi = "Error acquiring a token for a downstream web API - MsalUiRequiredException message is: " -> string! const Microsoft.Identity.Web.LogMessages.ExceptionOccurredWhenAddingAnAccountToTheCacheFromAuthCode = "Exception occurred while adding an account to the cache from the auth code. " -> string! const Microsoft.Identity.Web.LogMessages.MethodBegin = "Begin {0}. " -> string! @@ -188,7 +187,6 @@ Microsoft.Identity.Web.IdHelper Microsoft.Identity.Web.IDWebErrorMessage Microsoft.Identity.Web.IMergedOptionsStore Microsoft.Identity.Web.IMergedOptionsStore.Get(string! name) -> Microsoft.Identity.Web.MergedOptions! -Microsoft.Identity.Web.Internal.MicrosoftIdentityOptionsBinder Microsoft.Identity.Web.ITokenAcquisitionHost Microsoft.Identity.Web.ITokenAcquisitionHost.GetAuthenticatedUserAsync(System.Security.Claims.ClaimsPrincipal? user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.ITokenAcquisitionHost.GetCurrentRedirectUri(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? @@ -259,6 +257,9 @@ Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.Micr Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger.MicrosoftIdentityApplicationOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptions) -> void Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger.PostConfigure(string? name, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> void +Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger +Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptionsStore) -> void +Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger.PostConfigure(string? name, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> void Microsoft.Identity.Web.MicrosoftIdentityOptions.HasClientCredentials.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptions.IsB2C.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptionsMerger @@ -310,9 +311,6 @@ Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcqui Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUserAsync(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.Util.Base64UrlHelpers -readonly Microsoft.Identity.Web.TokenAcquisition._certificatesObserver -> Microsoft.Identity.Web.Experimental.ICertificatesObserver? -readonly Microsoft.Identity.Web.TokenAcquisition._certificatesObservers -> System.Collections.Generic.IReadOnlyList! -readonly Microsoft.Identity.Web.TokenAcquisition._credentialsLoader -> Microsoft.Identity.Abstractions.ICredentialsLoader! readonly Microsoft.Identity.Web.TokenAcquisition._httpClientFactory -> Microsoft.Identity.Client.IMsalHttpClientFactory! readonly Microsoft.Identity.Web.TokenAcquisition._logger -> Microsoft.Extensions.Logging.ILogger! readonly Microsoft.Identity.Web.TokenAcquisition._serviceProvider -> System.IServiceProvider! @@ -333,8 +331,6 @@ static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logg static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingPodIdentityFile(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionFileDiskPath) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromVault(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentials(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters! credentialSourceLoaderParameters) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.GetKey(string? authority, string? clientId, string? region) -> string! static Microsoft.Identity.Web.HttpContextExtensions.GetTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.HttpContextExtensions.StoreTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.IdentityModel.Tokens.SecurityToken? token) -> void diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt index 7dc5c5811..30035ea1d 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt @@ -1 +1,15 @@ #nullable enable +Microsoft.Identity.Web.CredentialsProvider +Microsoft.Identity.Web.CredentialsProvider.CredentialsProvider(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, System.Collections.Generic.IEnumerable! certificatesObservers, Microsoft.Identity.Web.ITokenAcquisitionHost? tokenHost = null) -> void +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! options, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.TokenAcquisition(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.TokenAcquisitionAspNetCore.TokenAcquisitionAspNetCore(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.IServiceProvider! serviceProvider) -> void +readonly Microsoft.Identity.Web.TokenAcquisition._credentialsProvider -> Microsoft.Identity.Web.ICredentialsProvider! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ProtocolNames +const Microsoft.Identity.Web.ProtocolNames.Bearer = "Bearer" -> string! +const Microsoft.Identity.Web.ProtocolNames.MtlsPop = "MTLS_POP" -> string! +const Microsoft.Identity.Web.ProtocolNames.Mtls = "MTLS" -> string! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt index c12c7d6e4..9897c9040 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1,3 +1,8 @@ #nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt index 7dc5c5811..30035ea1d 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt @@ -1 +1,15 @@ #nullable enable +Microsoft.Identity.Web.CredentialsProvider +Microsoft.Identity.Web.CredentialsProvider.CredentialsProvider(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, System.Collections.Generic.IEnumerable! certificatesObservers, Microsoft.Identity.Web.ITokenAcquisitionHost? tokenHost = null) -> void +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! options, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.CredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.TokenAcquisition(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.TokenAcquisitionAspNetCore.TokenAcquisitionAspNetCore(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.IServiceProvider! serviceProvider) -> void +readonly Microsoft.Identity.Web.TokenAcquisition._credentialsProvider -> Microsoft.Identity.Web.ICredentialsProvider! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.ICredentialsProvider! credentialsProvider, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ProtocolNames +const Microsoft.Identity.Web.ProtocolNames.Bearer = "Bearer" -> string! +const Microsoft.Identity.Web.ProtocolNames.MtlsPop = "MTLS_POP" -> string! +const Microsoft.Identity.Web.ProtocolNames.Mtls = "MTLS" -> string! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index c12c7d6e4..9897c9040 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,8 @@ #nullable enable +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.get -> Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? +Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.CredentialSourceLoaderParameters.set -> void +Microsoft.Identity.Web.ICredentialsProvider +Microsoft.Identity.Web.ICredentialsProvider.GetCredentialAsync(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.ICredentialsProvider.NotifyCertificateUsed(Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? sourceLoaderParameters, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription, System.Security.Cryptography.X509Certificates.X509Certificate2! certificate, bool successful, System.Exception? exception) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions, Microsoft.Identity.Client.IMsalMtlsHttpClientFactory? mtlsHttpClientFactory, Microsoft.Extensions.Logging.ILogger? logger = null) -> void Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions(Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! other) -> void diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs index 49c8a50fc..dee774478 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs @@ -46,11 +46,21 @@ public static IServiceCollection AddTokenAcquisition( bool forceSdk = !services.Any(s => s.ServiceType.FullName == "Microsoft.AspNetCore.Authentication.IAuthenticationService"); #endif + if (!HasImplementationType(services, typeof(CredentialsProvider))) + { + services.TryAddSingleton(); + } + if (!HasImplementationType(services, typeof(DefaultCertificateLoader))) { services.TryAddSingleton(); } + if (!HasImplementationType(services, typeof(MsalMtlsHttpClientFactory))) + { + services.TryAddSingleton(); + } + if (!HasImplementationType(services, typeof(MicrosoftIdentityOptionsMerger))) { services.TryAddSingleton, MicrosoftIdentityOptionsMerger>(); diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index a0f4cf5d0..2dd191edb 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -59,13 +59,9 @@ class OAuthConstants protected readonly ILogger _logger; protected readonly IServiceProvider _serviceProvider; protected readonly ITokenAcquisitionHost _tokenAcquisitionHost; - protected readonly ICredentialsLoader _credentialsLoader; - protected readonly IReadOnlyList _certificatesObservers; + protected readonly ICredentialsProvider _credentialsProvider; protected readonly IOptionsMonitor? tokenAcquisitionExtensionOptionsMonitor; - [Obsolete("Use _certificatesObservers instead.")] - protected readonly ICertificatesObserver? _certificatesObserver; - /// /// Scopes which are already requested by MSAL.NET. They should not be re-requested;. /// @@ -96,27 +92,29 @@ class OAuthConstants /// HTTP client factory. /// Logger. /// Service provider. - /// Credential loader used to provide the credentials. public TokenAcquisition( IMsalTokenCacheProvider tokenCacheProvider, ITokenAcquisitionHost tokenAcquisitionHost, IHttpClientFactory httpClientFactory, ILogger logger, - IServiceProvider serviceProvider, - ICredentialsLoader credentialsLoader) + IServiceProvider serviceProvider) { _tokenCacheProvider = tokenCacheProvider; _httpClientFactory = serviceProvider.GetService() ?? new MsalMtlsHttpClientFactory(httpClientFactory); _logger = logger; _serviceProvider = serviceProvider; _tokenAcquisitionHost = tokenAcquisitionHost; - _credentialsLoader = credentialsLoader; - _certificatesObservers = [.. serviceProvider.GetServices()]; -#pragma warning disable CS0618 // Type or member is obsolete. Setup for backward compatibility. - _certificatesObserver = serviceProvider.GetService(); -#pragma warning restore CS0618 // Type or member is obsolete tokenAcquisitionExtensionOptionsMonitor = serviceProvider.GetService>(); _miHttpFactory = serviceProvider.GetService(); + + var credentialsProvider = serviceProvider.GetService(); + if (credentialsProvider == null) + { + var credentialsLoader = serviceProvider.GetService() ?? throw new InvalidOperationException("Either ICredentialsProvider or ICredentialsLoader must be registered and neither were."); + credentialsProvider = new CredentialsProvider(new LogAdapter(logger), credentialsLoader, [.. serviceProvider.GetServices()], tokenAcquisitionHost); + } + + _credentialsProvider = credentialsProvider; } #if NET6_0_OR_GREATER @@ -135,6 +133,7 @@ private async Task AddAccountToCacheFromAuthorizationCodeInt _ = Throws.IfNull(authCodeRedemptionParameters.Scopes); MergedOptions mergedOptions = _tokenAcquisitionHost.GetOptions(authCodeRedemptionParameters.AuthenticationScheme, out string effectiveAuthenticationScheme); + CredentialSourceLoaderParameters? loaderParameters = null; IConfidentialClientApplication? application = null; try { @@ -161,6 +160,11 @@ private async Task AddAccountToCacheFromAuthorizationCodeInt .WithCcsRoutingHint(backUpAuthRoutingHint) .WithSpaAuthorizationCode(mergedOptions.WithSpaAuthCode); + loaderParameters = new CredentialSourceLoaderParameters(application.AppConfig.ClientId, application.Authority) + { + Protocol = ProtocolNames.Bearer, + }; + if (mergedOptions.ExtraQueryParameters != null) { builder.WithExtraQueryParameters(MergeExtraQueryParameters(mergedOptions, null)); @@ -186,7 +190,12 @@ private async Task AddAccountToCacheFromAuthorizationCodeInt _tokenAcquisitionHost.SetSession(Constants.SpaAuthCode, result.SpaAuthCode); } - NotifyCertificateSelection(mergedOptions, application, CerticateObserverAction.SuccessfullyUsed, null); + NotifyCertificateSelection( + loaderParameters, + mergedOptions, + application, + true, + null); return new AcquireTokenResult( result.AccessToken, @@ -205,8 +214,7 @@ private async Task AddAccountToCacheFromAuthorizationCodeInt exMsal); string applicationKey = GetApplicationKey(mergedOptions); - NotifyCertificateSelection(mergedOptions, application!, CerticateObserverAction.Deselected, exMsal); - DefaultCertificateLoader.ResetCertificates(mergedOptions.ClientCredentials); + NotifyCertificateSelection(loaderParameters, mergedOptions, application!, false, exMsal); _applicationsByAuthorityClientId[applicationKey] = null; // Retry with incremented counter @@ -296,6 +304,11 @@ private async Task GetAuthenticationResultForUserInternalA tokenAcquisitionOptions.ExtraParameters[Constants.ExtensionOptionsServiceProviderKey] = _serviceProvider; } + CredentialSourceLoaderParameters loaderParameters = new CredentialSourceLoaderParameters(application.AppConfig.ClientId, application.Authority) + { + Protocol = ProtocolNames.Bearer, + }; + try { AuthenticationResult? authenticationResult; @@ -351,8 +364,7 @@ private async Task GetAuthenticationResultForUserInternalA exMsal); string applicationKey = GetApplicationKey(mergedOptions); - NotifyCertificateSelection(mergedOptions, application, CerticateObserverAction.Deselected, exMsal); - DefaultCertificateLoader.ResetCertificates(mergedOptions.ClientCredentials); + NotifyCertificateSelection(loaderParameters, mergedOptions, application, false, exMsal); _applicationsByAuthorityClientId[applicationKey] = null; // Retry with incremented counter @@ -752,7 +764,17 @@ private async Task GetAuthenticationResultForAppInternalAs try { var result = await builder.ExecuteAsync(tokenAcquisitionOptions != null ? tokenAcquisitionOptions.CancellationToken : CancellationToken.None); - NotifyCertificateSelection(mergedOptions, application, CerticateObserverAction.SuccessfullyUsed, null); + NotifyCertificateSelection( + new CredentialSourceLoaderParameters( + mergedOptions.ClientId ?? string.Empty, + mergedOptions.Authority ?? string.Empty) + { + Protocol = isTokenBinding ? ProtocolNames.MtlsPop : ProtocolNames.Bearer, + }, + mergedOptions, + application, + true, + null); return result; } catch (MsalServiceException exMsal) when (retryCount < MaxCertificateRetries && IsInvalidClientCertificateOrSignedAssertionError(exMsal)) @@ -763,8 +785,17 @@ private async Task GetAuthenticationResultForAppInternalAs exMsal); string applicationKey = GetApplicationKey(mergedOptions); - NotifyCertificateSelection(mergedOptions, application, CerticateObserverAction.Deselected, exMsal); - DefaultCertificateLoader.ResetCertificates(mergedOptions.ClientCredentials); + NotifyCertificateSelection( + new CredentialSourceLoaderParameters( + mergedOptions.ClientId ?? string.Empty, + mergedOptions.Authority ?? string.Empty) + { + Protocol = isTokenBinding ? ProtocolNames.MtlsPop : ProtocolNames.Bearer, + }, + mergedOptions, + application, + false, + exMsal); _applicationsByAuthorityClientId[applicationKey] = null; // Retry with incremented counter @@ -1141,10 +1172,12 @@ private async Task BuildConfidentialClientApplic try { await builder.WithClientCredentialsAsync( - mergedOptions.ClientCredentials!, - _logger, - _credentialsLoader, - new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority), + mergedOptions, + _credentialsProvider, + new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority) + { + Protocol = isTokenBinding ? ProtocolNames.MtlsPop : ProtocolNames.Bearer, + }, isTokenBinding); } catch (ArgumentException ex) when (ex.Message == IDWebErrorMessage.ClientCertificatesHaveExpiredOrCannotBeLoaded) @@ -1158,10 +1191,6 @@ await builder.WithClientCredentialsAsync( IConfidentialClientApplication app = builder.Build(); - // If the client application has set certificate observer, - // fire the event to notify the client app that a certificate was selected. - NotifyCertificateSelection(mergedOptions, app, CerticateObserverAction.Selected, null); - // Initialize token cache providers if (!(_tokenCacheProvider is MsalMemoryTokenCacheProvider)) { @@ -1184,30 +1213,28 @@ await builder.WithClientCredentialsAsync( /// /// Find the certificate used by the app and fire the event to notify the client app that a certificate was selected/unselected. /// - /// - /// - /// + /// The source loader parameters. + /// The merged options object. + /// The confidential app. + /// Whether this was successful or not. /// The thrown exception, if any. private void NotifyCertificateSelection( + CredentialSourceLoaderParameters? sourceLoaderParameters, MergedOptions mergedOptions, IConfidentialClientApplication app, - CerticateObserverAction action, + bool successful, Exception? exception) { X509Certificate2 selectedCertificate = app.AppConfig.ClientCredentialCertificate; - if (selectedCertificate != null) + CredentialDescription? description = mergedOptions.ClientCredentials?.FirstOrDefault(c => c.Certificate == selectedCertificate); + if (selectedCertificate != null && description != null) { - for (int i = 0; i < _certificatesObservers.Count; i++) - { - _certificatesObservers[i].OnClientCertificateChanged( - new CertificateChangeEventArg() - { - Action = action, - Certificate = app.AppConfig.ClientCredentialCertificate, - CredentialDescription = mergedOptions.ClientCredentials?.FirstOrDefault(c => c.Certificate == selectedCertificate), - ThrownException = exception, - }); - } + _credentialsProvider.NotifyCertificateUsed( + sourceLoaderParameters, + description, + selectedCertificate, + successful, + exception); } } @@ -1696,5 +1723,15 @@ clientAssertionObj is string clientAssertion && return hasClientAssertion; } + + // Used for backcompat support. + private class LogAdapter(ILogger innerLogger) : ILogger + { + public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) => innerLogger.IsEnabled(logLevel); + public void Log(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => + innerLogger.Log(logLevel, eventId, state, exception, formatter); + IDisposable ILogger.BeginScope(TState state) => + innerLogger.BeginScope(state)!; + } } } diff --git a/tests/DevApps/Mtls/MtlsClient/MtlsClient.csproj b/tests/DevApps/Mtls/MtlsClient/MtlsClient.csproj new file mode 100644 index 000000000..3cf5cd8a6 --- /dev/null +++ b/tests/DevApps/Mtls/MtlsClient/MtlsClient.csproj @@ -0,0 +1,30 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/tests/DevApps/Mtls/MtlsClient/Program.cs b/tests/DevApps/Mtls/MtlsClient/Program.cs new file mode 100644 index 000000000..a9c741133 --- /dev/null +++ b/tests/DevApps/Mtls/MtlsClient/Program.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +namespace MtlsSample +{ + public class Program + { + public static async Task Main(string[] args) + { + var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + + tokenAcquirerFactory.Services.AddLogging(builder => + builder.AddConsole().SetMinimumLevel(LogLevel.Information)); + + tokenAcquirerFactory.Services.AddDownstreamApi("WebApi", + tokenAcquirerFactory.Configuration.GetSection("WebApi")); + + var sp = tokenAcquirerFactory.Build(); + + Console.WriteLine("Calling web API with pure mTLS..."); + var webApi = sp.GetRequiredService(); + var result = await webApi.GetForAppAsync>("WebApi").ConfigureAwait(false); + + Console.WriteLine("Web API result:"); + foreach (var forecast in result!) + { + Console.WriteLine($"{forecast.Date}: {forecast.Summary} - {forecast.TemperatureC}C/{forecast.TemperatureF}F"); + } + } + } +} diff --git a/tests/DevApps/Mtls/MtlsClient/WeatherForecast.cs b/tests/DevApps/Mtls/MtlsClient/WeatherForecast.cs new file mode 100644 index 000000000..341d0b438 --- /dev/null +++ b/tests/DevApps/Mtls/MtlsClient/WeatherForecast.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace MtlsSample +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/tests/DevApps/Mtls/MtlsClient/appsettings.json b/tests/DevApps/Mtls/MtlsClient/appsettings.json new file mode 100644 index 000000000..352831079 --- /dev/null +++ b/tests/DevApps/Mtls/MtlsClient/appsettings.json @@ -0,0 +1,21 @@ +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "bea21ebe-8b64-4d06-9f6d-6a889b120a7c", + "ClientId": "163ffef9-a313-45b4-ab2f-c7e2f5e0e23e", + "AzureRegion": "westus3", + "ClientCredentials": [ + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "CurrentUser/My", + "CertificateDistinguishedName": "CN=LabAuth.MSIDLab.com" + } + ], + "SendX5c": true + }, + "WebApi": { + "BaseUrl": "https://localhost:7060/", + "RelativePath": "WeatherForecast", + "ProtocolScheme": "MTLS" + } +} diff --git a/tests/DevApps/Mtls/MtlsWebApi/Controllers/WeatherForecastController.cs b/tests/DevApps/Mtls/MtlsWebApi/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..86c5c5422 --- /dev/null +++ b/tests/DevApps/Mtls/MtlsWebApi/Controllers/WeatherForecastController.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace MtlsSample.Controllers +{ + [ApiController] + [Route("[controller]")] + [Authorize(AuthenticationSchemes = MtlsAuthenticationHandler.ProtocolScheme)] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} diff --git a/tests/DevApps/Mtls/MtlsWebApi/MtlsAuthenticationHandler.cs b/tests/DevApps/Mtls/MtlsWebApi/MtlsAuthenticationHandler.cs new file mode 100644 index 000000000..7350daf9e --- /dev/null +++ b/tests/DevApps/Mtls/MtlsWebApi/MtlsAuthenticationHandler.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace MtlsSample +{ + public class MtlsAuthenticationHandler : AuthenticationHandler + { + public const string ProtocolScheme = "MTLS"; + + public MtlsAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + protected override async Task HandleAuthenticateAsync() + { + Logger.LogInformation("MtlsAuthenticationHandler invoked"); + + var certificate = await Request.HttpContext.Connection.GetClientCertificateAsync(); + + if (certificate == null) + { + Logger.LogWarning("No certificate found"); + return AuthenticateResult.NoResult(); + } + + if (!certificate.MatchesHostname("LabAuth.MSIDLab.com")) + { + Logger.LogWarning($"Certificate has wrong subject name: {certificate.Subject}"); + return AuthenticateResult.Fail($"Certificate has wrong subject name: {certificate.Subject}"); + } + + // Create claims principal from the certificate + var identity = new CaseSensitiveClaimsIdentity( + [new("SubjectName", certificate.GetNameInfo(X509NameType.SimpleName, false))], + ProtocolScheme); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, ProtocolScheme); + + return AuthenticateResult.Success(ticket); + } + } +} diff --git a/tests/DevApps/Mtls/MtlsWebApi/MtlsWebApi.csproj b/tests/DevApps/Mtls/MtlsWebApi/MtlsWebApi.csproj new file mode 100644 index 000000000..4434be346 --- /dev/null +++ b/tests/DevApps/Mtls/MtlsWebApi/MtlsWebApi.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/tests/DevApps/Mtls/MtlsWebApi/Program.cs b/tests/DevApps/Mtls/MtlsWebApi/Program.cs new file mode 100644 index 000000000..f92a5a5c0 --- /dev/null +++ b/tests/DevApps/Mtls/MtlsWebApi/Program.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Web; + +namespace MtlsSample +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddControllers(); + + // Learn more about configuring OpenAPI at https://learn.microsoft.com/aspnet/core/fundamentals/openapi/aspnetcore-openapi + builder.Services.AddEndpointsApiExplorer(); + + // Add custom MTLS_POP authentication handler + builder.Services.AddAuthentication() + .AddScheme( + MtlsAuthenticationHandler.ProtocolScheme, + options => {}); + + var app = builder.Build(); + + app.UseHttpsRedirection(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); + } + } +} diff --git a/tests/DevApps/Mtls/MtlsWebApi/Properties/launchSettings.json b/tests/DevApps/Mtls/MtlsWebApi/Properties/launchSettings.json new file mode 100644 index 000000000..d0f30c895 --- /dev/null +++ b/tests/DevApps/Mtls/MtlsWebApi/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7060", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/tests/DevApps/Mtls/MtlsWebApi/WeatherForecast.cs b/tests/DevApps/Mtls/MtlsWebApi/WeatherForecast.cs new file mode 100644 index 000000000..341d0b438 --- /dev/null +++ b/tests/DevApps/Mtls/MtlsWebApi/WeatherForecast.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace MtlsSample +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/tests/DevApps/Mtls/MtlsWebApi/appsettings.json b/tests/DevApps/Mtls/MtlsWebApi/appsettings.json new file mode 100644 index 000000000..1562bd980 --- /dev/null +++ b/tests/DevApps/Mtls/MtlsWebApi/appsettings.json @@ -0,0 +1,24 @@ +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a021aff4-57ad-453a-bae8-e4192e5860f3", + "Scopes": "https://graph.microsoft.com/.default" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "HttpsClientCert": { + "Url": "https://localhost:7060", + "ClientCertificateMode": "RequireCertificate", + "CheckCertificateRevocation": false + } + } + } +} diff --git a/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Properties/launchSettings.json b/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Properties/launchSettings.json new file mode 100644 index 000000000..07a1e5a97 --- /dev/null +++ b/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Microsoft.Identity.Web.AotCompatibility.TestApp": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:59646;http://localhost:59647" + } + } +} \ No newline at end of file diff --git a/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForAppIntegrationTests.cs b/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForAppIntegrationTests.cs index d3ec32825..7dfdda8e3 100644 --- a/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForAppIntegrationTests.cs +++ b/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForAppIntegrationTests.cs @@ -5,9 +5,11 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -15,16 +17,15 @@ using Microsoft.Extensions.Options; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Platforms.Features.DesktopOs.Kerberos; +using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Identity.Web.Test.Common; using Microsoft.Identity.Web.Test.Common.Mocks; using Microsoft.Identity.Web.Test.Common.TestHelpers; -using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Identity.Web.TokenCacheProviders.InMemory; +using Newtonsoft.Json.Linq; using Xunit; using Xunit.Abstractions; -using Microsoft.Identity.Client.Platforms.Features.DesktopOs.Kerberos; -using System.Threading; -using Microsoft.AspNetCore.Builder; namespace Microsoft.Identity.Web.Test.Integration { @@ -36,7 +37,7 @@ public class AcquireTokenForAppIntegrationTests private MsalTestTokenCacheProvider _msalTestTokenCacheProvider; private IOptionsMonitor _microsoftIdentityOptionsMonitor; private IOptionsMonitor _applicationOptionsMonitor; - private ICredentialsLoader _credentialsLoader; + private ICredentialsProvider _credentialsProvider; private readonly string _ccaSecret; private readonly ITestOutputHelper _output; @@ -297,8 +298,8 @@ public async Task GetAccessTokenForApp_ServiceProviderSetInExtraParameters() private void InitializeTokenAcquisitionObjects() { - _credentialsLoader = new DefaultCredentialsLoader(); MergedOptions mergedOptions = Provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); + MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); @@ -311,13 +312,18 @@ private void InitializeTokenAcquisitionObjects() MockHttpContextAccessor.CreateMockHttpContextAccessor(), Provider.GetService()!, Provider); + _credentialsProvider = new CredentialsProvider( + Provider.GetRequiredService>(), + new DefaultCredentialsLoader(), + [], + tokenAcquisitionAspnetCoreHost); _tokenAcquisition = new TokenAcquisitionAspNetCore( _msalTestTokenCacheProvider, Provider.GetService()!, Provider.GetService>()!, tokenAcquisitionAspnetCoreHost, - Provider, - _credentialsLoader); + Provider); + tokenAcquisitionAspnetCoreHost.GetOptions(OpenIdConnectDefaults.AuthenticationScheme, out _); } diff --git a/tests/Microsoft.Identity.Web.Test/Certificates/WithClientCredentialsTests.cs b/tests/Microsoft.Identity.Web.Test/Certificates/WithClientCredentialsTests.cs index 43a4691f5..e4003814a 100644 --- a/tests/Microsoft.Identity.Web.Test/Certificates/WithClientCredentialsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/Certificates/WithClientCredentialsTests.cs @@ -22,7 +22,7 @@ public class WithClientCredentialsTests public async Task FicFails_CertificateFallbackAsync() { // Arrange - ILogger logger = Substitute.For(); + var logger = Substitute.For>(); ICredentialsLoader credLoader = Substitute.For(); CredentialDescription[] credentialDescriptions = new[] @@ -60,14 +60,19 @@ public async Task FicFails_CertificateFallbackAsync() } }); - // Act -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. - CredentialDescription cd = await ConfidentialClientApplicationBuilderExtension.LoadCredentialForMsalOrFailAsync( - credentialDescriptions, + CredentialsProvider provider = new CredentialsProvider( logger, credLoader, + [], + null); + + // Act + CredentialDescription? cd = await provider.GetCredentialAsync( + new MergedOptions() + { + ClientCredentials = credentialDescriptions, + }, null); -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. Assert.Equal(credentialDescriptions[1], cd); } @@ -166,7 +171,7 @@ public async Task FailsForPodAndCert_ReturnsMeaningfulMessageAsync() private static async Task RunFailToLoadLogicAsync(IEnumerable credentialDescriptions) { // Arrange - var logger = Substitute.For(); + var logger = Substitute.For>(); ICredentialsLoader credLoader = Substitute.For(); // Mock the credential loader to fail to load any certificate @@ -180,12 +185,18 @@ private static async Task RunFailToLoadLogicAsync(IEnumerable(() => ConfidentialClientApplicationBuilderExtension.LoadCredentialForMsalOrFailAsync( - credentialDescriptions, + CredentialsProvider provider = new CredentialsProvider( logger, credLoader, + [], + null); + + // Act + var ex = await Assert.ThrowsAsync(() => provider.GetCredentialAsync( + new MergedOptions() + { + ClientCredentials = credentialDescriptions, + }, null)); // Assert @@ -202,7 +213,7 @@ private static async Task RunFailToLoadLogicAsync(IEnumerable(); + var logger = Substitute.For>(); var credLoader = Substitute.For(); var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithAuthority(TestConstants.AuthorityCommonTenant); @@ -230,9 +241,11 @@ public async Task WithBindingCertificateAsync_ValidCertificate_ReturnsOriginalBu // Act var result = await builder.WithClientCredentialsAsync( - new[] { credentialDescription }, - logger, - credLoader, + new MergedOptions() + { + ClientCredentials = new[] { credentialDescription }, + }, + new CredentialsProvider(logger, credLoader, [], null), credentialSourceLoaderParameters: null, isTokenBinding: true); @@ -246,7 +259,7 @@ public async Task WithBindingCertificateAsync_ValidCertificate_ReturnsOriginalBu public async Task WithBindingCertificateAsync_NoValidCredentials_ThrowsException() { // Arrange - var logger = Substitute.For(); + var logger = Substitute.For>(); var credLoader = Substitute.For(); var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithAuthority(TestConstants.AuthorityCommonTenant); @@ -267,13 +280,21 @@ public async Task WithBindingCertificateAsync_NoValidCredentials_ThrowsException return Task.CompletedTask; }); + CredentialsProvider provider = new CredentialsProvider( + logger, + credLoader, + [], + null); + // Act & Assert // This should throw because LoadCredentialForMsalOrFailAsync throws when no credentials can be loaded await Assert.ThrowsAsync( () => builder.WithClientCredentialsAsync( - new[] { credentialDescription }, - logger, - credLoader, + new MergedOptions() + { + ClientCredentials = [ credentialDescription ], + }, + provider, credentialSourceLoaderParameters: null, isTokenBinding: true)); } @@ -282,7 +303,7 @@ await Assert.ThrowsAsync( public async Task WithBindingCertificateAsync_CredentialWithoutCertificate_ThrowsException() { // Arrange - var logger = Substitute.For(); + var logger = Substitute.For>(); var credLoader = Substitute.For(); var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithAuthority(TestConstants.AuthorityCommonTenant); @@ -302,12 +323,20 @@ public async Task WithBindingCertificateAsync_CredentialWithoutCertificate_Throw return Task.CompletedTask; }); + CredentialsProvider provider = new CredentialsProvider( + logger, + credLoader, + [], + null); + // Act & Assert await Assert.ThrowsAsync( () => builder.WithClientCredentialsAsync( - new[] { credentialDescription }, - logger, - credLoader, + new MergedOptions() + { + ClientCredentials = [credentialDescription], + }, + provider, credentialSourceLoaderParameters: null, isTokenBinding: true)); } @@ -316,7 +345,7 @@ await Assert.ThrowsAsync( public async Task WithBindingCertificateAsync_CredentialLoadingFails_PropagatesException() { // Arrange - var logger = Substitute.For(); + var logger = Substitute.For>(); var credLoader = Substitute.For(); var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithAuthority(TestConstants.AuthorityCommonTenant); @@ -334,12 +363,20 @@ public async Task WithBindingCertificateAsync_CredentialLoadingFails_PropagatesE credLoader.LoadCredentialsIfNeededAsync(Arg.Any(), Arg.Any()) .ThrowsAsync(expectedException); + CredentialsProvider provider = new CredentialsProvider( + logger, + credLoader, + [], + null); + // Act & Assert var actualException = await Assert.ThrowsAsync( () => builder.WithClientCredentialsAsync( - new[] { credentialDescription }, - logger, - credLoader, + new MergedOptions() + { + ClientCredentials = [credentialDescription], + }, + provider, credentialSourceLoaderParameters: null, isTokenBinding: true)); @@ -351,17 +388,22 @@ public async Task WithBindingCertificateAsync_CredentialLoadingFails_PropagatesE public async Task WithBindingCertificateAsync_EmptyCredentialsList_ThrowsException() { // Arrange - var logger = Substitute.For(); + var logger = Substitute.For>(); var credLoader = Substitute.For(); var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithAuthority(TestConstants.AuthorityCommonTenant); + CredentialsProvider provider = new CredentialsProvider( + logger, + credLoader, + [], + null); + // Act & Assert await Assert.ThrowsAsync( () => builder.WithClientCredentialsAsync( - new CredentialDescription[0], - logger, - credLoader, + new MergedOptions(), + provider, credentialSourceLoaderParameters: null, isTokenBinding: true)); } @@ -370,7 +412,7 @@ await Assert.ThrowsAsync( public async Task WithBindingCertificateAsync_WithCredentialSourceLoaderParameters_PassesParametersCorrectly() { // Arrange - var logger = Substitute.For(); + var logger = Substitute.For>(); var credLoader = Substitute.For(); var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithAuthority(TestConstants.AuthorityCommonTenant); @@ -398,11 +440,19 @@ public async Task WithBindingCertificateAsync_WithCredentialSourceLoaderParamete return Task.CompletedTask; }); - // Act - var result = await builder.WithClientCredentialsAsync( - new[] { credentialDescription }, + CredentialsProvider provider = new CredentialsProvider( logger, credLoader, + [], + null); + + // Act + var result = await builder.WithClientCredentialsAsync( + new MergedOptions() + { + ClientCredentials = [credentialDescription], + }, + provider, credentialSourceLoaderParameters, isTokenBinding: true); diff --git a/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs b/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs index 353dc5283..cf06f5599 100644 --- a/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs @@ -13,7 +13,9 @@ using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; using Microsoft.Identity.Web.Experimental; using Xunit; @@ -22,7 +24,7 @@ namespace Microsoft.Identity.Web.Test public class CertificatesObserverTests { [Fact] - public async Task ObserverSendsCorrectEvents() + public async Task ObserverSendsCorrectEvents_Tokens() { static void RemoveCertificate(X509Certificate2? certificate) { @@ -44,7 +46,7 @@ static void RemoveCertificate(X509Certificate2? certificate) var clientId = Guid.NewGuid(); var tenantId = Guid.NewGuid(); var instance = "https://login.microsoftonline.com/"; - var authority = instance + tenantId; + var authority = instance + tenantId + "/"; string certName = $"CN=TestCert-{Guid.NewGuid():N}"; cert1 = CreateAndInstallCertificate(certName); @@ -101,6 +103,10 @@ static void RemoveCertificate(X509Certificate2? certificate) Assert.Equal(CerticateObserverAction.Selected, eventArg.Action); Assert.Equal(cert1, eventArg.Certificate); Assert.Equal(description, eventArg.CredentialDescription); + Assert.NotNull(eventArg.CredentialSourceLoaderParameters); + Assert.Equal(ProtocolNames.Bearer, eventArg.CredentialSourceLoaderParameters.Protocol); + Assert.Equal(new Uri(authority), new Uri(eventArg.CredentialSourceLoaderParameters.Authority)); + Assert.Equal(clientId.ToString(), eventArg.CredentialSourceLoaderParameters.ClientId); // Second event was successful usage. observer1.Events.TryDequeue(out eventArg); @@ -108,6 +114,10 @@ static void RemoveCertificate(X509Certificate2? certificate) Assert.Equal(CerticateObserverAction.SuccessfullyUsed, eventArg.Action); Assert.Equal(cert1, eventArg.Certificate); Assert.Equal(description, eventArg.CredentialDescription); + Assert.NotNull(eventArg.CredentialSourceLoaderParameters); + Assert.Equal(ProtocolNames.Bearer, eventArg.CredentialSourceLoaderParameters.Protocol); + Assert.Equal(new Uri(authority), new Uri(eventArg.CredentialSourceLoaderParameters.Authority)); + Assert.Equal(clientId.ToString(), eventArg.CredentialSourceLoaderParameters.ClientId); // No further events Assert.Empty(observer1.Events); @@ -143,6 +153,174 @@ static void RemoveCertificate(X509Certificate2? certificate) Assert.NotNull(eventArg); Assert.Equal(CerticateObserverAction.Deselected, eventArg.Action); Assert.Equal(cert1, eventArg.Certificate); + Assert.NotNull(eventArg.CredentialSourceLoaderParameters); + Assert.Equal(ProtocolNames.Bearer, eventArg.CredentialSourceLoaderParameters.Protocol); + Assert.Equal(new Uri(authority), new Uri(eventArg.CredentialSourceLoaderParameters.Authority)); + Assert.Equal(clientId.ToString(), eventArg.CredentialSourceLoaderParameters.ClientId); + + // Then, it uses a new cert successfully. + observer1.Events.TryDequeue(out eventArg); + Assert.NotNull(eventArg); + Assert.Equal(CerticateObserverAction.Selected, eventArg.Action); + Assert.Equal(cert2, eventArg.Certificate); + observer1.Events.TryDequeue(out eventArg); + Assert.NotNull(eventArg); + Assert.Equal(CerticateObserverAction.SuccessfullyUsed, eventArg.Action); + Assert.Equal(cert2, eventArg.Certificate); + + // No events left. + Assert.Empty(observer1.Events); + } + finally + { + RemoveCertificate(cert1); + RemoveCertificate(cert2); + } + } + + [Fact] + public async Task ObserverSendsCorrectEvents_mTLS() + { + static void RemoveCertificate(X509Certificate2? certificate) + { + if (certificate is null) + { + return; + } + + using X509Store x509Store = new(StoreName.My, StoreLocation.CurrentUser); + x509Store.Open(OpenFlags.ReadWrite); + x509Store.Remove(certificate); + x509Store.Close(); + } + + X509Certificate2? cert1 = null; + X509Certificate2? cert2 = null; + try + { + var clientId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var instance = "https://login.microsoftonline.com/"; + var authority = instance + tenantId; + + string certName = $"CN=TestCert-{Guid.NewGuid():N}"; + cert1 = CreateAndInstallCertificate(certName); + + // Verify certificate is properly installed in store with timeout + VerifyCertificateInStore(cert1); + var description = new CredentialDescription + { + SourceType = CredentialSource.StoreWithDistinguishedName, + CertificateDistinguishedName = cert1.SubjectName.Name, + CertificateStorePath = "CurrentUser/My", + }; + + var taf = new CustomTAF(); + taf.Services.AddDownstreamApi("mtls", opts => { opts.BaseUrl = "https://test.example"; }); + taf.Services.Configure(options => + { + options.Instance = instance; + options.ClientId = clientId.ToString(); + options.TenantId = tenantId.ToString(); + options.ClientCredentials = [description]; + }); + taf.Services.AddMockClientFactory(description); + + // Add two observers so that we can check if multiple observers works as intended. + TestCertificatesObserver observer1 = new TestCertificatesObserver(); + taf.Services.AddSingleton(observer1); + TestCertificatesObserver observer2 = new TestCertificatesObserver(); + taf.Services.AddSingleton(observer2); + + var provider = taf.Build(); + + var mockHttpFactory = provider.GetRequiredService(); + + // Configure successful STS responses + mockHttpFactory.ConfigureSuccessfulTokenResponse(authority); + mockHttpFactory.ValidCertificates.Add(cert1); + + IDownstreamApi downstreamApi = provider.GetRequiredService(); + + DownstreamApiOptions options = new DownstreamApiOptions() + { + BaseUrl = authority, + ProtocolScheme = "mTLS", + RelativePath = "/oauth2/v2.0/token" + }; + + string apiUrl = authority + options.RelativePath; + + HttpResponseMessage result = await downstreamApi.CallApiAsync(options); + + // Assert + Assert.NotNull(result); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + + // Verify both observers got all events. + Assert.Equal(observer1.Events.Count, observer2.Events.Count); + + // First event was selection. + observer1.Events.TryDequeue(out var eventArg); + Assert.NotNull(eventArg); + Assert.Equal(CerticateObserverAction.Selected, eventArg.Action); + Assert.Equal(cert1, eventArg.Certificate); + Assert.Equal(description, eventArg.CredentialDescription); + Assert.NotNull(eventArg.CredentialSourceLoaderParameters); + Assert.Equal(ProtocolNames.Mtls, eventArg.CredentialSourceLoaderParameters.Protocol); + Assert.Equal(apiUrl, eventArg.CredentialSourceLoaderParameters.ApiUrl); + + // Second event was successful usage. + observer1.Events.TryDequeue(out eventArg); + Assert.NotNull(eventArg); + Assert.Equal(CerticateObserverAction.SuccessfullyUsed, eventArg.Action); + Assert.Equal(cert1, eventArg.Certificate); + Assert.Equal(description, eventArg.CredentialDescription); + Assert.NotNull(eventArg.CredentialSourceLoaderParameters); + Assert.Equal(ProtocolNames.Mtls, eventArg.CredentialSourceLoaderParameters.Protocol); + Assert.Equal(apiUrl, eventArg.CredentialSourceLoaderParameters.ApiUrl); + + // No further events + Assert.Empty(observer1.Events); + + // Rerun, should only get success event. + result = await downstreamApi.CallApiAsync(options); + + // We get selected events each time we use mTLS for now. + observer1.Events.TryDequeue(out eventArg); + Assert.NotNull(eventArg); + Assert.Equal(CerticateObserverAction.Selected, eventArg.Action); + + observer1.Events.TryDequeue(out eventArg); + Assert.NotNull(eventArg); + Assert.Equal(CerticateObserverAction.SuccessfullyUsed, eventArg.Action); + Assert.Empty(observer1.Events); + + // Change out the cert, so that if it reloads there will be a new one + RemoveCertificate(cert1); + cert2 = CreateAndInstallCertificate(certName); + + // Verify certificate is properly installed in store with timeout + VerifyCertificateInStore(cert2); + + // Rerun but it fails this time + mockHttpFactory.ValidCertificates.Clear(); + mockHttpFactory.ValidCertificates.Add(cert2); + result = await downstreamApi.CallApiAsync(options); + + // We get selected events each time we use mTLS for now. + observer1.Events.TryDequeue(out eventArg); + Assert.NotNull(eventArg); + Assert.Equal(CerticateObserverAction.Selected, eventArg.Action); + + // First it deselects the old cert. + observer1.Events.TryDequeue(out eventArg); + Assert.NotNull(eventArg); + Assert.Equal(CerticateObserverAction.Deselected, eventArg.Action); + Assert.Equal(cert1, eventArg.Certificate); + Assert.NotNull(eventArg.CredentialSourceLoaderParameters); + Assert.Equal(ProtocolNames.Mtls, eventArg.CredentialSourceLoaderParameters.Protocol); + Assert.Equal(apiUrl, eventArg.CredentialSourceLoaderParameters.ApiUrl); // Then, it uses a new cert successfully. observer1.Events.TryDequeue(out eventArg); @@ -224,7 +402,7 @@ private class TestCertificatesObserver : ICertificatesObserver /// /// Mock HTTP client factory for simulating STS backend interactions. /// - internal class MockHttpClientFactory : IHttpClientFactory + internal class MockHttpClientFactory : IHttpClientFactory, IMsalMtlsHttpClientFactory { private readonly MockHttpMessageHandler handler; @@ -237,6 +415,8 @@ public MockHttpClientFactory(CredentialDescription credential) /// public HttpClient CreateClient(string name) => new(this.handler); + public HttpClient GetHttpClient(X509Certificate2 x509Certificate2) => new(this.handler); + public HttpClient GetHttpClient() => new(this.handler); public void ConfigureSuccessfulTokenResponse(string authority) { @@ -432,14 +612,17 @@ protected override string DefineConfiguration(IConfigurationBuilder builder) } #pragma warning disable SA1402 // File may only contain a single type. Acceptable for extension methods. - file static class TestSericeExtensions + file static class TestServiceExtensions #pragma warning restore SA1402 // File may only contain a single type { public static IServiceCollection AddMockClientFactory(this IServiceCollection services, CredentialDescription description) { + //services.Remove(services.First(d => d.ServiceType == typeof(IMsalHttpClientFactory))); + return services .AddSingleton(new CertificatesObserverTests.MockHttpClientFactory(description)) - .AddSingleton(s => s.GetRequiredService()); + .AddSingleton(s => s.GetRequiredService()) + .AddSingleton(s => s.GetRequiredService()); } } } diff --git a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs index 243d186a5..59b44b3a9 100644 --- a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs +++ b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs @@ -34,6 +34,7 @@ public class DownstreamApiTests private readonly IHttpClientFactory _httpClientFactory; private readonly IOptionsMonitor _namedDownstreamApiOptions; private readonly ILogger _logger; + private readonly ICredentialsProvider _provider; private readonly DownstreamApi _input; private readonly DownstreamApi _inputSaml; @@ -43,19 +44,25 @@ public DownstreamApiTests() _authorizationHeaderProviderSaml = new MySamlAuthorizationHeaderProvider(); _httpClientFactory = new HttpClientFactoryTest(); _namedDownstreamApiOptions = new MyMonitor(); - _logger = new LoggerFactory().CreateLogger(); + var loggerFactory = new LoggerFactory(); + _logger = loggerFactory.CreateLogger(); + _provider = new CredentialsProvider(loggerFactory.CreateLogger(), new DefaultCredentialsLoader(), [], null); _input = new DownstreamApi( _authorizationHeaderProvider, _namedDownstreamApiOptions, _httpClientFactory, - _logger); + _logger, + msalHttpClientFactory: null, + credentialsProvider: _provider); _inputSaml = new DownstreamApi( _authorizationHeaderProviderSaml, _namedDownstreamApiOptions, _httpClientFactory, - _logger); + _logger, + msalHttpClientFactory: null, + credentialsProvider: _provider); } [Fact] @@ -67,7 +74,7 @@ public async Task UpdateRequestAsync_WithContent_AddsContentToRequestAsync() var options = new DownstreamApiOptions(); // Act - await _input.UpdateRequestAsync(httpRequestMessage, content, options, false, null, CancellationToken.None); + await _input.UpdateRequestWithCertificateAsync(httpRequestMessage, content, options, false, null, CancellationToken.None); // Assert Assert.Equal(content, httpRequestMessage.Content); @@ -94,7 +101,7 @@ public async Task UpdateRequestAsync_AddsToExtraQP() } }; // Act - await _input.UpdateRequestAsync(httpRequestMessage, content, options, false, null, CancellationToken.None); + await _input.UpdateRequestWithCertificateAsync(httpRequestMessage, content, options, false, null, CancellationToken.None); // Assert Assert.Equal(content, httpRequestMessage.Content); @@ -128,7 +135,7 @@ public async Task UpdateRequestAsync_WithScopes_AddsAuthorizationHeaderToRequest var user = new ClaimsPrincipal(); // Act - await _input.UpdateRequestAsync(httpRequestMessage, content, options, appToken, user, CancellationToken.None); + await _input.UpdateRequestWithCertificateAsync(httpRequestMessage, content, options, appToken, user, CancellationToken.None); // Assert Assert.True(httpRequestMessage.Headers.Contains("Authorization")); @@ -155,7 +162,7 @@ public async Task UpdateRequestAsync_WithScopes_AddsSamlAuthorizationHeaderToReq var user = new ClaimsPrincipal(); // Act - await _inputSaml.UpdateRequestAsync(httpRequestMessage, content, options, appToken, user, CancellationToken.None); + await _inputSaml.UpdateRequestWithCertificateAsync(httpRequestMessage, content, options, appToken, user, CancellationToken.None); // Assert Assert.True(httpRequestMessage.Headers.Contains("Authorization")); @@ -488,7 +495,9 @@ public void DownstreamApi_Constructor_WithBoundProvider_AcceptsIMsalMtlsHttpClie mockBoundProvider, _namedDownstreamApiOptions, mockMtlsHttpClientFactory, - _logger); + _logger, + msalHttpClientFactory: null, + credentialsProvider: _provider); Assert.NotNull(downstreamApi); } @@ -510,7 +519,9 @@ public async Task UpdateRequestAsync_WithAuthorizationHeaderBoundProvider_CallsC mockBoundProvider, _namedDownstreamApiOptions, _httpClientFactory, - _logger); + _logger, + msalHttpClientFactory: null, + credentialsProvider: _provider); var options = new DownstreamApiOptions { @@ -548,7 +559,7 @@ public async Task UpdateRequestAsync_WithAuthorizationHeaderBoundProvider_CallsC var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com"); // Act - var result = await downstreamApi.UpdateRequestAsync( + var result = await downstreamApi.UpdateRequestWithCertificateAsync( httpRequestMessage, null, options, @@ -570,9 +581,9 @@ await mockBoundProvider.DidNotReceive().CreateAuthorizationHeaderAsync( Arg.Any(), Arg.Any()); - Assert.NotNull(result); - Assert.Equal("MTLS_POP test-token", result.AuthorizationHeaderValue); - Assert.Equal(testCertificate, result.BindingCertificate); + Assert.NotNull(result.HeaderInfo); + Assert.Equal("MTLS_POP test-token", result.HeaderInfo.AuthorizationHeaderValue); + Assert.Equal(testCertificate, result.HeaderInfo.BindingCertificate); Assert.Equal("MTLS_POP test-token", httpRequestMessage.Headers.Authorization?.ToString()); } else @@ -588,7 +599,7 @@ await mockBoundProvider.Received(1).CreateAuthorizationHeaderAsync( Arg.Any(), Arg.Any()); - Assert.Null(result); + Assert.Null(result.HeaderInfo); Assert.Equal("Bearer test-token", httpRequestMessage.Headers.Authorization?.ToString()); } } @@ -604,7 +615,7 @@ public async Task UpdateRequestAsync_WithRegularAuthorizationHeaderProvider_Fall }; // Act - var result = await _input.UpdateRequestAsync( + var result = await _input.UpdateRequestWithCertificateAsync( httpRequestMessage, null, options, @@ -613,7 +624,7 @@ public async Task UpdateRequestAsync_WithRegularAuthorizationHeaderProvider_Fall CancellationToken.None); // Assert - Assert.Null(result); // Regular provider doesn't return AuthorizationHeaderInformation + Assert.Null(result.HeaderInfo); // Regular provider doesn't return AuthorizationHeaderInformation Assert.Equal("Bearer ey", httpRequestMessage.Headers.Authorization?.ToString()); } @@ -638,7 +649,9 @@ public async Task CallApiInternalAsync_WithRegularAuthorizationHeaderProvider_Us _authorizationHeaderProvider, // Regular provider _namedDownstreamApiOptions, mockHttpClientFactory, - _logger); + _logger, + msalHttpClientFactory: null, + credentialsProvider: _provider); var options = new DownstreamApiOptions { @@ -681,6 +694,7 @@ public async Task CallApiInternalAsync_WithAuthorizationHeaderBoundProviderAndWi _namedDownstreamApiOptions, mockMtlsHttpClientFactory, _logger, + _provider, (IMsalHttpClientFactory)mockMtlsHttpClientFactory); var options = new DownstreamApiOptions @@ -743,6 +757,7 @@ public async Task CallApiInternalAsync_WithAuthorizationHeaderBoundProviderButWi _namedDownstreamApiOptions, mockMtlsHttpClientFactory, _logger, + _provider, (IMsalHttpClientFactory)mockMtlsHttpClientFactory); var options = new DownstreamApiOptions @@ -796,7 +811,9 @@ public async Task CallApiInternalAsync_WithAuthorizationHeaderBoundProviderFailu mockBoundProvider, _namedDownstreamApiOptions, mockHttpClientFactory, - _logger); + _logger, + msalHttpClientFactory: null, + credentialsProvider: _provider); var options = new DownstreamApiOptions { @@ -823,6 +840,198 @@ public async Task CallApiInternalAsync_WithAuthorizationHeaderBoundProviderFailu Assert.Equal("Cannot acquire bound authorization header.", exception.Message); } + [Fact] + public async Task UpdateRequestWithCertificateAsync_WithMtlsProtocolScheme_DoesNotAddAuthorizationHeader() + { + // Arrange + var mockCredentialsProvider = Substitute.For(); + var testCertificate = CreateTestCertificate(); + var mockMtlsHttpClientFactory = Substitute.For(); + + var credentialDescription = new CertificateDescription + { + Certificate = testCertificate, + SourceType = CertificateSource.Certificate + }; + + mockCredentialsProvider + .GetCredentialAsync( + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(credentialDescription)); + + var downstreamApi = new DownstreamApi( + new ThrowingAuthorizationHeaderProvider(), + _namedDownstreamApiOptions, + mockMtlsHttpClientFactory, + _logger, + mockCredentialsProvider, + (IMsalHttpClientFactory)mockMtlsHttpClientFactory); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com"); + var options = new DownstreamApiOptions + { + ProtocolScheme = "MTLS" // mTLS-only path + }; + + // Act + var (headerInfo, mtlsCred) = await downstreamApi.UpdateRequestWithCertificateAsync( + httpRequestMessage, + null, + options, + false, + null, + CancellationToken.None); + + // Assert - No authorization header should be added + Assert.False(httpRequestMessage.Headers.Contains("Authorization")); + Assert.NotNull(headerInfo); + Assert.Null(headerInfo.AuthorizationHeaderValue); // No auth header value + Assert.Equal(testCertificate, headerInfo.BindingCertificate); // But certificate is present + Assert.Equal(credentialDescription, mtlsCred); + } + + [Fact] + public async Task UpdateRequestWithCertificateAsync_WithMtlsProtocolScheme_ThrowsWhenNoCredentialsProvider() + { + // Arrange + var downstreamApi = new DownstreamApi( + _authorizationHeaderProvider, + _namedDownstreamApiOptions, + _httpClientFactory, + _logger, + msalHttpClientFactory: null, + credentialsProvider: null!); // No credentials provider + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com"); + var options = new DownstreamApiOptions + { + ProtocolScheme = "MTLS" + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + downstreamApi.UpdateRequestWithCertificateAsync( + httpRequestMessage, + null, + options, + false, + null, + CancellationToken.None)); + + Assert.Contains("mTLS authentication requires a Credentials Provider", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public async Task UpdateRequestWithCertificateAsync_WithMtlsProtocolScheme_ThrowsWhenNoCertificate() + { + // Arrange + var mockCredentialsProvider = Substitute.For(); + + // Return a credential description without a certificate + var credentialDescription = new CertificateDescription + { + Certificate = null, // No certificate + SourceType = CertificateSource.Certificate + }; + + mockCredentialsProvider + .GetCredentialAsync( + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(credentialDescription)); + + var downstreamApi = new DownstreamApi( + _authorizationHeaderProvider, + _namedDownstreamApiOptions, + _httpClientFactory, + _logger, + mockCredentialsProvider, + msalHttpClientFactory: null); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com"); + var options = new DownstreamApiOptions + { + ProtocolScheme = "MTLS" + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + downstreamApi.UpdateRequestWithCertificateAsync( + httpRequestMessage, + null, + options, + false, + null, + CancellationToken.None)); + + Assert.Contains("mTLS authentication requires a certificate", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public async Task CallApiInternalAsync_WithMtlsProtocolScheme_UsesMtlsHttpClientFactory() + { + // Arrange + var mockCredentialsProvider = Substitute.For(); + var mockMtlsHttpClientFactory = Substitute.For(); + var testCertificate = CreateTestCertificate(); + + var credentialDescription = new CertificateDescription + { + Certificate = testCertificate, + SourceType = CertificateSource.Certificate + }; + + mockCredentialsProvider + .GetCredentialAsync( + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(credentialDescription)); + + var mockHandler = new MockHttpMessageHandler() + { + ExpectedMethod = HttpMethod.Get, + ResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"result\": \"success\"}") + } + }; + + var mockMtlsHttpClient = new HttpClient(mockHandler); + + ((IMsalMtlsHttpClientFactory)mockMtlsHttpClientFactory) + .GetHttpClient(testCertificate) + .Returns(mockMtlsHttpClient); + + var downstreamApi = new DownstreamApi( + new ThrowingAuthorizationHeaderProvider(), + _namedDownstreamApiOptions, + mockMtlsHttpClientFactory, + _logger, + mockCredentialsProvider, + (IMsalHttpClientFactory)mockMtlsHttpClientFactory); + + var options = new DownstreamApiOptions + { + BaseUrl = "https://api.example.com", + HttpMethod = "GET", + ProtocolScheme = "MTLS" // mTLS-only path, no scopes + }; + + // Act + var response = await downstreamApi.CallApiInternalAsync(null, options, false, null, null, CancellationToken.None); + + // Assert + Assert.NotNull(response); + Assert.True(response.IsSuccessStatusCode); + + // Verify mTLS HTTP client factory was used + var _ = ((IMsalMtlsHttpClientFactory)mockMtlsHttpClientFactory).Received(1).GetHttpClient(testCertificate); + + // Verify regular HTTP client factory was NOT used + ((IHttpClientFactory)mockMtlsHttpClientFactory).DidNotReceive().CreateClient(Arg.Any()); + } + private static X509Certificate2 CreateTestCertificate() { // Create a simple test certificate for mocking purposes @@ -907,5 +1116,12 @@ public Task CreateAuthorizationHeaderAsync(IEnumerable scopes, A return Task.FromResult("http://schemas.microsoft.com/dsts/saml2-bearer ey"); } } + + public class ThrowingAuthorizationHeaderProvider : IAuthorizationHeaderProvider + { + public Task CreateAuthorizationHeaderAsync(IEnumerable scopes, AuthorizationHeaderProviderOptions? options = null, ClaimsPrincipal? claimsPrincipal = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task CreateAuthorizationHeaderForAppAsync(string scopes, AuthorizationHeaderProviderOptions? downstreamApiOptions = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task CreateAuthorizationHeaderForUserAsync(IEnumerable scopes, AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, ClaimsPrincipal? claimsPrincipal = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + } } diff --git a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/ExtraParametersTests.cs b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/ExtraParametersTests.cs index e63810920..e692c53fa 100644 --- a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/ExtraParametersTests.cs +++ b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/ExtraParametersTests.cs @@ -8,6 +8,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using Castle.Core.Logging; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Identity.Abstractions; @@ -30,13 +31,17 @@ public ExtraParametersTests() _authorizationHeaderProvider = new MyAuthorizationHeaderProvider(); _httpClientFactory = new HttpClientFactoryTest(); _namedDownstreamApiOptions = new MyMonitor(); - _logger = new LoggerFactory().CreateLogger(); + var loggerFactory = new LoggerFactory(); + _logger = loggerFactory.CreateLogger(); + var provider = new CredentialsProvider(loggerFactory.CreateLogger(), new DefaultCredentialsLoader(), [], null); _downstreamApi = new DownstreamApi( _authorizationHeaderProvider, _namedDownstreamApiOptions, _httpClientFactory, - _logger); + _logger, + msalHttpClientFactory: null, + credentialsProvider: provider); } [Fact] @@ -54,7 +59,7 @@ public async Task UpdateRequestAsync_WithExtraHeaderParameters_AddsHeadersToRequ }; // Act - await _downstreamApi.UpdateRequestAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); + await _downstreamApi.UpdateRequestWithCertificateAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); // Assert Assert.True(httpRequestMessage.Headers.Contains("OData-Version")); @@ -78,7 +83,7 @@ public async Task UpdateRequestAsync_WithExtraQueryParameters_AddsQueryParameter }; // Act - await _downstreamApi.UpdateRequestAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); + await _downstreamApi.UpdateRequestWithCertificateAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); // Assert var requestUri = httpRequestMessage.RequestUri!.ToString(); @@ -100,7 +105,7 @@ public async Task UpdateRequestAsync_WithExtraQueryParameters_AppendsToExistingQ }; // Act - await _downstreamApi.UpdateRequestAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); + await _downstreamApi.UpdateRequestWithCertificateAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); // Assert var requestUri = httpRequestMessage.RequestUri!.ToString(); @@ -117,7 +122,7 @@ public async Task UpdateRequestAsync_WithoutExtraParameters_DoesNotModifyRequest var options = new DownstreamApiOptions(); // No extra parameters // Act - await _downstreamApi.UpdateRequestAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); + await _downstreamApi.UpdateRequestWithCertificateAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); // Assert Assert.Equal(originalUri, httpRequestMessage.RequestUri!.ToString()); @@ -137,7 +142,7 @@ public async Task UpdateRequestAsync_WithEmptyExtraParameters_DoesNotModifyReque }; // Act - await _downstreamApi.UpdateRequestAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); + await _downstreamApi.UpdateRequestWithCertificateAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); // Assert Assert.Equal(originalUri, httpRequestMessage.RequestUri!.ToString()); @@ -157,7 +162,7 @@ public async Task UpdateRequestAsync_WithSpecialCharacters_EscapesCorrectly() }; // Act - await _downstreamApi.UpdateRequestAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); + await _downstreamApi.UpdateRequestWithCertificateAsync(httpRequestMessage, null, options, false, null, CancellationToken.None); // Assert var requestUri = httpRequestMessage.RequestUri!.ToString(); diff --git a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs index 570f5debe..b875facdb 100644 --- a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs @@ -85,6 +85,20 @@ public void AddTokenAcquisition_Sdk_AddsWithCorrectLifetime() Assert.Null(actual.ImplementationFactory); }, actual => + { + Assert.Equal(typeof(IMsalHttpClientFactory), actual.ServiceType); + Assert.Equal(typeof(MsalMtlsHttpClientFactory), actual.ImplementationType); + Assert.Null(actual.ImplementationInstance); + Assert.Null(actual.ImplementationFactory); + }, + actual => + { + Assert.Equal(typeof(ICredentialsProvider), actual.ServiceType); + Assert.Equal(typeof(CredentialsProvider), actual.ImplementationType); + Assert.Null(actual.ImplementationInstance); + Assert.Null(actual.ImplementationFactory); + }, + actual => { Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); Assert.Equal(typeof(IMergedOptionsStore), actual.ServiceType); @@ -129,7 +143,7 @@ public void AddTokenAcquisition_Sdk_SupportsKeyedServices() services.AddTokenAcquisition(); // Verify the number of services added by AddTokenAcquisition (ignoring the service we added here). - Assert.Equal(11, services.Count(t => t.ServiceType != typeof(ServiceCollectionExtensionsTests))); + Assert.Equal(13, services.Count(t => t.ServiceType != typeof(ServiceCollectionExtensionsTests))); } #endif @@ -227,6 +241,20 @@ public void AddTokenAcquisitionCalledTwice_RegistersTokenAcquisitionOnlyAsSingle Assert.Null(actual.ImplementationFactory); }, actual => + { + Assert.Equal(typeof(IMsalHttpClientFactory), actual.ServiceType); + Assert.Equal(typeof(MsalMtlsHttpClientFactory), actual.ImplementationType); + Assert.Null(actual.ImplementationInstance); + Assert.Null(actual.ImplementationFactory); + }, + actual => + { + Assert.Equal(typeof(ICredentialsProvider), actual.ServiceType); + Assert.Equal(typeof(CredentialsProvider), actual.ImplementationType); + Assert.Null(actual.ImplementationInstance); + Assert.Null(actual.ImplementationFactory); + }, + actual => { Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); Assert.Equal(typeof(IMergedOptionsStore), actual.ServiceType); diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs index c93a0b8ae..4c15df89f 100644 --- a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs @@ -41,17 +41,16 @@ private void InitializeTokenAcquisitionObjects() _credentialsLoader = new DefaultCredentialsLoader(); _tokenAcquisitionAspnetCoreHost = new TokenAcquisitionAspnetCoreHost( MockHttpContextAccessor.CreateMockHttpContextAccessor(), - _provider.GetService()!, + _provider.GetRequiredService(), _provider); _tokenAcquisition = new TokenAcquisitionAspNetCore( new MsalTestTokenCacheProvider( - _provider.GetService()!, - _provider.GetService>()!), - _provider.GetService()!, - _provider.GetService>()!, + _provider.GetRequiredService(), + _provider.GetRequiredService>()), + _provider.GetRequiredService(), + _provider.GetRequiredService>(), _tokenAcquisitionAspnetCoreHost, - _provider, - _credentialsLoader); + _provider); } private void BuildTheRequiredServices() @@ -62,6 +61,8 @@ private void BuildTheRequiredServices() services.AddTransient( provider => _applicationOptionsMonitor); services.Configure(options => { }); + services.AddHttpClient(); + services.AddInMemoryTokenCaches(); services.AddTokenAcquisition(); services.AddLogging(); services.AddAuthentication();