Skip to content

Commit 2e7b8ca

Browse files
authored
Add Support for Custom Saml Bearer in HttpRequest Headers (#3273)
* Change Headers.Add() to Headers.TryAddWithoutValidation() to ensure saml bearer headers are supported * Use TryAddWithoutValidation only for specific scenario
1 parent ee18453 commit 2e7b8ca

File tree

2 files changed

+63
-1
lines changed

2 files changed

+63
-1
lines changed

src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ internal partial class DownstreamApi : IDownstreamApi
2828
private readonly IOptionsMonitor<DownstreamApiOptions> _namedDownstreamApiOptions;
2929
private const string Authorization = "Authorization";
3030
protected readonly ILogger<DownstreamApi> _logger;
31+
private const string AuthSchemeDstsSamlBearer = "http://schemas.microsoft.com/dsts/saml2-bearer";
3132

3233
/// <summary>
3334
/// Constructor.
@@ -522,7 +523,15 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
522523
user,
523524
cancellationToken).ConfigureAwait(false);
524525

525-
httpRequestMessage.Headers.Add(Authorization, authorizationHeader);
526+
if (authorizationHeader.StartsWith(AuthSchemeDstsSamlBearer, StringComparison.OrdinalIgnoreCase))
527+
{
528+
// TryAddWithoutValidation method bypasses strict validation, allowing non-standard headers to be added for custom Header schemes that cannot be parsed.
529+
httpRequestMessage.Headers.TryAddWithoutValidation(Authorization, authorizationHeader);
530+
}
531+
else
532+
{
533+
httpRequestMessage.Headers.Add(Authorization, authorizationHeader);
534+
}
526535
}
527536
else
528537
{

tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@ namespace Microsoft.Identity.Web.Tests
2424
public class DownstreamApiTests
2525
{
2626
private readonly IAuthorizationHeaderProvider _authorizationHeaderProvider;
27+
private readonly IAuthorizationHeaderProvider _authorizationHeaderProviderSaml;
2728
private readonly IHttpClientFactory _httpClientFactory;
2829
private readonly IOptionsMonitor<DownstreamApiOptions> _namedDownstreamApiOptions;
2930
private readonly ILogger<DownstreamApi> _logger;
3031
private readonly DownstreamApi _input;
32+
private readonly DownstreamApi _inputSaml;
3133

3234
public DownstreamApiTests()
3335
{
3436
_authorizationHeaderProvider = new MyAuthorizationHeaderProvider();
37+
_authorizationHeaderProviderSaml = new MySamlAuthorizationHeaderProvider();
3538
_httpClientFactory = new HttpClientFactoryTest();
3639
_namedDownstreamApiOptions = new MyMonitor();
3740
_logger = new LoggerFactory().CreateLogger<DownstreamApi>();
@@ -41,6 +44,12 @@ public DownstreamApiTests()
4144
_namedDownstreamApiOptions,
4245
_httpClientFactory,
4346
_logger);
47+
48+
_inputSaml = new DownstreamApi(
49+
_authorizationHeaderProviderSaml,
50+
_namedDownstreamApiOptions,
51+
_httpClientFactory,
52+
_logger);
4453
}
4554

4655
[Fact]
@@ -123,6 +132,32 @@ public async Task UpdateRequestAsync_WithScopes_AddsAuthorizationHeaderToRequest
123132
Assert.Equal(options.AcquireTokenOptions.ExtraQueryParameters, DownstreamApi.CallerSDKDetails);
124133
}
125134

135+
[Theory]
136+
[InlineData(true)]
137+
[InlineData(false)]
138+
139+
public async Task UpdateRequestAsync_WithScopes_AddsSamlAuthorizationHeaderToRequestAsync(bool appToken)
140+
{
141+
// Arrange
142+
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://example.com");
143+
var content = new StringContent("test content");
144+
var options = new DownstreamApiOptions
145+
{
146+
Scopes = ["scope1", "scope2"],
147+
BaseUrl = "https://localhost:44321/WeatherForecast"
148+
};
149+
var user = new ClaimsPrincipal();
150+
151+
// Act
152+
await _inputSaml.UpdateRequestAsync(httpRequestMessage, content, options, appToken, user, CancellationToken.None);
153+
154+
// Assert
155+
Assert.True(httpRequestMessage.Headers.Contains("Authorization"));
156+
Assert.Equal("http://schemas.microsoft.com/dsts/saml2-bearer ey", httpRequestMessage.Headers.GetValues("Authorization").FirstOrDefault());
157+
Assert.Equal("application/json", httpRequestMessage.Headers.Accept.Single().MediaType);
158+
Assert.Equal(options.AcquireTokenOptions.ExtraQueryParameters, DownstreamApi.CallerSDKDetails);
159+
}
160+
126161
[Fact]
127162
public void SerializeInput_ReturnsCorrectHttpContent()
128163
{
@@ -420,5 +455,23 @@ public Task<string> CreateAuthorizationHeaderAsync(IEnumerable<string> scopes, A
420455
return Task.FromResult("Bearer ey");
421456
}
422457
}
458+
459+
public class MySamlAuthorizationHeaderProvider : IAuthorizationHeaderProvider
460+
{
461+
public Task<string> CreateAuthorizationHeaderForAppAsync(string scopes, AuthorizationHeaderProviderOptions? downstreamApiOptions = null, CancellationToken cancellationToken = default)
462+
{
463+
return Task.FromResult("http://schemas.microsoft.com/dsts/saml2-bearer ey");
464+
}
465+
466+
public Task<string> CreateAuthorizationHeaderForUserAsync(IEnumerable<string> scopes, AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, ClaimsPrincipal? claimsPrincipal = null, CancellationToken cancellationToken = default)
467+
{
468+
return Task.FromResult("http://schemas.microsoft.com/dsts/saml2-bearer ey");
469+
}
470+
471+
public Task<string> CreateAuthorizationHeaderAsync(IEnumerable<string> scopes, AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, ClaimsPrincipal? claimsPrincipal = null, CancellationToken cancellationToken = default)
472+
{
473+
return Task.FromResult("http://schemas.microsoft.com/dsts/saml2-bearer ey");
474+
}
475+
}
423476
}
424477

0 commit comments

Comments
 (0)