Skip to content

Commit a37afec

Browse files
authored
New POP test - Sni Assertion Flow Uses JwtPop And Succeeds (#5667)
Sni_AssertionFlow_Uses_JwtPop_And_Succeeds_TestAsync
1 parent 55b575f commit a37afec

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using System.Linq;
45
using System.Security.Cryptography.X509Certificates;
6+
using System.Threading;
57
using System.Threading.Tasks;
68
using Microsoft.Identity.Client;
9+
using Microsoft.Identity.Client.Extensibility;
710
using Microsoft.Identity.Client.Internal;
811
using Microsoft.Identity.Test.Common.Core.Helpers;
912
using Microsoft.Identity.Test.Integration.Infrastructure;
@@ -14,10 +17,13 @@
1417
namespace Microsoft.Identity.Test.Integration.HeadlessTests
1518
{
1619
// 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
1722
[TestClass]
1823
public class ClientCredentialsMtlsPopTests
1924
{
2025
private const string MsiAllowListedAppIdforSNI = "163ffef9-a313-45b4-ab2f-c7e2f5e0e23e";
26+
private const string TokenExchangeUrl = "api://AzureADTokenExchange/.default";
2127

2228
[TestInitialize]
2329
public void TestInitialize()
@@ -76,5 +82,99 @@ public async Task Sni_Gets_Pop_Token_Successfully_TestAsync()
7682
authResult.BindingCertificate.Thumbprint,
7783
"BindingCertificate must match the certificate supplied via WithCertificate().");
7884
}
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+
}
79179
}
80180
}

0 commit comments

Comments
 (0)