|
9 | 9 | using System.Text; |
10 | 10 | using System.Threading.Tasks; |
11 | 11 | using Microsoft.Identity.Client; |
| 12 | +using Microsoft.Identity.Client.AuthScheme; |
12 | 13 | using Microsoft.Identity.Client.Cache.Items; |
13 | 14 | using Microsoft.Identity.Client.Extensibility; |
14 | 15 | using Microsoft.Identity.Test.Integration.Infrastructure; |
|
20 | 21 |
|
21 | 22 | namespace Microsoft.Identity.Test.Integration.HeadlessTests |
22 | 23 | { |
| 24 | + internal static class MsAuth10AtPop |
| 25 | + { |
| 26 | + internal class MsAuth10AtPopOperation : IAuthenticationOperation |
| 27 | + { |
| 28 | + private readonly string _reqCnf; |
| 29 | + |
| 30 | + public MsAuth10AtPopOperation(string keyId, string reqCnf) |
| 31 | + { |
| 32 | + KeyId = keyId; |
| 33 | + _reqCnf = reqCnf; |
| 34 | + } |
| 35 | + public int TelemetryTokenType => 4; // as per TelemetryTokenTypeConstants |
| 36 | + |
| 37 | + public string AuthorizationHeaderPrefix => "Bearer"; // these tokens go over bearer |
| 38 | + |
| 39 | + public string KeyId { get; } |
| 40 | + |
| 41 | + public string AccessTokenType => "pop"; // eSTS returns token_type=pop and MSAL needs to know |
| 42 | + |
| 43 | + public void FormatResult(AuthenticationResult authenticationResult) |
| 44 | + { |
| 45 | + // no-op, adding the SHR is done by the caller |
| 46 | + } |
| 47 | + |
| 48 | + public IReadOnlyDictionary<string, string> GetTokenRequestParams() |
| 49 | + { |
| 50 | + return new Dictionary<string, string>() |
| 51 | + { |
| 52 | + {"req_cnf", Base64UrlEncoder.Encode(_reqCnf) }, |
| 53 | + {"token_type", "pop" } |
| 54 | + }; |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + internal static AcquireTokenForClientParameterBuilder WithAtPop( |
| 59 | + this AcquireTokenForClientParameterBuilder builder, |
| 60 | + string popPublicKey, |
| 61 | + string jwkClaim) |
| 62 | + { |
| 63 | + MsAuth10AtPopOperation op = new MsAuth10AtPopOperation(popPublicKey, jwkClaim); |
| 64 | + builder.WithAuthenticationExtension(new MsalAuthenticationExtension() |
| 65 | + { |
| 66 | + AuthenticationOperation = op |
| 67 | + }); |
| 68 | + return builder; |
| 69 | + } |
| 70 | + } |
| 71 | + |
23 | 72 | // These tests run on .NET FWK as well. Use the RunOn attribute to limit this. |
24 | 73 | [TestClass] |
25 | 74 | public class LegacyPopTests |
@@ -345,6 +394,84 @@ public async Task LegacyPoPAsync() |
345 | 394 | Assert.IsNotNull(ats.SingleOrDefault(a => a.KeyId == popKey.KeyId)); |
346 | 395 | } |
347 | 396 |
|
| 397 | + [TestMethod] |
| 398 | + public async Task LegacyPopUsingNewProtocol_CertThumbprinJWK_Async() |
| 399 | + { |
| 400 | + IConfidentialAppSettings settings = ConfidentialAppSettings.GetSettings(Cloud.Public); |
| 401 | + X509Certificate2 clientCredsCert = settings.GetCertificate(); |
| 402 | + |
| 403 | + var cca = ConfidentialClientApplicationBuilder |
| 404 | + .Create(settings.ClientId) |
| 405 | + .WithAuthority(settings.Authority, true) |
| 406 | + .WithCertificate(clientCredsCert) |
| 407 | + .WithExperimentalFeatures(true) |
| 408 | + .WithTestLogging() |
| 409 | + .Build(); |
| 410 | + |
| 411 | + var thumbprint = Base64UrlEncoder.Encode(clientCredsCert.GetCertHash(HashAlgorithmName.SHA256)); |
| 412 | + var reqCnf = $@"{{""kty"":""RSA"",""x5t#S256"":""{thumbprint}"",""kid"":""{thumbprint}""}}"; |
| 413 | + |
| 414 | + var result = await cca.AcquireTokenForClient(settings.AppScopes) |
| 415 | + .WithAtPop(thumbprint, reqCnf) |
| 416 | + .ExecuteAsync().ConfigureAwait(false); |
| 417 | + |
| 418 | + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); |
| 419 | + MsalAccessTokenCacheItem at = (cca.AppTokenCache as ITokenCacheInternal).Accessor.GetAllAccessTokens().Single(); |
| 420 | + Assert.AreEqual(at.KeyId, thumbprint); |
| 421 | + |
| 422 | + result = await cca.AcquireTokenForClient(settings.AppScopes) |
| 423 | + .WithAtPop(thumbprint, reqCnf) |
| 424 | + .ExecuteAsync().ConfigureAwait(false); |
| 425 | + |
| 426 | + Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource); |
| 427 | + at = (cca.AppTokenCache as ITokenCacheInternal).Accessor.GetAllAccessTokens().Single(); |
| 428 | + Assert.AreEqual(at.KeyId, thumbprint); |
| 429 | + |
| 430 | + string otherKeyId = thumbprint + "2"; |
| 431 | + result = await cca.AcquireTokenForClient(settings.AppScopes) |
| 432 | + .WithAtPop(otherKeyId, reqCnf) |
| 433 | + .ExecuteAsync().ConfigureAwait(false); |
| 434 | + |
| 435 | + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); |
| 436 | + var ats = (cca.AppTokenCache as ITokenCacheInternal).Accessor.GetAllAccessTokens(); |
| 437 | + Assert.IsNotNull(ats.SingleOrDefault(a => a.KeyId == otherKeyId)); |
| 438 | + Assert.IsNotNull(ats.SingleOrDefault(a => a.KeyId == thumbprint)); |
| 439 | + } |
| 440 | + |
| 441 | + [TestMethod] |
| 442 | + public async Task LegacyPopUsingNewProtocol_RsaKey_Async() |
| 443 | + { |
| 444 | + IConfidentialAppSettings settings = ConfidentialAppSettings.GetSettings(Cloud.Public); |
| 445 | + X509Certificate2 clientCredsCert = settings.GetCertificate(); |
| 446 | + |
| 447 | + var cca = ConfidentialClientApplicationBuilder |
| 448 | + .Create(settings.ClientId) |
| 449 | + .WithAuthority(settings.Authority, true) |
| 450 | + .WithCertificate(clientCredsCert) |
| 451 | + .WithExperimentalFeatures(true) |
| 452 | + .WithTestLogging() |
| 453 | + .Build(); |
| 454 | + |
| 455 | + RsaSecurityKey popKey = CreateRsaSecurityKey(); |
| 456 | + var reqCnf = CreateJwkClaim(popKey, SecurityAlgorithms.RsaSha256); |
| 457 | + var keyId = ComputeSHA256(reqCnf); // can be anything |
| 458 | + |
| 459 | + var result = await cca.AcquireTokenForClient(settings.AppScopes) |
| 460 | + .WithAtPop(keyId, reqCnf) |
| 461 | + .ExecuteAsync().ConfigureAwait(false); |
| 462 | + |
| 463 | + Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); |
| 464 | + MsalAccessTokenCacheItem at = (cca.AppTokenCache as ITokenCacheInternal).Accessor.GetAllAccessTokens().Single(); |
| 465 | + Assert.AreEqual(at.KeyId, keyId); |
| 466 | + |
| 467 | + result = await cca.AcquireTokenForClient(settings.AppScopes) |
| 468 | + .WithAtPop(keyId, reqCnf) |
| 469 | + .ExecuteAsync().ConfigureAwait(false); |
| 470 | + |
| 471 | + Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource); |
| 472 | + |
| 473 | + } |
| 474 | + |
348 | 475 | private static void ModifyRequestWithLegacyPop(OnBeforeTokenRequestData data, IConfidentialAppSettings settings, X509Certificate2 clientCredsCert, RsaSecurityKey popKey) |
349 | 476 | { |
350 | 477 | var clientCredsSigningCredentials = new SigningCredentials(new X509SecurityKey(clientCredsCert), SecurityAlgorithms.RsaSha256, SecurityAlgorithms.Sha256); |
@@ -449,6 +576,20 @@ private static string CreateJwkClaim(RsaSecurityKey key, string algorithm) |
449 | 576 | var parameters = key.Rsa == null ? key.Parameters : key.Rsa.ExportParameters(false); |
450 | 577 | return "{\"kty\":\"RSA\",\"n\":\"" + Base64UrlEncoder.Encode(parameters.Modulus) + "\",\"e\":\"" + Base64UrlEncoder.Encode(parameters.Exponent) + "\",\"alg\":\"" + algorithm + "\",\"kid\":\"" + key.KeyId + "\"}"; |
451 | 578 | } |
| 579 | + |
| 580 | + private static string ComputeSHA256(string token) |
| 581 | + { |
| 582 | +#if NET6_0_OR_GREATER |
| 583 | + byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(token)); |
| 584 | + return Convert.ToBase64String(hashBytes); |
| 585 | +#else |
| 586 | + using (var sha256 = SHA256.Create()) |
| 587 | + { |
| 588 | + byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(token)); |
| 589 | + return Convert.ToBase64String(hashBytes); |
| 590 | + } |
| 591 | +#endif |
| 592 | + } |
452 | 593 | } |
453 | 594 |
|
454 | 595 | } |
0 commit comments