From ae5c69e8a8904a963310564fe6749a8e25dd4bbc Mon Sep 17 00:00:00 2001 From: Saravanan Thanu Date: Fri, 28 Feb 2025 07:49:23 -0800 Subject: [PATCH 1/3] Change Headers.Add() to Headers.TryAddWithoutValidation() to ensure saml bearer headers are supported --- .../DownstreamApi.cs | 2 +- .../DownstreamApiTests.cs | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs index d55d89def..484d8f1dd 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs +++ b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs @@ -522,7 +522,7 @@ public Task CallApiForAppAsync( user, cancellationToken).ConfigureAwait(false); - httpRequestMessage.Headers.Add(Authorization, authorizationHeader); + httpRequestMessage.Headers.TryAddWithoutValidation(Authorization, authorizationHeader); } else { diff --git a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs index 1e0bce09a..fe356b089 100644 --- a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs +++ b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs @@ -24,14 +24,17 @@ namespace Microsoft.Identity.Web.Tests public class DownstreamApiTests { private readonly IAuthorizationHeaderProvider _authorizationHeaderProvider; + private readonly IAuthorizationHeaderProvider _authorizationHeaderProviderSaml; private readonly IHttpClientFactory _httpClientFactory; private readonly IOptionsMonitor _namedDownstreamApiOptions; private readonly ILogger _logger; private readonly DownstreamApi _input; + private readonly DownstreamApi _inputSaml; public DownstreamApiTests() { _authorizationHeaderProvider = new MyAuthorizationHeaderProvider(); + _authorizationHeaderProviderSaml = new MySamlAuthorizationHeaderProvider(); _httpClientFactory = new HttpClientFactoryTest(); _namedDownstreamApiOptions = new MyMonitor(); _logger = new LoggerFactory().CreateLogger(); @@ -41,6 +44,12 @@ public DownstreamApiTests() _namedDownstreamApiOptions, _httpClientFactory, _logger); + + _inputSaml = new DownstreamApi( + _authorizationHeaderProviderSaml, + _namedDownstreamApiOptions, + _httpClientFactory, + _logger); } [Fact] @@ -123,6 +132,32 @@ public async Task UpdateRequestAsync_WithScopes_AddsAuthorizationHeaderToRequest Assert.Equal(options.AcquireTokenOptions.ExtraQueryParameters, DownstreamApi.CallerSDKDetails); } + [Theory] + [InlineData(true)] + [InlineData(false)] + + public async Task UpdateRequestAsync_WithScopes_AddsSamlAuthorizationHeaderToRequestAsync(bool appToken) + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://example.com"); + var content = new StringContent("test content"); + var options = new DownstreamApiOptions + { + Scopes = ["scope1", "scope2"], + BaseUrl = "https://localhost:44321/WeatherForecast" + }; + var user = new ClaimsPrincipal(); + + // Act + await _inputSaml.UpdateRequestAsync(httpRequestMessage, content, options, appToken, user, CancellationToken.None); + + // Assert + Assert.True(httpRequestMessage.Headers.Contains("Authorization")); + Assert.Equal("http://schemas.microsoft.com/dsts/saml2-bearer ey", httpRequestMessage.Headers.GetValues("Authorization").FirstOrDefault()); + Assert.Equal("application/json", httpRequestMessage.Headers.Accept.Single().MediaType); + Assert.Equal(options.AcquireTokenOptions.ExtraQueryParameters, DownstreamApi.CallerSDKDetails); + } + [Fact] public void SerializeInput_ReturnsCorrectHttpContent() { @@ -420,5 +455,23 @@ public Task CreateAuthorizationHeaderAsync(IEnumerable scopes, A return Task.FromResult("Bearer ey"); } } + + public class MySamlAuthorizationHeaderProvider : IAuthorizationHeaderProvider + { + public Task CreateAuthorizationHeaderForAppAsync(string scopes, AuthorizationHeaderProviderOptions? downstreamApiOptions = null, CancellationToken cancellationToken = default) + { + return Task.FromResult("http://schemas.microsoft.com/dsts/saml2-bearer ey"); + } + + public Task CreateAuthorizationHeaderForUserAsync(IEnumerable scopes, AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, ClaimsPrincipal? claimsPrincipal = null, CancellationToken cancellationToken = default) + { + return Task.FromResult("http://schemas.microsoft.com/dsts/saml2-bearer ey"); + } + + public Task CreateAuthorizationHeaderAsync(IEnumerable scopes, AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, ClaimsPrincipal? claimsPrincipal = null, CancellationToken cancellationToken = default) + { + return Task.FromResult("http://schemas.microsoft.com/dsts/saml2-bearer ey"); + } + } } From 762f8618137479ce3b8c1ce18b90a60e4bb87e93 Mon Sep 17 00:00:00 2001 From: Saravanan Thanu Date: Sun, 2 Mar 2025 09:23:52 -0800 Subject: [PATCH 2/3] Add comment for change --- src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs index 484d8f1dd..4b6fde311 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs +++ b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs @@ -522,6 +522,7 @@ public Task CallApiForAppAsync( user, cancellationToken).ConfigureAwait(false); + // The TryAddWithoutValidation method is used to bypass the strict checks on format of Authorization header httpRequestMessage.Headers.TryAddWithoutValidation(Authorization, authorizationHeader); } else From 66e599f2c273bc34a45c76c7334abbf7961652de Mon Sep 17 00:00:00 2001 From: Saravanan Thanu Date: Mon, 3 Mar 2025 09:36:44 -0800 Subject: [PATCH 3/3] Use TryAddWithoutValidation only for specific scenario --- .../DownstreamApi.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs index 4b6fde311..eb498f990 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs +++ b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs @@ -28,6 +28,7 @@ internal partial class DownstreamApi : IDownstreamApi private readonly IOptionsMonitor _namedDownstreamApiOptions; private const string Authorization = "Authorization"; protected readonly ILogger _logger; + private const string AuthSchemeDstsSamlBearer = "http://schemas.microsoft.com/dsts/saml2-bearer"; /// /// Constructor. @@ -522,8 +523,15 @@ public Task CallApiForAppAsync( user, cancellationToken).ConfigureAwait(false); - // The TryAddWithoutValidation method is used to bypass the strict checks on format of Authorization header - httpRequestMessage.Headers.TryAddWithoutValidation(Authorization, authorizationHeader); + if (authorizationHeader.StartsWith(AuthSchemeDstsSamlBearer, StringComparison.OrdinalIgnoreCase)) + { + // TryAddWithoutValidation method bypasses strict validation, allowing non-standard headers to be added for custom Header schemes that cannot be parsed. + httpRequestMessage.Headers.TryAddWithoutValidation(Authorization, authorizationHeader); + } + else + { + httpRequestMessage.Headers.Add(Authorization, authorizationHeader); + } } else {