1010using System . Linq ;
1111using System . Net . Http ;
1212using System . Security . Claims ;
13+ using System . Security . Cryptography . X509Certificates ;
1314using System . Threading ;
1415using System . Threading . Tasks ;
16+ using Microsoft . Extensions . DependencyInjection ;
1517using Microsoft . Extensions . Logging ;
1618using Microsoft . Identity . Abstractions ;
1719using Microsoft . Identity . Client ;
2123using Microsoft . Identity . Web . TokenCacheProviders . InMemory ;
2224using Microsoft . IdentityModel . JsonWebTokens ;
2325using Microsoft . IdentityModel . Tokens ;
26+ using Microsoft . Identity . Web . Experimental ;
2427
2528namespace Microsoft . Identity . Web
2629{
@@ -53,6 +56,7 @@ class OAuthConstants
5356 protected readonly IServiceProvider _serviceProvider ;
5457 protected readonly ITokenAcquisitionHost _tokenAcquisitionHost ;
5558 protected readonly ICredentialsLoader _credentialsLoader ;
59+ protected readonly ICertificatesObserver ? _certificatesObserver ;
5660
5761 /// <summary>
5862 /// Scopes which are already requested by MSAL.NET. They should not be re-requested;.
@@ -99,6 +103,7 @@ public TokenAcquisition(
99103 _serviceProvider = serviceProvider ;
100104 _tokenAcquisitionHost = tokenAcquisitionHost ;
101105 _credentialsLoader = credentialsLoader ;
106+ _certificatesObserver = serviceProvider . GetService < ICertificatesObserver > ( ) ;
102107 }
103108
104109#if NET6_0_OR_GREATER
@@ -110,9 +115,10 @@ public async Task<AcquireTokenResult> AddAccountToCacheFromAuthorizationCodeAsyn
110115 _ = Throws . IfNull ( authCodeRedemptionParameters . Scopes ) ;
111116 MergedOptions mergedOptions = _tokenAcquisitionHost . GetOptions ( authCodeRedemptionParameters . AuthenticationScheme , out string effectiveAuthenticationScheme ) ;
112117
118+ IConfidentialClientApplication ? application = null ;
113119 try
114120 {
115- var application = GetOrBuildConfidentialClientApplication ( mergedOptions ) ;
121+ application = GetOrBuildConfidentialClientApplication ( mergedOptions ) ;
116122
117123 // Do not share the access token with ASP.NET Core otherwise ASP.NET will cache it and will not send the OAuth 2.0 request in
118124 // case a further call to AcquireTokenByAuthorizationCodeAsync in the future is required for incremental consent (getting a code requesting more scopes)
@@ -171,7 +177,8 @@ public async Task<AcquireTokenResult> AddAccountToCacheFromAuthorizationCodeAsyn
171177 }
172178 catch ( MsalServiceException exMsal ) when ( IsInvalidClientCertificateOrSignedAssertionError ( exMsal ) )
173179 {
174- DefaultCertificateLoader . ResetCertificates ( mergedOptions . ClientCertificates ) ;
180+ NotifyCertificateSelection ( mergedOptions , application ! , CerticateObserverAction . Deselected ) ;
181+ DefaultCertificateLoader . ResetCertificates ( mergedOptions . ClientCredentials ) ;
175182 _applicationsByAuthorityClientId [ GetApplicationKey ( mergedOptions ) ] = null ;
176183
177184 // Retry
@@ -267,7 +274,8 @@ public async Task<AuthenticationResult> GetAuthenticationResultForUserAsync(
267274 }
268275 catch ( MsalServiceException exMsal ) when ( IsInvalidClientCertificateOrSignedAssertionError ( exMsal ) )
269276 {
270- DefaultCertificateLoader . ResetCertificates ( mergedOptions . ClientCertificates ) ;
277+ NotifyCertificateSelection ( mergedOptions , application , CerticateObserverAction . Deselected ) ;
278+ DefaultCertificateLoader . ResetCertificates ( mergedOptions . ClientCredentials ) ;
271279 _applicationsByAuthorityClientId [ GetApplicationKey ( mergedOptions ) ] = null ;
272280
273281 // Retry
@@ -325,7 +333,7 @@ private void LogAuthResult(AuthenticationResult? authenticationResult)
325333 /// for multi tenant apps or daemons.</param>
326334 /// <param name="tokenAcquisitionOptions">Options passed-in to create the token acquisition object which calls into MSAL .NET.</param>
327335 /// <returns>An authentication result for the app itself, based on its scopes.</returns>
328- public Task < AuthenticationResult > GetAuthenticationResultForAppAsync (
336+ public async Task < AuthenticationResult > GetAuthenticationResultForAppAsync (
329337 string scope ,
330338 string ? authenticationScheme = null ,
331339 string ? tenant = null ,
@@ -415,21 +423,29 @@ public Task<AuthenticationResult> GetAuthenticationResultForAppAsync(
415423
416424 try
417425 {
418- return builder . ExecuteAsync ( tokenAcquisitionOptions != null ? tokenAcquisitionOptions . CancellationToken : CancellationToken . None ) ;
426+ return await builder . ExecuteAsync ( tokenAcquisitionOptions != null ? tokenAcquisitionOptions . CancellationToken : CancellationToken . None ) ;
419427 }
420428 catch ( MsalServiceException exMsal ) when ( IsInvalidClientCertificateOrSignedAssertionError ( exMsal ) )
421429 {
422- DefaultCertificateLoader . ResetCertificates ( mergedOptions . ClientCertificates ) ;
430+ NotifyCertificateSelection ( mergedOptions , application , CerticateObserverAction . Deselected ) ;
431+ DefaultCertificateLoader . ResetCertificates ( mergedOptions . ClientCredentials ) ;
423432 _applicationsByAuthorityClientId [ GetApplicationKey ( mergedOptions ) ] = null ;
424433
425434 // Retry
426435 _retryClientCertificate = true ;
427- return GetAuthenticationResultForAppAsync (
436+ return await GetAuthenticationResultForAppAsync (
428437 scope ,
429438 authenticationScheme : authenticationScheme ,
430439 tenant : tenant ,
431440 tokenAcquisitionOptions : tokenAcquisitionOptions ) ;
432441 }
442+ catch ( MsalException ex )
443+ {
444+ // GetAuthenticationResultForAppAsync is an abstraction that can be called from
445+ // a web app or a web API
446+ Logger . TokenAcquisitionError ( _logger , ex . Message , ex ) ;
447+ throw ;
448+ }
433449 finally
434450 {
435451 _retryClientCertificate = false ;
@@ -635,6 +651,10 @@ private IConfidentialClientApplication BuildConfidentialClientApplication(Merged
635651
636652 IConfidentialClientApplication app = builder . Build ( ) ;
637653
654+ // If the client application has set certificate observer,
655+ // fire the event to notify the client app that a certificate was selected.
656+ NotifyCertificateSelection ( mergedOptions , app , CerticateObserverAction . Selected ) ;
657+
638658 // Initialize token cache providers
639659 if ( ! ( _tokenCacheProvider is MsalMemoryTokenCacheProvider ) )
640660 {
@@ -654,6 +674,29 @@ private IConfidentialClientApplication BuildConfidentialClientApplication(Merged
654674 }
655675 }
656676
677+ /// <summary>
678+ /// Find the certificate used by the app and fire the event to notify the client app that a certificate was selected/unselected.
679+ /// </summary>
680+ /// <param name="mergedOptions"></param>
681+ /// <param name="app"></param>
682+ /// <param name="action"></param>
683+ private void NotifyCertificateSelection ( MergedOptions mergedOptions , IConfidentialClientApplication app , CerticateObserverAction action )
684+ {
685+ X509Certificate2 selectedCertificate = app . AppConfig . ClientCredentialCertificate ;
686+ if ( _certificatesObserver != null
687+ && selectedCertificate != null )
688+ {
689+ _certificatesObserver . OnClientCertificateChanged (
690+ new CertificateChangeEventArg ( )
691+ {
692+ Action = action ,
693+ Certificate = app . AppConfig . ClientCredentialCertificate ,
694+ CredentialDescription = mergedOptions . ClientCredentials ? . FirstOrDefault ( c => c . Certificate == selectedCertificate )
695+ } ) ;
696+ ;
697+ }
698+ }
699+
657700 private async Task < AuthenticationResult ? > GetAuthenticationResultForWebApiToCallDownstreamApiAsync (
658701 IConfidentialClientApplication application ,
659702 string ? tenantId ,
0 commit comments