-
Notifications
You must be signed in to change notification settings - Fork 254
Update the ROPC flow CCA to pass the send x5c to MSAL #3671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -188,5 +188,200 @@ await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync( | |
|
|
||
| Assert.StartsWith(IDWebErrorMessage.MissingIdentityConfiguration, exception.Message, System.StringComparison.Ordinal); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that SendX5C=true results in x5c claim being included in the client assertion. | ||
| /// This test examines the actual HTTP request to verify x5c presence in the JWT header. | ||
| /// </summary> | ||
| [Fact] | ||
| public async Task RopcFlow_WithSendX5CTrue_IncludesX5CInClientAssertion() | ||
| { | ||
| // Arrange | ||
| var factory = InitTokenAcquirerFactoryForRopcWithCertificate(sendX5C: true); | ||
| IServiceProvider serviceProvider = factory.Build(); | ||
|
|
||
| var mockHttpClient = serviceProvider.GetRequiredService<IMsalHttpClientFactory>() as MockHttpClientFactory; | ||
|
|
||
| var mockHandler = MockHttpCreator.CreateClientCredentialTokenHandler(); | ||
| mockHttpClient!.AddMockHandler(mockHandler); | ||
|
|
||
| IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>(); | ||
|
|
||
| // Create claims principal with username and password for pure ROPC flow | ||
| var claims = new List<System.Security.Claims.Claim> | ||
| { | ||
| new System.Security.Claims.Claim(ClaimConstants.Username, "testuser@contoso.com"), | ||
| new System.Security.Claims.Claim(ClaimConstants.Password, "testpassword123") | ||
| }; | ||
| var claimsPrincipal = new System.Security.Claims.ClaimsPrincipal( | ||
| new Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity(claims)); | ||
|
|
||
| // Act | ||
| string result = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync( | ||
| new[] { "https://graph.microsoft.com/.default" }, | ||
| authorizationHeaderProviderOptions: null, | ||
| claimsPrincipal: claimsPrincipal); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(result); | ||
| Assert.Equal("Bearer header.payload.signature", result); | ||
|
|
||
| // Verify the request was made | ||
| Assert.NotNull(mockHandler.ActualRequestMessage); | ||
| Assert.NotNull(mockHandler.ActualRequestPostData); | ||
|
|
||
| // Verify it's ROPC flow | ||
| Assert.True(mockHandler.ActualRequestPostData.ContainsKey("grant_type")); | ||
| Assert.Equal("password", mockHandler.ActualRequestPostData["grant_type"]); | ||
|
|
||
| // Verify x5c is present in client_assertion JWT header | ||
| string? clientAssertion = GetClientAssertionFromPostData(mockHandler.ActualRequestPostData); | ||
| if (clientAssertion != null) | ||
| { | ||
| string jwtHeader = DecodeJwtHeader(clientAssertion); | ||
| // With SendX5C=true, the header should contain "x5c" claim | ||
| Assert.Contains("\"x5c\"", jwtHeader, StringComparison.Ordinal); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tests that SendX5C=false results in NO x5c claim in the client assertion. | ||
| /// This verifies that the x5c certificate chain is excluded when SendX5C=false. | ||
| /// </summary> | ||
| [Fact] | ||
| public async Task RopcFlow_WithSendX5CFalse_DoesNotIncludeX5CInClientAssertion() | ||
| { | ||
| // Arrange | ||
| var factory = InitTokenAcquirerFactoryForRopcWithCertificate(sendX5C: false); | ||
| IServiceProvider serviceProvider = factory.Build(); | ||
|
|
||
| var mockHttpClient = serviceProvider.GetRequiredService<IMsalHttpClientFactory>() as MockHttpClientFactory; | ||
|
|
||
| var mockHandler = MockHttpCreator.CreateClientCredentialTokenHandler(); | ||
| mockHttpClient!.AddMockHandler(mockHandler); | ||
|
|
||
| IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>(); | ||
|
|
||
| // Create claims principal with username and password | ||
| var claims = new List<System.Security.Claims.Claim> | ||
| { | ||
| new System.Security.Claims.Claim(ClaimConstants.Username, "user@contoso.com"), | ||
| new System.Security.Claims.Claim(ClaimConstants.Password, "password123") | ||
| }; | ||
| var claimsPrincipal = new System.Security.Claims.ClaimsPrincipal( | ||
| new Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity(claims)); | ||
|
|
||
| // Act | ||
| string result = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync( | ||
| new[] { "https://graph.microsoft.com/.default" }, | ||
| authorizationHeaderProviderOptions: null, | ||
| claimsPrincipal: claimsPrincipal); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(result); | ||
| Assert.NotNull(mockHandler.ActualRequestMessage); | ||
| Assert.NotNull(mockHandler.ActualRequestPostData); | ||
|
|
||
| // Verify it's ROPC flow | ||
| Assert.True(mockHandler.ActualRequestPostData.ContainsKey("grant_type")); | ||
| Assert.Equal("password", mockHandler.ActualRequestPostData["grant_type"]); | ||
|
|
||
| // Verify x5c is NOT present in client_assertion JWT header | ||
| string? clientAssertion = GetClientAssertionFromPostData(mockHandler.ActualRequestPostData); | ||
| if (clientAssertion != null) | ||
| { | ||
| string jwtHeader = DecodeJwtHeader(clientAssertion); | ||
| // With SendX5C=false, the header should NOT contain "x5c" claim | ||
| Assert.DoesNotContain("\"x5c\"", jwtHeader, StringComparison.Ordinal); | ||
| } | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are any of these helpers generic enough to add to the Tests.Common project as TestHelpers?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are very specific to validating that the x5c is propagated to MSAL and included in the request to ests. |
||
| /// <summary> | ||
| /// Extracts the client_assertion parameter from HTTP POST data. | ||
| /// </summary> | ||
| /// <param name="postData">The HTTP POST data dictionary.</param> | ||
| /// <returns>The client_assertion JWT string, or null if not present.</returns> | ||
| private static string? GetClientAssertionFromPostData(Dictionary<string, string> postData) | ||
| { | ||
| return postData.ContainsKey("client_assertion") ? postData["client_assertion"] : null; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Decodes the header portion of a JWT (JSON Web Token). | ||
| /// Converts base64url encoding to standard base64, then decodes to UTF-8 string. | ||
| /// </summary> | ||
| /// <param name="jwt">The complete JWT string in format: header.payload.signature</param> | ||
| /// <returns>The decoded JWT header as a JSON string.</returns> | ||
| private static string DecodeJwtHeader(string jwt) | ||
| { | ||
| // Split JWT into parts (header.payload.signature) | ||
| var parts = jwt.Split('.'); | ||
| if (parts.Length < 2) | ||
| { | ||
| return string.Empty; | ||
| } | ||
|
|
||
| // Convert base64url to base64 | ||
| string base64 = parts[0].Replace('-', '+').Replace('_', '/'); | ||
|
|
||
| // Add padding if needed | ||
| switch (base64.Length % 4) | ||
| { | ||
| case 2: base64 += "=="; break; | ||
| case 3: base64 += "="; break; | ||
| } | ||
|
|
||
| // Decode base64 to bytes, then to UTF-8 string | ||
| return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(base64)); | ||
| } | ||
|
|
||
| private TokenAcquirerFactory InitTokenAcquirerFactoryForRopcWithCertificate(bool sendX5C) | ||
| { | ||
| TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); | ||
| TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); | ||
|
|
||
| var mockHttpFactory = new MockHttpClientFactory(); | ||
|
|
||
| tokenAcquirerFactory.Services.Configure<MicrosoftIdentityApplicationOptions>(options => | ||
| { | ||
| options.Instance = "https://login.microsoftonline.com/"; | ||
| options.TenantId = "f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; | ||
| options.ClientId = "idu773ld-e38d-jud3-45lk-d1b09a74a8ca"; | ||
| options.SendX5C = sendX5C; // Set the SendX5C flag | ||
|
|
||
| // SendX5C is only meaningful with certificate credentials | ||
| // Certificate is used for CLIENT authentication, username/password for USER authentication (ROPC) | ||
| options.ClientCredentials = [ | ||
| CertificateDescription.FromCertificate(CreateTestCertificate()) | ||
| ]; | ||
| }); | ||
|
|
||
| // Add MockedHttpClientFactory | ||
| tokenAcquirerFactory.Services.AddSingleton<IMsalHttpClientFactory>(mockHttpFactory); | ||
|
|
||
| return tokenAcquirerFactory; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a minimal self-signed certificate for testing purposes. | ||
| /// In unit tests, the mock HTTP handlers don't actually validate the certificate. | ||
| /// </summary> | ||
| private static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateTestCertificate() | ||
| { | ||
| // Create a minimal self-signed certificate for testing | ||
| // The certificate details don't matter for unit tests as HTTP calls are mocked | ||
| using var rsa = System.Security.Cryptography.RSA.Create(2048); | ||
| var request = new System.Security.Cryptography.X509Certificates.CertificateRequest( | ||
| "CN=TestCertificate", | ||
| rsa, | ||
| System.Security.Cryptography.HashAlgorithmName.SHA256, | ||
| System.Security.Cryptography.RSASignaturePadding.Pkcs1); | ||
|
|
||
| var certificate = request.CreateSelfSigned( | ||
| DateTimeOffset.UtcNow.AddDays(-1), | ||
| DateTimeOffset.UtcNow.AddDays(365)); | ||
|
|
||
| return certificate; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.