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
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 4.12.3

### Bug fixes
- Apply reserved-header handling on the request-clone path used for challenge retries and mTLS PoP, consistently with `ExtraHeaderParameters`, and broaden the reserved `X-MS-TOKEN-` prefix to cover the whole `X-MS-TOKEN-*` family rather than only `X-MS-TOKEN-AAD-*`.

## 4.12.2

### Bug fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -813,8 +813,9 @@ private static async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpR
// Copy headers
foreach (var header in originalRequest.Headers)
{
// Skip Authorization header as it will be set by the handler
if (header.Key != "Authorization")
// The Authorization header is set by the handler. Reserved headers are handled
// consistently with ExtraHeaderParameters and are not carried over to the clone.
if (header.Key != "Authorization" && !ReservedHeaderNames.IsReserved(header.Key))
{
clonedRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ internal static class ReservedHeaderNames
private static readonly string[] s_prefixes = new[]
{
"X-Forwarded-",
"X-MS-TOKEN-AAD-",
// Reserve the whole X-MS-TOKEN- family (any X-MS-TOKEN-* header, not only X-MS-TOKEN-AAD-*).
"X-MS-TOKEN-",
};

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@ public async Task UpdateRequestAsync_ExtraHeaderParameters_ReservedNames_AreIgno
[InlineData("X-MS-TOKEN-AAD-ID-TOKEN")]
[InlineData("X-MS-TOKEN-AAD-ACCESS-TOKEN")]
[InlineData("x-ms-token-aad-refresh-token")]
[InlineData("X-MS-TOKEN-EXAMPLE-ACCESS-TOKEN")]
[InlineData("x-ms-token-example-refresh-token")]
public async Task UpdateRequestAsync_ExtraHeaderParameters_ReservedPrefixes_AreIgnored(string headerName)
{
// Arrange
Expand Down
69 changes: 69 additions & 0 deletions tests/Microsoft.Identity.Web.Test/ReservedHeaderCloneTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Identity.Web.Test
{
// Verifies reserved headers are dropped when a request is cloned (challenge retry / mTLS PoP),
// consistently with ExtraHeaderParameters handling, and that the reserved X-MS-TOKEN- prefix
// covers the whole X-MS-TOKEN-* family.
public class ReservedHeaderCloneTests
{
[Theory]
// Exact reserved names.
[InlineData("Authorization", true)]
[InlineData("Cookie", true)]
[InlineData("Host", true)]
[InlineData("X-MS-CLIENT-PRINCIPAL", true)]
[InlineData("x-ms-client-principal-id", true)]
// AAD token headers stay reserved.
[InlineData("X-MS-TOKEN-AAD-ID-TOKEN", true)]
[InlineData("x-ms-token-aad-refresh-token", true)]
// Any other X-MS-TOKEN- header is reserved by the broadened prefix.
[InlineData("X-MS-TOKEN-EXAMPLE-ACCESS-TOKEN", true)]
[InlineData("x-ms-token-example-refresh-token", true)]
// Forwarded headers stay reserved.
[InlineData("X-Forwarded-For", true)]
// Ordinary headers are not reserved.
[InlineData("Accept", false)]
[InlineData("X-Custom-Header", false)]
[InlineData("X-MS-Something-Else", false)]
[InlineData("", false)]
public void IsReserved_ClassifiesHeaders(string headerName, bool expected)
{
Assert.Equal(expected, ReservedHeaderNames.IsReserved(headerName));
}

[Fact]
public async Task CloneHttpRequestMessageAsync_DropsReservedHeaders_KeepsOrdinaryHeaders()
{
// Arrange: an original request carrying Authorization, reserved headers, and an ordinary one.
var original = new HttpRequestMessage(HttpMethod.Get, "https://example.com/resource");
original.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "sample-token");
original.Headers.TryAddWithoutValidation("X-MS-CLIENT-PRINCIPAL", "sample-principal");
original.Headers.TryAddWithoutValidation("X-MS-TOKEN-EXAMPLE-ACCESS-TOKEN", "sample-token-value");
original.Headers.TryAddWithoutValidation("X-Forwarded-For", "10.0.0.1");
original.Headers.TryAddWithoutValidation("X-Custom-Header", "keep-me");

// Act: invoke the private static clone helper.
var method = typeof(MicrosoftIdentityMessageHandler).GetMethod(
"CloneHttpRequestMessageAsync",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
var clone = await (Task<HttpRequestMessage>)method!.Invoke(null, new object[] { original })!;

// Assert: reserved headers (and Authorization, set later by the handler) are not copied.
Assert.False(clone.Headers.Contains("Authorization"));
Assert.False(clone.Headers.Contains("X-MS-CLIENT-PRINCIPAL"));
Assert.False(clone.Headers.Contains("X-MS-TOKEN-EXAMPLE-ACCESS-TOKEN"));
Assert.False(clone.Headers.Contains("X-Forwarded-For"));
// Ordinary header survives the clone.
Assert.True(clone.Headers.Contains("X-Custom-Header"));
}
}
}
Loading