Skip to content

Commit aac4dfe

Browse files
authored
Add a unit test around WithClientClaims (#5216)
* Add a unit test around WithClientClaims * Test with extra claim * 1 * 2
1 parent c776ab9 commit aac4dfe

File tree

1 file changed

+117
-3
lines changed

1 file changed

+117
-3
lines changed

tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using Microsoft.VisualStudio.TestTools.UnitTesting;
2323
using static Microsoft.Identity.Client.Internal.JsonWebToken;
2424
using Microsoft.Identity.Client.RP;
25+
using Microsoft.Identity.Client.Http;
2526

2627
namespace Microsoft.Identity.Test.Unit
2728
{
@@ -68,6 +69,7 @@ private static MockHttpMessageHandler CreateTokenResponseHttpHandlerWithX5CValid
6869
var handler = new JwtSecurityTokenHandler();
6970
var jsonToken = handler.ReadJwtToken(encodedJwt);
7071
var x5c = jsonToken.Header.FirstOrDefault(header => header.Key == "x5c");
72+
7173
if (expectedX5C != null)
7274
{
7375
Assert.AreEqual("x5c", x5c.Key, "x5c should be present");
@@ -220,6 +222,118 @@ public async Task TestX5C(
220222
}
221223
}
222224

225+
[TestMethod]
226+
public async Task ClientAssertionHasExpiration()
227+
{
228+
using (var harness = CreateTestHarness())
229+
{
230+
harness.HttpManager.AddInstanceDiscoveryMockHandler();
231+
var certificate = CertHelper.GetOrCreateTestCert();
232+
var exportedCertificate = Convert.ToBase64String(certificate.Export(X509ContentType.Cert));
233+
234+
IDictionary<string, string> extraAssertionContent = new Dictionary<string, string>
235+
{
236+
{ "foo", "bar" },
237+
238+
};
239+
240+
var cca = ConfidentialClientApplicationBuilder
241+
.Create(TestConstants.ClientId)
242+
.WithAuthority("https://login.microsoftonline.com/tid")
243+
.WithHttpManager(harness.HttpManager)
244+
.WithClientClaims(certificate, extraAssertionContent, mergeWithDefaultClaims: true, sendX5C: true) // x5c can also be enabled on the request
245+
.Build();
246+
247+
// Checks the client assertion for x5c and for expiration
248+
var handler = harness.HttpManager.AddTokenResponse(TokenResponseType.Valid_ClientCredentials);
249+
handler.AdditionalRequestValidation = (r) => ValidateClientAssertion(r, exportedCertificate, validateStandardClaims: true);
250+
251+
AuthenticationResult result = await cca.AcquireTokenForClient(TestConstants.s_scope)
252+
.WithSendX5C(true) // x5c can also be enabled here on the request
253+
.ExecuteAsync()
254+
.ConfigureAwait(false);
255+
256+
Assert.IsNotNull(result.AccessToken);
257+
}
258+
}
259+
260+
[TestMethod]
261+
public async Task ClientAssertionWithClaimOverride()
262+
{
263+
using (var harness = CreateTestHarness())
264+
{
265+
harness.HttpManager.AddInstanceDiscoveryMockHandler();
266+
var certificate = CertHelper.GetOrCreateTestCert();
267+
var exportedCertificate = Convert.ToBase64String(certificate.Export(X509ContentType.Cert));
268+
269+
IDictionary<string, string> extraAssertionContent = new Dictionary<string, string>
270+
{
271+
{ "foo", "bar" },
272+
{ "iss", "issuer_override" }
273+
};
274+
275+
var cca = ConfidentialClientApplicationBuilder
276+
.Create(TestConstants.ClientId)
277+
.WithAuthority("https://login.microsoftonline.com/tid")
278+
.WithHttpManager(harness.HttpManager)
279+
.WithClientClaims(certificate, extraAssertionContent, mergeWithDefaultClaims: true, sendX5C: false)
280+
.Build();
281+
282+
// Checks the client assertion for x5c and for expiration
283+
var handler = harness.HttpManager.AddTokenResponse(TokenResponseType.Valid_ClientCredentials);
284+
JwtSecurityToken assertion = null;
285+
handler.AdditionalRequestValidation = (r) => assertion = ValidateClientAssertion(r, exportedCertificate, validateStandardClaims: false);
286+
287+
AuthenticationResult result = await cca.AcquireTokenForClient(TestConstants.s_scope)
288+
.WithSendX5C(true)
289+
.ExecuteAsync()
290+
.ConfigureAwait(false);
291+
292+
Assert.IsNotNull(result.AccessToken);
293+
assertion.Claims.Single(c => c.Type == "iss").Value.Equals("issuer_override");
294+
}
295+
}
296+
297+
private JwtSecurityToken ValidateClientAssertion(HttpRequestMessage request, string expectedX5cValue, bool validateStandardClaims )
298+
{
299+
var requestContent = request.Content.ReadAsStringAsync().GetAwaiter().GetResult();
300+
var formsData = CoreHelpers.ParseKeyValueList(requestContent, '&', true, null);
301+
302+
// Check presence of client_assertion in request
303+
Assert.IsTrue(formsData.TryGetValue("client_assertion", out string encodedJwt), "Missing client_assertion from request");
304+
305+
// Check presence and value of x5c cert claim.
306+
var handler = new JwtSecurityTokenHandler();
307+
JwtSecurityToken assertionJwt = handler.ReadJwtToken(encodedJwt);
308+
if (validateStandardClaims)
309+
{
310+
Assert.AreEqual("https://login.microsoftonline.com/tid/oauth2/v2.0/token", assertionJwt.Claims.Single(c => c.Type == "aud").Value);
311+
Assert.AreEqual(TestConstants.ClientId, assertionJwt.Claims.Single(c => c.Type == "iss").Value);
312+
Assert.AreEqual(TestConstants.ClientId, assertionJwt.Claims.Single(c => c.Type == "sub").Value);
313+
}
314+
315+
// Assert extra claims
316+
Assert.AreEqual("bar", assertionJwt.Claims.Single(c => c.Type == "foo").Value);
317+
318+
// Assert exp and nbf claims
319+
long exp = long.Parse(assertionJwt.Claims.Single(c => c.Type == "exp").Value);
320+
321+
DateTimeOffset actualExpDate = DateTimeOffset.FromUnixTimeSeconds(exp);
322+
DateTimeOffset expectedExpDate = DateTimeOffset.Now + TimeSpan.FromSeconds(JsonWebToken.JwtToAadLifetimeInSeconds);
323+
CoreAssert.IsWithinRange(expectedExpDate, actualExpDate, TimeSpan.FromSeconds(5));
324+
325+
long nbf = long.Parse(assertionJwt.Claims.Single(c => c.Type == "nbf").Value);
326+
DateTimeOffset actualNbfDate = DateTimeOffset.FromUnixTimeSeconds(nbf);
327+
CoreAssert.IsWithinRange(DateTimeOffset.Now, actualNbfDate, TimeSpan.FromSeconds(5));
328+
329+
var x5c = assertionJwt.Header.FirstOrDefault(header => header.Key == "x5c");
330+
331+
Assert.AreEqual("x5c", x5c.Key, "x5c should be present");
332+
Assert.AreEqual(x5c.Value.ToString(), expectedX5cValue);
333+
334+
return assertionJwt;
335+
}
336+
223337
[TestMethod]
224338
[Description("Test for client assertion with X509 public certificate using sendCertificate")]
225339
public async Task JsonWebTokenWithX509PublicCertSendCertificateTestAsync()
@@ -694,8 +808,8 @@ public async Task RopcCcaSendsX5CAsync(bool sendX5C)
694808

695809
harness.HttpManager.AddMockHandler(
696810
CreateTokenResponseHttpHandlerWithX5CValidation(
697-
clientCredentialFlow: false,
698-
expectedX5C: sendX5C ? exportedCertificate: null));
811+
clientCredentialFlow: false,
812+
expectedX5C: sendX5C ? exportedCertificate : null));
699813

700814
var result = await (app as IByUsernameAndPassword)
701815
.AcquireTokenByUsernamePassword(
@@ -900,7 +1014,7 @@ public void EnsureNullCertDoesNotSetSerialNumberTestAsync()
9001014
.WithExperimentalFeatures()
9011015
.BuildConcrete();
9021016
});
903-
1017+
9041018
Assert.IsTrue(exception.Message.Contains("Value cannot be null"));
9051019
}
9061020
}

0 commit comments

Comments
 (0)