@@ -353,9 +353,16 @@ public async Task ClientAssertion_BearerAsync()
353
353
var result = await cca . AcquireTokenForClient ( TestConstants . s_scope )
354
354
. ExecuteAsync ( ) . ConfigureAwait ( false ) ;
355
355
356
+ Assert . AreEqual ( TokenSource . IdentityProvider , result . AuthenticationResultMetadata . TokenSource ) ;
357
+
356
358
Assert . AreEqual (
357
359
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer" ,
358
360
handler . ActualRequestPostData [ "client_assertion_type" ] ) ;
361
+
362
+ result = await cca . AcquireTokenForClient ( TestConstants . s_scope )
363
+ . ExecuteAsync ( ) . ConfigureAwait ( false ) ;
364
+
365
+ Assert . AreEqual ( TokenSource . Cache , result . AuthenticationResultMetadata . TokenSource ) ;
359
366
}
360
367
361
368
[ TestMethod ]
@@ -509,6 +516,73 @@ public async Task WithMtlsPop_AfterPoPDelegate_Works()
509
516
}
510
517
}
511
518
519
+ [ TestMethod ]
520
+ public async Task PoP_CachedTokenWithDifferentCertificate_IsBypassedAsync ( )
521
+ {
522
+ const string region = "eastus" ;
523
+
524
+ // ─────────── Set up HTTP mocks ───────────
525
+ using var httpManager = new MockHttpManager ( ) ;
526
+ using ( var envContext = new EnvVariableContext ( ) )
527
+ {
528
+ Environment . SetEnvironmentVariable ( "REGION_NAME" , region ) ;
529
+
530
+ // 1st network call returns token‑A
531
+ httpManager . AddMockHandlerSuccessfulClientCredentialTokenResponseMessage (
532
+ tokenType : "mtls_pop" ) ;
533
+
534
+ // 2nd network call returns token‑B
535
+ httpManager . AddMockHandlerSuccessfulClientCredentialTokenResponseMessage (
536
+ tokenType : "mtls_pop" ) ;
537
+
538
+ // ─────────── Two distinct certificates ───────────
539
+ var certA = CertHelper . GetOrCreateTestCert ( ) ;
540
+ var certB = CertHelper . GetOrCreateTestCert ( regenerateCert : true ) ;
541
+
542
+ // Delegate returns certA on first call, certB on second call
543
+ int callCount = 0 ;
544
+ Func < AssertionRequestOptions , CancellationToken , Task < AssertionResponse > > popDelegate =
545
+ ( opts , ct ) =>
546
+ {
547
+ callCount ++ ;
548
+ var cert = ( callCount == 1 ) ? certA : certB ;
549
+ return Task . FromResult ( new AssertionResponse
550
+ {
551
+ Assertion = $ "jwt_{ callCount } ", // payload not important for this test
552
+ TokenBindingCertificate = cert
553
+ } ) ;
554
+ } ;
555
+
556
+ // ─────────── Build the app ───────────
557
+ var cca = ConfidentialClientApplicationBuilder . Create ( TestConstants . ClientId )
558
+ . WithClientSecret ( TestConstants . ClientSecret )
559
+ . WithClientAssertion ( popDelegate )
560
+ . WithAuthority ( $ "https://login.microsoftonline.com/123456-1234-2345-1234561234")
561
+ . WithAzureRegion ( ConfidentialClientApplication . AttemptRegionDiscovery )
562
+ . WithHttpManager ( httpManager )
563
+ . BuildConcrete ( ) ;
564
+
565
+ // ─────────── First acquire – network call, caches token‑A bound to certA ───────────
566
+ AuthenticationResult first = await cca . AcquireTokenForClient ( TestConstants . s_scope )
567
+ . WithMtlsProofOfPossession ( )
568
+ . ExecuteAsync ( )
569
+ . ConfigureAwait ( false ) ;
570
+
571
+ Assert . AreEqual ( TokenSource . IdentityProvider , first . AuthenticationResultMetadata . TokenSource ) ;
572
+ Assert . AreEqual ( certA . Thumbprint , first . BindingCertificate . Thumbprint ) ;
573
+
574
+ // ─────────── Second acquire – delegate now returns certB ───────────
575
+ AuthenticationResult second = await cca . AcquireTokenForClient ( TestConstants . s_scope )
576
+ . WithMtlsProofOfPossession ( )
577
+ . ExecuteAsync ( )
578
+ . ConfigureAwait ( false ) ;
579
+
580
+ // The serial number mismatch should have forced a network call, not a cache hit
581
+ Assert . AreEqual ( TokenSource . IdentityProvider , second . AuthenticationResultMetadata . TokenSource ) ;
582
+ Assert . AreEqual ( certB . Thumbprint , second . BindingCertificate . Thumbprint ) ;
583
+ }
584
+ }
585
+
512
586
[ TestMethod ]
513
587
public async Task WithMtlsPop_AfterBearerDelegate_Throws ( )
514
588
{
0 commit comments