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.13.1

### Bug fixes
- `DownstreamApi`: restored `CustomizeHttpRequestMessage` to its documented timing — it runs after the authorization header (including the `Authorization` header) is set, just before the request is sent. [#3902](https://github.com/AzureAD/microsoft-identity-web/pull/3902) had moved it before header creation (to flow the finalized request for request-binding), which regressed callers that read the header in the callback — they saw `null`. See [#3943](https://github.com/AzureAD/microsoft-identity-web/pull/3943).

## 4.13.0

### New features
Expand Down
8 changes: 5 additions & 3 deletions src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -734,9 +734,6 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
httpRequestMessage.RequestUri = uriBuilder.Uri;
}

// Opportunity to change the request message before request-binding authorization headers are created.
effectiveOptions.CustomizeHttpRequestMessage?.Invoke(httpRequestMessage);

// Obtention of the authorization header (except when calling an anonymous endpoint)
// which is done by not specifying any scopes or mTLS scheme.
if (string.Equals(effectiveOptions.ProtocolScheme, Constants.MtlsProtocolScheme, StringComparison.OrdinalIgnoreCase))
Expand Down Expand Up @@ -841,6 +838,11 @@ public Task<HttpResponseMessage> CallApiForAppAsync(
Logger.UnauthenticatedApiCall(_logger, null);
}

// Opportunity to change the request message after the authorization header (including the Authorization
// header) has been set, just before the request is sent, per CustomizeHttpRequestMessage's documented
// contract. #3902 had moved this before header creation, which regressed callers that read the header.
effectiveOptions.CustomizeHttpRequestMessage?.Invoke(httpRequestMessage);

return (authorizationHeaderInformation, credential);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,6 @@ public async Task UpdateRequestAsync_WithScopes_FlowsFinalRequestToAuthorization
ExtraQueryParameters = new Dictionary<string, string>
{
{ "fromOptions", "query-value" }
},
CustomizeHttpRequestMessage = request =>
{
request.Headers.TryAddWithoutValidation("X-From-Customizer", "customized");
}
};

Expand All @@ -185,11 +181,36 @@ public async Task UpdateRequestAsync_WithScopes_FlowsFinalRequestToAuthorization
Assert.Same(content, authorizationHeaderProvider.CapturedRequest!.Content);
Assert.Contains("fromOptions=query-value", authorizationHeaderProvider.CapturedRequest.RequestUri!.Query, StringComparison.Ordinal);
Assert.True(authorizationHeaderProvider.CapturedRequest.Headers.Contains("X-From-Options"));
Assert.True(authorizationHeaderProvider.CapturedRequest.Headers.Contains("X-From-Customizer"));
Assert.False(authorizationHeaderProvider.AuthorizationHeaderPresentWhenCaptured);
Assert.True(httpRequestMessage.Headers.Contains("Authorization"));
}

[Fact]
// Regression test for CustomizeHttpRequestMessage timing: it must run after the authorization header is set
// (its documented contract), so a callback that reads the Authorization header observes it. #3902 had moved
// it before header creation, so such callbacks saw null.
public async Task UpdateRequestAsync_CustomizeHttpRequestMessage_SeesAuthorizationHeaderAsync()
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://localhost:44352");
var content = new StringContent("test content");
string? authorizationHeaderSeenByCustomizer = null;
var options = new DownstreamApiOptions
{
Scopes = ["api://a021aff4-57ad-453a-bae8-e4192e5860f3/.default"],
BaseUrl = "https://localhost:44352",
RelativePath = "/WeatherForecast",
RequestAppToken = true,
CustomizeHttpRequestMessage = request => authorizationHeaderSeenByCustomizer = request.Headers.Authorization?.Parameter,
};

// Act
await _input.UpdateRequestWithCertificateAsync(httpRequestMessage, content, options, appToken: true, new ClaimsPrincipal(), CancellationToken.None);

// Assert - the customizer runs after the header is set, so it observes the Authorization header.
Assert.Equal("ey", authorizationHeaderSeenByCustomizer);
}

[Theory]
[InlineData("Authorization")]
[InlineData("Cookie")]
Expand Down
Loading