|
1 | 1 | // Copyright (c) Microsoft Corporation. All rights reserved. |
2 | 2 | // Licensed under the MIT License. |
3 | 3 |
|
| 4 | +using System.Linq; |
4 | 5 | using System.Security.Cryptography.X509Certificates; |
| 6 | +using System.Threading; |
5 | 7 | using System.Threading.Tasks; |
6 | 8 | using Microsoft.Identity.Client; |
| 9 | +using Microsoft.Identity.Client.Extensibility; |
7 | 10 | using Microsoft.Identity.Client.Internal; |
8 | 11 | using Microsoft.Identity.Test.Common.Core.Helpers; |
9 | 12 | using Microsoft.Identity.Test.Integration.Infrastructure; |
|
14 | 17 | namespace Microsoft.Identity.Test.Integration.HeadlessTests |
15 | 18 | { |
16 | 19 | // Tests in this class will run on .NET Core |
| 20 | + // POP tests only work on the allow listed SNI app |
| 21 | + // and tenant ("bea21ebe-8b64-4d06-9f6d-6a889b120a7c") - MSI team tenant |
17 | 22 | [TestClass] |
18 | 23 | public class ClientCredentialsMtlsPopTests |
19 | 24 | { |
20 | 25 | private const string MsiAllowListedAppIdforSNI = "163ffef9-a313-45b4-ab2f-c7e2f5e0e23e"; |
| 26 | + private const string TokenExchangeUrl = "api://AzureADTokenExchange/.default"; |
21 | 27 |
|
22 | 28 | [TestInitialize] |
23 | 29 | public void TestInitialize() |
@@ -76,5 +82,99 @@ public async Task Sni_Gets_Pop_Token_Successfully_TestAsync() |
76 | 82 | authResult.BindingCertificate.Thumbprint, |
77 | 83 | "BindingCertificate must match the certificate supplied via WithCertificate()."); |
78 | 84 | } |
| 85 | + |
| 86 | + [DoNotRunOnLinux] |
| 87 | + [TestMethod] |
| 88 | + public async Task Sni_AssertionFlow_Uses_JwtPop_And_Succeeds_TestAsync() |
| 89 | + { |
| 90 | + X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName); |
| 91 | + |
| 92 | + // Step 1: obtain a real JWT to reuse as the "assertion" |
| 93 | + IConfidentialClientApplication firstApp = ConfidentialClientApplicationBuilder.Create(MsiAllowListedAppIdforSNI) |
| 94 | + .WithAuthority("https://login.microsoftonline.com/bea21ebe-8b64-4d06-9f6d-6a889b120a7c") |
| 95 | + .WithAzureRegion("westus3") |
| 96 | + .WithCertificate(cert, true) |
| 97 | + .WithTestLogging() |
| 98 | + .Build(); |
| 99 | + |
| 100 | + AuthenticationResult first = await firstApp |
| 101 | + .AcquireTokenForClient(new[] { TokenExchangeUrl }) |
| 102 | + .WithMtlsProofOfPossession() |
| 103 | + .ExecuteAsync() |
| 104 | + .ConfigureAwait(false); |
| 105 | + |
| 106 | + string assertionJwt = first.AccessToken; |
| 107 | + Assert.IsFalse(string.IsNullOrEmpty(assertionJwt), "First leg did not return an access token to reuse as assertion."); |
| 108 | + |
| 109 | + // Step 2: build the assertion-based app (NO WithCertificate here) |
| 110 | + bool assertionProviderCalled = false; |
| 111 | + string tokenEndpointSeenByProvider = null; |
| 112 | + |
| 113 | + string requestUriSeen = null; |
| 114 | + string clientAssertionType = null; |
| 115 | + bool sawClientAssertionParam = false; |
| 116 | + bool sawClientAssertionTypeParam = false; |
| 117 | + |
| 118 | + IConfidentialClientApplication assertionApp = ConfidentialClientApplicationBuilder.Create(MsiAllowListedAppIdforSNI) |
| 119 | + .WithExperimentalFeatures() |
| 120 | + .WithAuthority("https://login.microsoftonline.com/bea21ebe-8b64-4d06-9f6d-6a889b120a7c") |
| 121 | + .WithAzureRegion("westus3") |
| 122 | + .WithClientAssertion((AssertionRequestOptions options, CancellationToken ct) => |
| 123 | + { |
| 124 | + assertionProviderCalled = true; |
| 125 | + tokenEndpointSeenByProvider = options.TokenEndpoint; |
| 126 | + |
| 127 | + return Task.FromResult(new ClientSignedAssertion |
| 128 | + { |
| 129 | + Assertion = assertionJwt, // forwarded as client_assertion |
| 130 | + TokenBindingCertificate = cert // binds assertion for mTLS PoP (jwt-pop) |
| 131 | + }); |
| 132 | + }) |
| 133 | + .WithTestLogging() |
| 134 | + .Build(); |
| 135 | + |
| 136 | + // Step 3: second leg should now SUCCEED |
| 137 | + AuthenticationResult second = await assertionApp |
| 138 | + .AcquireTokenForClient(new[] { "https://vault.azure.net/.default" }) |
| 139 | + .WithMtlsProofOfPossession() |
| 140 | + .OnBeforeTokenRequest(data => |
| 141 | + { |
| 142 | + requestUriSeen = data.RequestUri?.ToString(); |
| 143 | + |
| 144 | + if (data.BodyParameters != null) |
| 145 | + { |
| 146 | + sawClientAssertionParam = data.BodyParameters.ContainsKey("client_assertion"); |
| 147 | + sawClientAssertionTypeParam = data.BodyParameters.ContainsKey("client_assertion_type"); |
| 148 | + |
| 149 | + data.BodyParameters.TryGetValue("client_assertion_type", out clientAssertionType); |
| 150 | + } |
| 151 | + |
| 152 | + return Task.CompletedTask; |
| 153 | + }) |
| 154 | + .ExecuteAsync() |
| 155 | + .ConfigureAwait(false); |
| 156 | + |
| 157 | + // Success assertions |
| 158 | + Assert.IsNotNull(second, "Second leg returned null AuthenticationResult."); |
| 159 | + Assert.IsFalse(string.IsNullOrEmpty(second.AccessToken), "Second leg did not return an access token."); |
| 160 | + CollectionAssert.Contains(second.Scopes.ToArray(), "https://vault.azure.net/.default", |
| 161 | + "Second leg token is not for Key Vault scope."); |
| 162 | + |
| 163 | + // Prove MSAL used the assertion + jwt-pop binding |
| 164 | + Assert.IsTrue(assertionProviderCalled, "Client assertion provider should have been invoked."); |
| 165 | + Assert.IsFalse(string.IsNullOrEmpty(tokenEndpointSeenByProvider), |
| 166 | + "AssertionRequestOptions.TokenEndpoint should be provided to the callback."); |
| 167 | + |
| 168 | + Assert.IsTrue(sawClientAssertionParam, "Token request should include client_assertion body parameter."); |
| 169 | + Assert.IsTrue(sawClientAssertionTypeParam, "Token request should include client_assertion_type body parameter."); |
| 170 | + |
| 171 | + Assert.AreEqual( |
| 172 | + "urn:ietf:params:oauth:client-assertion-type:jwt-pop", |
| 173 | + clientAssertionType, |
| 174 | + "When TokenBindingCertificate is supplied and PoP is enabled, MSAL should use jwt-pop client_assertion_type."); |
| 175 | + |
| 176 | + // Optional: if you rely on regional mTLS endpoints, check the host |
| 177 | + StringAssert.Contains(requestUriSeen ?? "", "mtlsauth.microsoft.com"); |
| 178 | + } |
79 | 179 | } |
80 | 180 | } |
0 commit comments