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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ internal static class Logger
DownstreamApiLoggingEventId.UnauthenticatedApiCall,
"[MsIdWeb] An unauthenticated call was made to the Api with null Scopes");

private static readonly Action<ILogger, string, Exception?> s_reservedHeaderIgnored =
LoggerMessage.Define<string>(
LogLevel.Warning,
DownstreamApiLoggingEventId.ReservedHeaderIgnored,
"[MsIdWeb] Header '{HeaderName}' supplied through ExtraHeaderParameters was ignored because the name is reserved for the library.");

private static readonly Action<ILogger, string, Exception?> s_duplicateHeaderIgnored =
LoggerMessage.Define<string>(
LogLevel.Warning,
DownstreamApiLoggingEventId.DuplicateHeaderIgnored,
"[MsIdWeb] Header '{HeaderName}' supplied through ExtraHeaderParameters was ignored because the request already carries a value for it.");

/// <summary>
/// Logger for handling options exceptions in DownstreamApi.
Expand All @@ -52,6 +63,25 @@ public static void HttpRequestError(
public static void UnauthenticatedApiCall(
ILogger logger,
Exception? ex) => s_unauthenticatedApiCall(logger, ex);

/// <summary>
/// Logs that an ExtraHeaderParameters entry was skipped because its name is reserved.
/// </summary>
/// <param name="logger">Logger.</param>
/// <param name="headerName">Header name that was ignored.</param>
public static void ReservedHeaderIgnored(
ILogger logger,
string headerName) => s_reservedHeaderIgnored(logger, headerName, null);

/// <summary>
/// Logs that an ExtraHeaderParameters entry was skipped because the request already
/// carries a value for the same header name.
/// </summary>
/// <param name="logger">Logger.</param>
/// <param name="headerName">Header name that was ignored.</param>
public static void DuplicateHeaderIgnored(
ILogger logger,
string headerName) => s_duplicateHeaderIgnored(logger, headerName, null);
}
}
}
99 changes: 57 additions & 42 deletions src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -582,48 +582,63 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
{
Logger.UnauthenticatedApiCall(_logger, null);
}
if (!string.IsNullOrEmpty(effectiveOptions.AcceptHeader))
{
httpRequestMessage.Headers.Accept.ParseAdd(effectiveOptions.AcceptHeader);
}

