|
22 | 22 | using Microsoft.VisualStudio.TestTools.UnitTesting; |
23 | 23 | using static Microsoft.Identity.Client.Internal.JsonWebToken; |
24 | 24 | using Microsoft.Identity.Client.RP; |
| 25 | +using Microsoft.Identity.Client.Http; |
25 | 26 |
|
26 | 27 | namespace Microsoft.Identity.Test.Unit |
27 | 28 | { |
@@ -68,6 +69,7 @@ private static MockHttpMessageHandler CreateTokenResponseHttpHandlerWithX5CValid |
68 | 69 | var handler = new JwtSecurityTokenHandler(); |
69 | 70 | var jsonToken = handler.ReadJwtToken(encodedJwt); |
70 | 71 | var x5c = jsonToken.Header.FirstOrDefault(header => header.Key == "x5c"); |
| 72 | + |
71 | 73 | if (expectedX5C != null) |
72 | 74 | { |
73 | 75 | Assert.AreEqual("x5c", x5c.Key, "x5c should be present"); |
@@ -220,6 +222,118 @@ public async Task TestX5C( |
220 | 222 | } |
221 | 223 | } |
222 | 224 |
|
| 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 | + |
223 | 337 | [TestMethod] |
224 | 338 | [Description("Test for client assertion with X509 public certificate using sendCertificate")] |
225 | 339 | public async Task JsonWebTokenWithX509PublicCertSendCertificateTestAsync() |
@@ -694,8 +808,8 @@ public async Task RopcCcaSendsX5CAsync(bool sendX5C) |
694 | 808 |
|
695 | 809 | harness.HttpManager.AddMockHandler( |
696 | 810 | CreateTokenResponseHttpHandlerWithX5CValidation( |
697 | | - clientCredentialFlow: false, |
698 | | - expectedX5C: sendX5C ? exportedCertificate: null)); |
| 811 | + clientCredentialFlow: false, |
| 812 | + expectedX5C: sendX5C ? exportedCertificate : null)); |
699 | 813 |
|
700 | 814 | var result = await (app as IByUsernameAndPassword) |
701 | 815 | .AcquireTokenByUsernamePassword( |
@@ -900,7 +1014,7 @@ public void EnsureNullCertDoesNotSetSerialNumberTestAsync() |
900 | 1014 | .WithExperimentalFeatures() |
901 | 1015 | .BuildConcrete(); |
902 | 1016 | }); |
903 | | - |
| 1017 | + |
904 | 1018 | Assert.IsTrue(exception.Message.Contains("Value cannot be null")); |
905 | 1019 | } |
906 | 1020 | } |
|
0 commit comments