Skip to content

Commit 826ff82

Browse files
authored
Jmprieur/test cert rotation (#2496)
Added an integration test validating the certificate rotation (including app registration of short time self-assigned certificates) Added a way for the apps to observe that the certs are selected or unselected (#2498). This is currently an experimental feature (to get feedback) Improved the cert rotation and fixed an issue in the rotation of client certificates. - Adding a ResetCertificates with an override for an enumeration of CredentialDescription - (unrelated) fixing mappings in OWIN devapps
1 parent 086bf8d commit 826ff82

File tree

10 files changed

+486
-36
lines changed

10 files changed

+486
-36
lines changed

src/Microsoft.Identity.Web.Certificate/DefaultCertificateLoader.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ public static void ResetCertificates(IEnumerable<CertificateDescription>? certif
118118
foreach (var cert in certificateDescriptions)
119119
{
120120
cert.Certificate = null;
121+
cert.CachedValue = null;
122+
}
123+
}
124+
}
125+
126+
/// <summary>
127+
/// Resets all the certificates in the certificate description list.
128+
/// Use, for example, before a retry.
129+
/// </summary>
130+
/// <param name="credentialDescription">Description of the certificates.</param>
131+
public static void ResetCertificates(IEnumerable<CredentialDescription>? credentialDescription)
132+
{
133+
if (credentialDescription != null)
134+
{
135+
foreach (var cert in credentialDescription.Where(c => c.Certificate != null))
136+
{
137+
cert.Certificate = null;
138+
cert.CachedValue = null;
121139
}
122140
}
123141
}

src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7-
using System.Security.Cryptography.X509Certificates;
8-
using System.Threading;
9-
using System.Threading.Tasks;
10-
using Azure.Identity;
117
using Microsoft.Extensions.Logging;
128
using Microsoft.Identity.Abstractions;
139
using Microsoft.Identity.Client;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Security.Cryptography.X509Certificates;
5+
using Microsoft.Identity.Abstractions;
6+
7+
// Types in the Microsoft.Identity.Web.Experimental namespace
8+
// are meant to get feedback from the community on proposed features, and
9+
// may be modified or removed in future releases without obeying to the
10+
// semantic versionning.
11+
namespace Microsoft.Identity.Web.Experimental
12+
{
13+
/// <summary>
14+
/// Action of the token acquirer on the certificate.
15+
/// </summary>
16+
public enum CerticateObserverAction
17+
{
18+
/// <summary>
19+
/// The certificate was selected as a client certificate.
20+
/// </summary>
21+
Selected,
22+
23+
/// <summary>
24+
/// The certificate was deselected as a client certificate. This
25+
/// happens when the STS does not accept the certificate any longer.
26+
/// </summary>
27+
Deselected,
28+
}
29+
30+
/// <summary>
31+
/// Event argument about the certificate consumption by the app
32+
/// </summary>
33+
public class CertificateChangeEventArg
34+
{
35+
/// <summary>
36+
/// Action on the certificate
37+
/// </summary>
38+
public CerticateObserverAction Action { get; set; }
39+
40+
/// <summary>
41+
/// Certificate
42+
/// </summary>
43+
public X509Certificate2? Certificate { get; set; }
44+
45+
/// <summary>
46+
/// Credential description
47+
/// </summary>
48+
public CredentialDescription? CredentialDescription { get; set; }
49+
}
50+
51+
/// <summary>
52+
/// Interface that apps can implement to be notified when a certificate is selected or removed.
53+
/// </summary>
54+
public interface ICertificatesObserver
55+
{
56+
/// <summary>
57+
/// Called when a certificate is selected or removed.
58+
/// </summary>
59+
/// <param name="e"></param>
60+
public void OnClientCertificateChanged(CertificateChangeEventArg e);
61+
}
62+
}

src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactory.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@ public ServiceCollection Services
4141
{
4242
get
4343
{
44-
if (ServiceProvider != null)
45-
{
46-
throw new InvalidOperationException("Cannot change services once you called Build()");
47-
}
4844
return _services;
4945
}
5046

src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
using System.Linq;
1111
using System.Net.Http;
1212
using System.Security.Claims;
13+
using System.Security.Cryptography.X509Certificates;
1314
using System.Threading;
1415
using System.Threading.Tasks;
16+
using Microsoft.Extensions.DependencyInjection;
1517
using Microsoft.Extensions.Logging;
1618
using Microsoft.Identity.Abstractions;
1719
using Microsoft.Identity.Client;
@@ -21,6 +23,7 @@
2123
using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
2224
using Microsoft.IdentityModel.JsonWebTokens;
2325
using Microsoft.IdentityModel.Tokens;
26+
using Microsoft.Identity.Web.Experimental;
2427

2528
namespace 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,

tests/DevApps/aspnet-mvc/OwinWebApi/Web.config

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
</dependentAssembly>
5959
<dependentAssembly>
6060
<assemblyIdentity name="System.IdentityModel.Tokens.Jwt" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
61-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
61+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
6262
</dependentAssembly>
6363
<dependentAssembly>
6464
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="CC7B13FFCD2DDD51" culture="neutral"/>
@@ -74,31 +74,31 @@
7474
</dependentAssembly>
7575
<dependentAssembly>
7676
<assemblyIdentity name="Microsoft.IdentityModel.Tokens" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
77-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
77+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
7878
</dependentAssembly>
7979
<dependentAssembly>
8080
<assemblyIdentity name="Microsoft.IdentityModel.Protocols.WsFederation" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
8181
<bindingRedirect oldVersion="0.0.0.0-5.5.0.0" newVersion="5.5.0.0"/>
8282
</dependentAssembly>
8383
<dependentAssembly>
8484
<assemblyIdentity name="Microsoft.IdentityModel.Protocols.OpenIdConnect" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
85-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
85+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
8686
</dependentAssembly>
8787
<dependentAssembly>
8888
<assemblyIdentity name="Microsoft.IdentityModel.Protocols" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
89-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
89+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
9090
</dependentAssembly>
9191
<dependentAssembly>
9292
<assemblyIdentity name="Microsoft.IdentityModel.Logging" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
93-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
93+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
9494
</dependentAssembly>
9595
<dependentAssembly>
9696
<assemblyIdentity name="Microsoft.IdentityModel.Abstractions" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
97-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
97+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
9898
</dependentAssembly>
9999
<dependentAssembly>
100100
<assemblyIdentity name="Microsoft.Identity.Client" publicKeyToken="0A613F4DD989E8AE" culture="neutral"/>
101-
<bindingRedirect oldVersion="0.0.0.0-4.55.0.0" newVersion="4.55.0.0"/>
101+
<bindingRedirect oldVersion="0.0.0.0-4.56.0.0" newVersion="4.56.0.0"/>
102102
</dependentAssembly>
103103
<dependentAssembly>
104104
<assemblyIdentity name="Microsoft.Extensions.Primitives" publicKeyToken="ADB9793829DDAE60" culture="neutral"/>

tests/DevApps/aspnet-mvc/OwinWebApp/Web.config

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
</dependentAssembly>
6060
<dependentAssembly>
6161
<assemblyIdentity name="System.IdentityModel.Tokens.Jwt" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
62-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
62+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
6363
</dependentAssembly>
6464
<dependentAssembly>
6565
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="CC7B13FFCD2DDD51" culture="neutral"/>
@@ -75,31 +75,31 @@
7575
</dependentAssembly>
7676
<dependentAssembly>
7777
<assemblyIdentity name="Microsoft.IdentityModel.Tokens" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
78-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
78+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
7979
</dependentAssembly>
8080
<dependentAssembly>
8181
<assemblyIdentity name="Microsoft.IdentityModel.Protocols.WsFederation" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
8282
<bindingRedirect oldVersion="0.0.0.0-5.5.0.0" newVersion="5.5.0.0"/>
8383
</dependentAssembly>
8484
<dependentAssembly>
8585
<assemblyIdentity name="Microsoft.IdentityModel.Protocols.OpenIdConnect" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
86-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
86+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
8787
</dependentAssembly>
8888
<dependentAssembly>
8989
<assemblyIdentity name="Microsoft.IdentityModel.Protocols" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
90-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
90+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
9191
</dependentAssembly>
9292
<dependentAssembly>
9393
<assemblyIdentity name="Microsoft.IdentityModel.Logging" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
94-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
94+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
9595
</dependentAssembly>
9696
<dependentAssembly>
9797
<assemblyIdentity name="Microsoft.IdentityModel.Abstractions" publicKeyToken="31BF3856AD364E35" culture="neutral"/>
98-
<bindingRedirect oldVersion="0.0.0.0-6.32.0.0" newVersion="6.32.0.0"/>
98+
<bindingRedirect oldVersion="0.0.0.0-6.32.3.0" newVersion="6.32.3.0"/>
9999
</dependentAssembly>
100100
<dependentAssembly>
101101
<assemblyIdentity name="Microsoft.Identity.Client" publicKeyToken="0A613F4DD989E8AE" culture="neutral"/>
102-
<bindingRedirect oldVersion="0.0.0.0-4.55.0.0" newVersion="4.55.0.0"/>
102+
<bindingRedirect oldVersion="0.0.0.0-4.56.0.0" newVersion="4.56.0.0"/>
103103
</dependentAssembly>
104104
<dependentAssembly>
105105
<assemblyIdentity name="Microsoft.Extensions.Primitives" publicKeyToken="ADB9793829DDAE60" culture="neutral"/>

0 commit comments

Comments
 (0)