Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions src/Microsoft.Identity.Web.TokenAcquisition/MsAuth10AtPop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
Expand All @@ -19,7 +20,8 @@ internal static AcquireTokenForClientParameterBuilder WithAtPop(
X509Certificate2 clientCertificate,
string popPublicKey,
string jwkClaim,
string clientId)
string clientId,
bool sendX5C)
{
_ = Throws.IfNull(popPublicKey);
_ = Throws.IfNull(jwkClaim);
Expand All @@ -31,10 +33,12 @@ internal static AcquireTokenForClientParameterBuilder WithAtPop(
clientCertificate,
data.RequestUri.AbsoluteUri,
jwkClaim,
clientId);
clientId,
sendX5C);

data.BodyParameters.Remove("client_assertion");
data.BodyParameters.Add("request", signedAssertion);

return Task.CompletedTask;
});

Expand All @@ -45,7 +49,8 @@ internal static AcquireTokenForClientParameterBuilder WithAtPop(
X509Certificate2 certificate,
string audience,
string jwkClaim,
string clientId)
string clientId,
bool sendX5C)
{
// no need to add exp, nbf as JsonWebTokenHandler will add them by default
var claims = new Dictionary<string, object>()
Expand All @@ -57,14 +62,24 @@ internal static AcquireTokenForClientParameterBuilder WithAtPop(
{ "pop_jwk", jwkClaim }
};

var signingCredentials = new X509SigningCredentials(certificate);
var securityTokenDescriptor = new SecurityTokenDescriptor
{
{
Claims = claims,
SigningCredentials = new X509SigningCredentials(certificate)
SigningCredentials = signingCredentials
};

var handler = new JsonWebTokenHandler();
return handler.CreateToken(securityTokenDescriptor);
if (sendX5C)
{
string x5cValue = Convert.ToBase64String(certificate.GetRawCertData());
securityTokenDescriptor.AdditionalHeaderClaims =
new Dictionary<string, object>() { { "x5c", new List<string> { x5cValue } } };
}

JsonWebTokenHandler tokenHandler = new JsonWebTokenHandler();
string token = tokenHandler.CreateToken(securityTokenDescriptor);

return token;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,8 @@ public async Task<AuthenticationResult> GetAuthenticationResultForAppAsync(
}
if (!string.IsNullOrEmpty(tokenAcquisitionOptions.PopPublicKey))
{
_logger.LogInformation("Regular SHR POP with server nonce configured");

if (string.IsNullOrEmpty(tokenAcquisitionOptions.PopClaim))
{
builder.WithProofOfPosessionKeyId(tokenAcquisitionOptions.PopPublicKey, "pop");
Expand All @@ -431,11 +433,21 @@ public async Task<AuthenticationResult> GetAuthenticationResultForAppAsync(
}
else
{
if (mergedOptions.SendX5C)
{
_logger.LogInformation("MSAuth POP configured with SN/I");
}
else
{
_logger.LogWarning("MSAuth POP configured with pinned certificate. This configuration is being deprecated.");
}

builder.WithAtPop(
application.AppConfig.ClientCredentialCertificate,
tokenAcquisitionOptions.PopPublicKey!,
tokenAcquisitionOptions.PopClaim!,
application.AppConfig.ClientId);
application.AppConfig.ClientId,
mergedOptions.SendX5C);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,31 @@

namespace Microsoft.Identity.Web.Test.Common.Mocks
{
/// <summary>
/// HttpClient that serves Http responses for testing purposes. Instance Discovery is added by default.
/// </summary>
public class MockHttpClientFactory : IMsalHttpClientFactory, IDisposable
{
// MSAL will statically cache instance discovery, so we need to add
private static bool s_instanceDiscoveryAdded = false;
private static object s_instanceDiscoveryLock = new object();

public MockHttpClientFactory()
{
// Auto-add instance discovery call, but only once per process
if (!s_instanceDiscoveryAdded)
{
lock (s_instanceDiscoveryLock)
{
if (!s_instanceDiscoveryAdded)
{
_httpMessageHandlerQueue.Enqueue(MockHttpCreator.CreateInstanceDiscoveryMockHandler());
s_instanceDiscoveryAdded = true;
}
}
}
}

/// <inheritdoc />
public void Dispose()
{
Expand Down Expand Up @@ -43,6 +66,8 @@ public MockHttpMessageHandler AddMockHandler(MockHttpMessageHandler handler)

public HttpClient GetHttpClient()
{


HttpMessageHandler messageHandler;

Assert.NotEmpty(_httpMessageHandlerQueue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
Expand All @@ -12,13 +13,13 @@ namespace Microsoft.Identity.Web.Test.Common.Mocks
{
public class MockHttpMessageHandler : HttpMessageHandler
{
public Func<MockHttpMessageHandler, MockHttpMessageHandler> ReplaceMockHttpMessageHandler;
private readonly bool _ignoreInstanceDiscovery;

#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public MockHttpMessageHandler()
public MockHttpMessageHandler(bool ignoreInstanceDiscovery = true)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{

_ignoreInstanceDiscovery = ignoreInstanceDiscovery;
}
public HttpResponseMessage ResponseMessage { get; set; }

Expand All @@ -32,37 +33,21 @@ public MockHttpMessageHandler()
/// Once the http message is executed, this property holds the request message.
/// </summary>
public HttpRequestMessage ActualRequestMessage { get; private set; }
public Dictionary<string, string> ActualRequestPostData { get; private set; }

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var uri = request.RequestUri;

ActualRequestMessage = request;

if (ExceptionToThrow != null)
{
throw ExceptionToThrow;
}

var uri = request.RequestUri;
Assert.NotNull(uri);

//Intercept instance discovery requests and serve a response.
//Also, requeue the current mock handler for MSAL's next request.
#if NET6_0_OR_GREATER
if (uri.AbsoluteUri.Contains("/discovery/instance", StringComparison.OrdinalIgnoreCase))
#else
if (uri.AbsoluteUri.Contains("/discovery/instance"))
#endif
{
ReplaceMockHttpMessageHandler(this);

var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(TestConstants.DiscoveryJsonResponse),
};

return Task.FromResult(responseMessage);
}

if (!string.IsNullOrEmpty(ExpectedUrl))
{
Assert.Equal(
Expand All @@ -76,12 +61,17 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques

Assert.Equal(ExpectedMethod, request.Method);




if (request.Method != HttpMethod.Get && request.Content != null)
{
string postData = request.Content.ReadAsStringAsync().Result;
string postData = await request.Content.ReadAsStringAsync();
ActualRequestPostData = QueryStringParser.ParseKeyValueList(postData, '&', true, false);

}

return Task.FromResult(ResponseMessage);
return ResponseMessage;
}
}
}
105 changes: 105 additions & 0 deletions tests/Microsoft.Identity.Web.Test.Common/Mocks/QueryStringParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;

namespace Microsoft.Identity.Web.Test.Common.Mocks
{
public static class QueryStringParser
{
public static Dictionary<string, string> ParseKeyValueList(string input, char delimiter, bool urlDecode,
bool lowercaseKeys)
{
var response = new Dictionary<string, string>();

var queryPairs = SplitWithQuotes(input, delimiter);

foreach (string queryPair in queryPairs)
{
var pair = SplitWithQuotes(queryPair, '=');

if (pair.Count == 2 && !string.IsNullOrWhiteSpace(pair[0]) && !string.IsNullOrWhiteSpace(pair[1]))
{
string key = pair[0];
string value = pair[1];

// Url decoding is needed for parsing OAuth response, but not for parsing WWW-Authenticate header in 401 challenge
if (urlDecode)
{
key = UrlDecode(key);
value = UrlDecode(value);
}

if (lowercaseKeys)
{
key = key.Trim().ToLowerInvariant();
}

value = value.Trim().Trim('\"').Trim();

response[key] = value;
}
}

return response;
}

public static Dictionary<string, string> ParseKeyValueList(string input, char delimiter, bool urlDecode)
{
return ParseKeyValueList(input, delimiter, urlDecode, true);
}

private static string UrlDecode(string message)
{
if (string.IsNullOrEmpty(message))
{
return message;
}

message = message.Replace("+", "%20");

Check warning on line 60 in tests/Microsoft.Identity.Web.Test.Common/Mocks/QueryStringParser.cs

View workflow job for this annotation

GitHub Actions / Analyse

'string.Replace(string, string?)' has a method overload that takes a 'StringComparison' parameter. Replace this call in 'Microsoft.Identity.Web.Test.Common.Mocks.QueryStringParser.UrlDecode(string)' with a call to 'string.Replace(string, string?, System.StringComparison)' for clarity of intent. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1307)
message = Uri.UnescapeDataString(message);

return message;
}

internal static IReadOnlyList<string> SplitWithQuotes(string input, char delimiter)
{
if (string.IsNullOrWhiteSpace(input))
{
return Array.Empty<string>();
}

var items = new List<string>();

int startIndex = 0;
bool insideString = false;
string item;
for (int i = 0; i < input.Length; i++)
{
if (input[i] == delimiter && !insideString)
{
item = input.Substring(startIndex, i - startIndex);
if (!string.IsNullOrWhiteSpace(item.Trim()))
{
items.Add(item);
}

startIndex = i + 1;
}
else if (input[i] == '"')
{
insideString = !insideString;
}
}

item = input.Substring(startIndex);
if (!string.IsNullOrWhiteSpace(item.Trim()))
{
items.Add(item);
}

return items;
}
}
}
4 changes: 0 additions & 4 deletions tests/Microsoft.Identity.Web.Test/CacheExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,6 @@ private static async Task<AuthenticationResult> CreateAppAndGetTokenAsync(
if (addTokenMock)
{
mockHttp.AddMockHandler(tokenHandler);

//Enables the mock handler to requeue requests that have been intercepted for instance discovery for example
tokenHandler.ReplaceMockHttpMessageHandler = mockHttp.AddMockHandler;
}

var confidentialApp = ConfidentialClientApplicationBuilder
Expand Down Expand Up @@ -251,7 +248,6 @@ private static async Task<AuthenticationResult> CreateAppAndGetTokenAsync(
var result = await confidentialApp.AcquireTokenForClient(new[] { TestConstants.s_scopeForApp })
.ExecuteAsync().ConfigureAwait(false);

tokenHandler.ReplaceMockHttpMessageHandler = null!;
return result;
}

Expand Down
Loading
Loading