// Add extra headers if specified directly on DownstreamApiOptions
if (effectiveOptions.ExtraHeaderParameters != null)
{
foreach (var header in effectiveOptions.ExtraHeaderParameters)
{
httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}

// Add extra query parameters if specified directly on DownstreamApiOptions
if (effectiveOptions.ExtraQueryParameters != null && effectiveOptions.ExtraQueryParameters.Count > 0)
{
var uriBuilder = new UriBuilder(httpRequestMessage.RequestUri!);
var existingQuery = uriBuilder.Query;
var queryString = new StringBuilder(existingQuery);

foreach (var queryParam in effectiveOptions.ExtraQueryParameters)
{
if (queryString.Length > 1) // if there are existing query parameters
{
queryString.Append('&');
}
else if (queryString.Length == 0)
{
queryString.Append('?');
}

queryString.Append(Uri.EscapeDataString(queryParam.Key));
queryString.Append('=');
queryString.Append(Uri.EscapeDataString(queryParam.Value));
}

uriBuilder.Query = queryString.ToString().TrimStart('?');
httpRequestMessage.RequestUri = uriBuilder.Uri;
}

// Opportunity to change the request message

if (!string.IsNullOrEmpty(effectiveOptions.AcceptHeader))
{
httpRequestMessage.Headers.Accept.ParseAdd(effectiveOptions.AcceptHeader);
}

// Add extra headers if specified directly on DownstreamApiOptions.
// Skip names that are reserved for the library or already present on
// the outgoing request to keep the library-set values authoritative.
if (effectiveOptions.ExtraHeaderParameters != null)
{
foreach (var header in effectiveOptions.ExtraHeaderParameters)
{
if (ReservedHeaderNames.IsReserved(header.Key))
{
Logger.ReservedHeaderIgnored(_logger, header.Key);
continue;
}

if (httpRequestMessage.Headers.Contains(header.Key))
{
Logger.DuplicateHeaderIgnored(_logger, header.Key);
continue;
}

httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}

// Add extra query parameters if specified directly on DownstreamApiOptions
if (effectiveOptions.ExtraQueryParameters != null && effectiveOptions.ExtraQueryParameters.Count > 0)
{
var uriBuilder = new UriBuilder(httpRequestMessage.RequestUri!);
var existingQuery = uriBuilder.Query;
var queryString = new StringBuilder(existingQuery);

foreach (var queryParam in effectiveOptions.ExtraQueryParameters)
{
if (queryString.Length > 1) // if there are existing query parameters
{
queryString.Append('&');
}
else if (queryString.Length == 0)
{
queryString.Append('?');
}

queryString.Append(Uri.EscapeDataString(queryParam.Key));
queryString.Append('=');
queryString.Append(Uri.EscapeDataString(queryParam.Value));
}

uriBuilder.Query = queryString.ToString().TrimStart('?');
httpRequestMessage.RequestUri = uriBuilder.Uri;
}

// Opportunity to change the request message
effectiveOptions.CustomizeHttpRequestMessage?.Invoke(httpRequestMessage);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ internal static class DownstreamApiLoggingEventId
// DownstreamApi EventIds 100+
public static readonly EventId HttpRequestError = new(100, "HttpRequestError");
public static readonly EventId UnauthenticatedApiCall = new(101, "UnauthenticatedApiCall");
public static readonly EventId ReservedHeaderIgnored = new(102, "ReservedHeaderIgnored");
public static readonly EventId DuplicateHeaderIgnored = new(103, "DuplicateHeaderIgnored");
#pragma warning restore IDE1006 // Naming styles
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#nullable enable
Microsoft.Identity.Web.ReservedHeaderNames
static Microsoft.Identity.Web.DownstreamApi.Logger.DuplicateHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.DownstreamApi.Logger.ReservedHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.ReservedHeaderNames.IsReserved(string! headerName) -> bool
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.DuplicateHeaderIgnored -> Microsoft.Extensions.Logging.EventId
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.ReservedHeaderIgnored -> Microsoft.Extensions.Logging.EventId
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#nullable enable
Microsoft.Identity.Web.ReservedHeaderNames
static Microsoft.Identity.Web.DownstreamApi.Logger.DuplicateHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.DownstreamApi.Logger.ReservedHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.ReservedHeaderNames.IsReserved(string! headerName) -> bool
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.DuplicateHeaderIgnored -> Microsoft.Extensions.Logging.EventId
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.ReservedHeaderIgnored -> Microsoft.Extensions.Logging.EventId
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#nullable enable
Microsoft.Identity.Web.ReservedHeaderNames
static Microsoft.Identity.Web.DownstreamApi.Logger.DuplicateHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.DownstreamApi.Logger.ReservedHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.ReservedHeaderNames.IsReserved(string! headerName) -> bool
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.DuplicateHeaderIgnored -> Microsoft.Extensions.Logging.EventId
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.ReservedHeaderIgnored -> Microsoft.Extensions.Logging.EventId
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#nullable enable
Microsoft.Identity.Web.ReservedHeaderNames
static Microsoft.Identity.Web.DownstreamApi.Logger.DuplicateHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.DownstreamApi.Logger.ReservedHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.ReservedHeaderNames.IsReserved(string! headerName) -> bool
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.DuplicateHeaderIgnored -> Microsoft.Extensions.Logging.EventId
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.ReservedHeaderIgnored -> Microsoft.Extensions.Logging.EventId
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#nullable enable
Microsoft.Identity.Web.ReservedHeaderNames
static Microsoft.Identity.Web.DownstreamApi.Logger.DuplicateHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.DownstreamApi.Logger.ReservedHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.ReservedHeaderNames.IsReserved(string! headerName) -> bool
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.DuplicateHeaderIgnored -> Microsoft.Extensions.Logging.EventId
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.ReservedHeaderIgnored -> Microsoft.Extensions.Logging.EventId
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#nullable enable
Microsoft.Identity.Web.ReservedHeaderNames
static Microsoft.Identity.Web.DownstreamApi.Logger.DuplicateHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.DownstreamApi.Logger.ReservedHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.ReservedHeaderNames.IsReserved(string! headerName) -> bool
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.DuplicateHeaderIgnored -> Microsoft.Extensions.Logging.EventId
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.ReservedHeaderIgnored -> Microsoft.Extensions.Logging.EventId
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#nullable enable
Microsoft.Identity.Web.ReservedHeaderNames
static Microsoft.Identity.Web.DownstreamApi.Logger.DuplicateHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.DownstreamApi.Logger.ReservedHeaderIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! headerName) -> void
static Microsoft.Identity.Web.ReservedHeaderNames.IsReserved(string! headerName) -> bool
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.DuplicateHeaderIgnored -> Microsoft.Extensions.Logging.EventId
static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.ReservedHeaderIgnored -> Microsoft.Extensions.Logging.EventId
65 changes: 65 additions & 0 deletions src/Microsoft.Identity.Web.DownstreamApi/ReservedHeaderNames.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Reserved header names that callers must not provide through
/// <see cref="Microsoft.Identity.Abstractions.DownstreamApiOptions.ExtraHeaderParameters"/>.
/// The library either sets these itself, or they have host-level meaning that
/// should not be controlled by per-request configuration.
/// </summary>
internal static class ReservedHeaderNames
{
// Exact-match names (case-insensitive).
private static readonly HashSet<string> s_exactNames = new(StringComparer.OrdinalIgnoreCase)
{
"Authorization",
"Cookie",
"Host",
"X-Original-URL",
"X-MS-CLIENT-PRINCIPAL",
"X-MS-CLIENT-PRINCIPAL-ID",
"X-MS-CLIENT-PRINCIPAL-NAME",
"X-MS-CLIENT-PRINCIPAL-IDP",
};

// Prefix-match names (case-insensitive). Any header name starting with one of
// these prefixes is treated as reserved.
private static readonly string[] s_prefixes = new[]
{
"X-Forwarded-",
"X-MS-TOKEN-AAD-",
};

/// <summary>
/// Returns <see langword="true"/> when <paramref name="headerName"/> matches any
/// reserved exact name or reserved prefix.
/// </summary>
public static bool IsReserved(string headerName)
{
if (string.IsNullOrEmpty(headerName))
{
return false;
}

if (s_exactNames.Contains(headerName))
{
return true;
}

for (int i = 0; i < s_prefixes.Length; i++)
{
if (headerName.StartsWith(s_prefixes[i], StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}
}
}
Loading