From e393115a33f52c6f89e461e70ffd50e30a7fe2fa Mon Sep 17 00:00:00 2001
From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com>
Date: Wed, 17 Jun 2026 06:35:12 -0700
Subject: [PATCH] Add tests + docs for x-ms-tokenboundauth header via
ExtraHeaderParameters
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Azure Key Vault requires 'x-ms-tokenboundauth: true' on mTLS PoP requests to
trigger TLS renegotiation for client cert binding. Without it, AKV returns 401.
Instead of hardcoding vault host detection in a DelegatingHandler, this uses
IdWeb's existing ExtraHeaderParameters config surface — letting developers
declare the header per-service in appsettings.json.
Changes:
- Add 16 unit tests proving ExtraHeaderParameters correctly applies the header
for MSI (user-assigned + system-assigned), FIC, and sovereign cloud scenarios
- Update daemon-app-msi-mtls sample with ExtraHeaderParameters config
- Document the AKV requirement in Program.cs header comment
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../daemon-app/daemon-app-msi-mtls/Program.cs | 4 +
.../daemon-app-msi-mtls/appsettings.json | 3 +
.../MtlsPopTokenBoundAuthHeaderTests.cs | 560 ++++++++++++++++++
3 files changed, 567 insertions(+)
create mode 100644 tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/MtlsPopTokenBoundAuthHeaderTests.cs
diff --git a/tests/DevApps/daemon-app/daemon-app-msi-mtls/Program.cs b/tests/DevApps/daemon-app/daemon-app-msi-mtls/Program.cs
index b08da4ff3..8e147e16e 100644
--- a/tests/DevApps/daemon-app/daemon-app-msi-mtls/Program.cs
+++ b/tests/DevApps/daemon-app/daemon-app-msi-mtls/Program.cs
@@ -13,6 +13,10 @@
//
// Trigger: AuthorizationHeaderProviderOptions.ProtocolScheme = "MTLS_POP" in appsettings.json
// (under AcquireTokenOptions, with RequestAppToken = true).
+//
+// Important: Azure Key Vault requires "x-ms-tokenboundauth": "true" header on mTLS PoP
+// requests to trigger TLS renegotiation for client cert binding. This is configured via
+// ExtraHeaderParameters in appsettings.json. Without it, AKV returns 401.
var factory = TokenAcquirerFactory.GetDefaultInstance();
diff --git a/tests/DevApps/daemon-app/daemon-app-msi-mtls/appsettings.json b/tests/DevApps/daemon-app/daemon-app-msi-mtls/appsettings.json
index 4b8af330c..ea1b9cc6c 100644
--- a/tests/DevApps/daemon-app/daemon-app-msi-mtls/appsettings.json
+++ b/tests/DevApps/daemon-app/daemon-app-msi-mtls/appsettings.json
@@ -13,6 +13,9 @@
"ManagedIdentity": {
"UserAssignedClientId": "4b7a4b0b-ecb2-409e-879a-1e21a15ddaf6"
}
+ },
+ "ExtraHeaderParameters": {
+ "x-ms-tokenboundauth": "true"
}
},
diff --git a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/MtlsPopTokenBoundAuthHeaderTests.cs b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/MtlsPopTokenBoundAuthHeaderTests.cs
new file mode 100644
index 000000000..0a9193ab4
--- /dev/null
+++ b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/MtlsPopTokenBoundAuthHeaderTests.cs
@@ -0,0 +1,560 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Identity.Abstractions;
+using Microsoft.Identity.Web.Test.Resource;
+using Xunit;
+
+namespace Microsoft.Identity.Web.Tests
+{
+ ///
+ /// Validates that developers can configure the x-ms-tokenboundauth header
+ /// required by Azure Key Vault for mTLS PoP via ExtraHeaderParameters.
+ /// This is the recommended approach per IdWeb's extensibility model.
+ ///
+ public class MtlsPopTokenBoundAuthHeaderTests
+ {
+ private readonly DownstreamApi _downstreamApi;
+
+ public MtlsPopTokenBoundAuthHeaderTests()
+ {
+ var authorizationHeaderProvider = new TestAuthorizationHeaderProvider();
+ var httpClientFactory = new HttpClientFactoryTest();
+ var namedOptions = new TestOptionsMonitor();
+ var loggerFactory = new LoggerFactory();
+ var logger = loggerFactory.CreateLogger();
+ var provider = new CredentialsProvider(
+ loggerFactory.CreateLogger(),
+ new DefaultCredentialsLoader(), [], null);
+
+ _downstreamApi = new DownstreamApi(
+ authorizationHeaderProvider,
+ namedOptions,
+ httpClientFactory,
+ logger,
+ msalHttpClientFactory: null,
+ credentialsProvider: provider);
+ }
+
+ [Fact]
+ public async Task ExtraHeaderParameters_TokenBoundAuth_AddsHeaderToAkvRequest()
+ {
+ // Arrange — simulates appsettings.json:
+ // "AzureKeyVault": {
+ // "BaseUrl": "https://myvault.vault.azure.net/",
+ // "ProtocolScheme": "MTLS_POP",
+ // "Scopes": [ "https://vault.azure.net/.default" ],
+ // "ExtraHeaderParameters": { "x-ms-tokenboundauth": "true" }
+ // }
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.azure.net/secrets/mysecret?api-version=7.4");
+
+ var options = new DownstreamApiOptions
+ {
+ ExtraHeaderParameters = new Dictionary
+ {
+ { "x-ms-tokenboundauth", "true" }
+ }
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert
+ Assert.True(
+ httpRequestMessage.Headers.Contains("x-ms-tokenboundauth"),
+ "x-ms-tokenboundauth header should be present on AKV request");
+ Assert.Equal(
+ "true",
+ httpRequestMessage.Headers.GetValues("x-ms-tokenboundauth").Single());
+ }
+
+ [Fact]
+ public void ExtraHeaderParameters_TokenBoundAuth_IsNotReserved()
+ {
+ // Verify x-ms-tokenboundauth is not in the reserved header list
+ // (if it were, ExtraHeaderParameters would silently drop it)
+ Assert.False(
+ ReservedHeaderNames.IsReserved("x-ms-tokenboundauth"),
+ "x-ms-tokenboundauth must not be reserved — developers need to set it via ExtraHeaderParameters");
+ }
+
+ [Fact]
+ public async Task ExtraHeaderParameters_TokenBoundAuth_WorksWithSovereignCloudVaults()
+ {
+ // Arrange — China sovereign cloud AKV
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.azure.cn/secrets/mysecret?api-version=7.4");
+
+ var options = new DownstreamApiOptions
+ {
+ ExtraHeaderParameters = new Dictionary
+ {
+ { "x-ms-tokenboundauth", "true" }
+ }
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert
+ Assert.True(httpRequestMessage.Headers.Contains("x-ms-tokenboundauth"));
+ Assert.Equal("true", httpRequestMessage.Headers.GetValues("x-ms-tokenboundauth").Single());
+ }
+
+ [Fact]
+ public async Task CustomizeHttpRequestMessage_TokenBoundAuth_AddsHeaderDynamically()
+ {
+ // Arrange — demonstrates the code-based approach:
+ // options.CustomizeHttpRequestMessage = msg =>
+ // msg.Headers.TryAddWithoutValidation("x-ms-tokenboundauth", "true");
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.azure.net/secrets/mysecret?api-version=7.4");
+
+ var options = new DownstreamApiOptions
+ {
+ CustomizeHttpRequestMessage = msg =>
+ msg.Headers.TryAddWithoutValidation("x-ms-tokenboundauth", "true")
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert
+ Assert.True(httpRequestMessage.Headers.Contains("x-ms-tokenboundauth"));
+ Assert.Equal("true", httpRequestMessage.Headers.GetValues("x-ms-tokenboundauth").Single());
+ }
+
+ [Fact]
+ public async Task ExtraHeaderParameters_TokenBoundAuth_DoesNotDuplicateIfAlreadyPresent()
+ {
+ // Arrange — header already on the request (e.g., set by a DelegatingHandler upstream)
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.azure.net/secrets/mysecret?api-version=7.4");
+ httpRequestMessage.Headers.TryAddWithoutValidation("x-ms-tokenboundauth", "true");
+
+ var options = new DownstreamApiOptions
+ {
+ ExtraHeaderParameters = new Dictionary
+ {
+ { "x-ms-tokenboundauth", "true" }
+ }
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert — should still be a single value, not duplicated
+ Assert.Single(httpRequestMessage.Headers.GetValues("x-ms-tokenboundauth"));
+ }
+
+ [Fact]
+ public async Task ExtraHeaderParameters_TokenBoundAuth_CaseInsensitiveDuplicateCheck()
+ {
+ // Arrange — header present with different casing (HTTP headers are case-insensitive)
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.azure.net/secrets/mysecret?api-version=7.4");
+ httpRequestMessage.Headers.TryAddWithoutValidation("X-MS-TOKENBOUNDAUTH", "true");
+
+ var options = new DownstreamApiOptions
+ {
+ ExtraHeaderParameters = new Dictionary
+ {
+ { "x-ms-tokenboundauth", "true" }
+ }
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert — Contains is case-insensitive for HTTP headers, so no duplicate
+ Assert.Single(httpRequestMessage.Headers.GetValues("x-ms-tokenboundauth"));
+ }
+
+ [Fact]
+ public async Task ExtraHeaderParameters_TokenBoundAuth_WorksWithUsGovCloud()
+ {
+ // Arrange — US Government cloud AKV
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.usgovcloudapi.net/secrets/mysecret?api-version=7.4");
+
+ var options = new DownstreamApiOptions
+ {
+ ExtraHeaderParameters = new Dictionary
+ {
+ { "x-ms-tokenboundauth", "true" }
+ }
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert
+ Assert.True(httpRequestMessage.Headers.Contains("x-ms-tokenboundauth"));
+ Assert.Equal("true", httpRequestMessage.Headers.GetValues("x-ms-tokenboundauth").Single());
+ }
+
+ [Fact]
+ public async Task ExtraHeaderParameters_MultipleHeaders_AllApplied()
+ {
+ // Arrange — realistic scenario: tokenboundauth + a custom correlation header
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.azure.net/secrets/mysecret?api-version=7.4");
+
+ var options = new DownstreamApiOptions
+ {
+ ExtraHeaderParameters = new Dictionary
+ {
+ { "x-ms-tokenboundauth", "true" },
+ { "x-correlation-id", "test-run-001" }
+ }
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert — both headers present
+ Assert.Equal("true", httpRequestMessage.Headers.GetValues("x-ms-tokenboundauth").Single());
+ Assert.Equal("test-run-001", httpRequestMessage.Headers.GetValues("x-correlation-id").Single());
+ }
+
+ [Fact]
+ public async Task ExtraHeaderParameters_ReservedHeader_IsIgnored()
+ {
+ // Arrange — Authorization is reserved; confirm it's silently skipped
+ // while x-ms-tokenboundauth still goes through
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.azure.net/secrets/mysecret?api-version=7.4");
+
+ var options = new DownstreamApiOptions
+ {
+ ExtraHeaderParameters = new Dictionary
+ {
+ { "Authorization", "Bearer should-be-ignored" },
+ { "x-ms-tokenboundauth", "true" }
+ }
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert — tokenboundauth applied, Authorization NOT applied via ExtraHeaders
+ Assert.True(httpRequestMessage.Headers.Contains("x-ms-tokenboundauth"));
+ // The reserved "Authorization" from ExtraHeaderParameters should be silently dropped.
+ // httpRequestMessage.Headers.Authorization remains null because
+ // UpdateRequestWithCertificateAsync does not set it (that happens later in the pipeline).
+ Assert.Null(httpRequestMessage.Headers.Authorization);
+ }
+
+ [Fact]
+ public async Task ExtraHeaderParameters_EmptyDictionary_NoHeaderAdded()
+ {
+ // Arrange — empty ExtraHeaderParameters should be a no-op
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.azure.net/secrets/mysecret?api-version=7.4");
+
+ var options = new DownstreamApiOptions
+ {
+ ExtraHeaderParameters = new Dictionary()
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert — no x-ms-tokenboundauth header
+ Assert.False(httpRequestMessage.Headers.Contains("x-ms-tokenboundauth"));
+ }
+
+ [Fact]
+ public async Task CustomizeHttpRequestMessage_ConditionalLogic_OnlyAddsForVaultHosts()
+ {
+ // Arrange — demonstrates conditional header injection pattern
+ // (for developers calling multiple resources from same service config)
+ Action conditionalCustomizer = msg =>
+ {
+ if (msg.RequestUri?.Host?.EndsWith(".vault.azure.net", StringComparison.OrdinalIgnoreCase) == true)
+ {
+ msg.Headers.TryAddWithoutValidation("x-ms-tokenboundauth", "true");
+ }
+ };
+
+ // Test with AKV URL — should add header
+ var akvRequest = new HttpRequestMessage(HttpMethod.Get, "https://myvault.vault.azure.net/secrets/foo");
+ var akvOptions = new DownstreamApiOptions { CustomizeHttpRequestMessage = conditionalCustomizer };
+ await _downstreamApi.UpdateRequestWithCertificateAsync(akvRequest, null, akvOptions, false, null, CancellationToken.None);
+ Assert.True(akvRequest.Headers.Contains("x-ms-tokenboundauth"));
+
+ // Test with non-AKV URL — should NOT add header
+ var armRequest = new HttpRequestMessage(HttpMethod.Get, "https://management.azure.com/subscriptions");
+ var armOptions = new DownstreamApiOptions { CustomizeHttpRequestMessage = conditionalCustomizer };
+ await _downstreamApi.UpdateRequestWithCertificateAsync(armRequest, null, armOptions, false, null, CancellationToken.None);
+ Assert.False(armRequest.Headers.Contains("x-ms-tokenboundauth"));
+ }
+
+ // --- Full config shape tests: MSI + FIC with mTLS PoP for AKV ---
+
+ [Fact]
+ public async Task FullConfigShape_MsiMtlsPop_AzureKeyVault_HeaderApplied()
+ {
+ // Arrange — mirrors the exact appsettings.json structure a developer would use
+ // for Managed Identity mTLS PoP against Azure Key Vault:
+ //
+ // "AzureKeyVault": {
+ // "BaseUrl": "https://myvault.vault.azure.net/",
+ // "RelativePath": "secrets/mysecret?api-version=7.4",
+ // "RequestAppToken": true,
+ // "ProtocolScheme": "MTLS_POP",
+ // "Scopes": [ "https://vault.azure.net/.default" ],
+ // "AcquireTokenOptions": {
+ // "ManagedIdentity": {
+ // "UserAssignedClientId": "4b7a4b0b-ecb2-409e-879a-1e21a15ddaf6"
+ // }
+ // },
+ // "ExtraHeaderParameters": { "x-ms-tokenboundauth": "true" }
+ // }
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.azure.net/secrets/mysecret?api-version=7.4");
+
+ var options = new DownstreamApiOptions
+ {
+ BaseUrl = "https://myvault.vault.azure.net/",
+ RelativePath = "secrets/mysecret?api-version=7.4",
+ RequestAppToken = true,
+ ProtocolScheme = "MTLS_POP",
+ Scopes = new[] { "https://vault.azure.net/.default" },
+ AcquireTokenOptions = new AcquireTokenOptions
+ {
+ ManagedIdentity = new ManagedIdentityOptions
+ {
+ UserAssignedClientId = "4b7a4b0b-ecb2-409e-879a-1e21a15ddaf6"
+ }
+ },
+ ExtraHeaderParameters = new Dictionary
+ {
+ { "x-ms-tokenboundauth", "true" }
+ }
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert — header is present regardless of token acquisition path
+ Assert.True(httpRequestMessage.Headers.Contains("x-ms-tokenboundauth"));
+ Assert.Equal("true", httpRequestMessage.Headers.GetValues("x-ms-tokenboundauth").Single());
+ }
+
+ [Fact]
+ public async Task FullConfigShape_MsiMtlsPop_SystemAssigned_HeaderApplied()
+ {
+ // Arrange — System-assigned MSI (no UserAssignedClientId)
+ // "AcquireTokenOptions": { "ManagedIdentity": { } }
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.azure.net/secrets/mysecret?api-version=7.4");
+
+ var options = new DownstreamApiOptions
+ {
+ BaseUrl = "https://myvault.vault.azure.net/",
+ RequestAppToken = true,
+ ProtocolScheme = "MTLS_POP",
+ Scopes = new[] { "https://vault.azure.net/.default" },
+ AcquireTokenOptions = new AcquireTokenOptions
+ {
+ ManagedIdentity = new ManagedIdentityOptions() // system-assigned
+ },
+ ExtraHeaderParameters = new Dictionary
+ {
+ { "x-ms-tokenboundauth", "true" }
+ }
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert
+ Assert.True(httpRequestMessage.Headers.Contains("x-ms-tokenboundauth"));
+ Assert.Equal("true", httpRequestMessage.Headers.GetValues("x-ms-tokenboundauth").Single());
+ }
+
+ [Fact]
+ public async Task FullConfigShape_FicMtlsPop_AzureKeyVault_HeaderApplied()
+ {
+ // Arrange — Federated Identity Credential (FIC) with mTLS PoP against AKV.
+ // FIC uses SignedAssertionFromManagedIdentity as the credential source,
+ // but the downstream call config is the same shape — ExtraHeaderParameters
+ // applies identically regardless of how the token was acquired.
+ //
+ // "AzureKeyVault": {
+ // "BaseUrl": "https://myvault.vault.azure.net/",
+ // "RequestAppToken": true,
+ // "ProtocolScheme": "MTLS_POP",
+ // "Scopes": [ "https://vault.azure.net/.default" ],
+ // "ExtraHeaderParameters": { "x-ms-tokenboundauth": "true" }
+ // }
+ //
+ // Note: FIC credential source (SignedAssertionFromManagedIdentity) is
+ // configured under MicrosoftIdentityOptions.ClientCredentials, not here.
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://myvault.vault.azure.net/secrets/mysecret?api-version=7.4");
+
+ var options = new DownstreamApiOptions
+ {
+ BaseUrl = "https://myvault.vault.azure.net/",
+ RequestAppToken = true,
+ ProtocolScheme = "MTLS_POP",
+ Scopes = new[] { "https://vault.azure.net/.default" },
+ ExtraHeaderParameters = new Dictionary
+ {
+ { "x-ms-tokenboundauth", "true" }
+ }
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert — FIC or MSI, the header injection behavior is the same
+ Assert.True(httpRequestMessage.Headers.Contains("x-ms-tokenboundauth"));
+ Assert.Equal("true", httpRequestMessage.Headers.GetValues("x-ms-tokenboundauth").Single());
+ }
+
+ [Fact]
+ public async Task FullConfigShape_MsiMtlsPop_NonVaultResource_NoHeaderNeeded()
+ {
+ // Arrange — ARM does NOT need x-ms-tokenboundauth (it binds cert on initial handshake).
+ // This test documents that developers should NOT add ExtraHeaderParameters
+ // for non-AKV resources.
+ var httpRequestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "https://management.azure.com/subscriptions?api-version=2023-01-01");
+
+ var options = new DownstreamApiOptions
+ {
+ BaseUrl = "https://management.azure.com/",
+ RequestAppToken = true,
+ ProtocolScheme = "MTLS_POP",
+ Scopes = new[] { "https://management.azure.com/.default" },
+ AcquireTokenOptions = new AcquireTokenOptions
+ {
+ ManagedIdentity = new ManagedIdentityOptions
+ {
+ UserAssignedClientId = "4b7a4b0b-ecb2-409e-879a-1e21a15ddaf6"
+ }
+ }
+ // No ExtraHeaderParameters — ARM doesn't need it
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert — header is NOT present (correct behavior for non-AKV)
+ Assert.False(httpRequestMessage.Headers.Contains("x-ms-tokenboundauth"));
+ }
+
+ [Fact]
+ public async Task FullConfigShape_MsiMtlsPop_SovereignClouds_AllHeaderApplied()
+ {
+ // Arrange — validate all sovereign cloud AKV endpoints work with the config
+ var sovereignUrls = new[]
+ {
+ "https://myvault.vault.azure.cn/secrets/mysecret?api-version=7.4", // China
+ "https://myvault.vault.usgovcloudapi.net/secrets/mysecret?api-version=7.4" // US Gov
+ };
+
+ foreach (var url in sovereignUrls)
+ {
+ var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
+
+ var options = new DownstreamApiOptions
+ {
+ RequestAppToken = true,
+ ProtocolScheme = "MTLS_POP",
+ Scopes = new[] { "https://vault.azure.net/.default" },
+ AcquireTokenOptions = new AcquireTokenOptions
+ {
+ ManagedIdentity = new ManagedIdentityOptions()
+ },
+ ExtraHeaderParameters = new Dictionary
+ {
+ { "x-ms-tokenboundauth", "true" }
+ }
+ };
+
+ // Act
+ await _downstreamApi.UpdateRequestWithCertificateAsync(
+ httpRequestMessage, null, options, false, null, CancellationToken.None);
+
+ // Assert
+ Assert.True(
+ httpRequestMessage.Headers.Contains("x-ms-tokenboundauth"),
+ $"Header should be present for sovereign URL: {url}");
+ }
+ }
+
+ // --- Test infrastructure (follows ExtraParametersTests.cs pattern) ---
+
+ private class TestAuthorizationHeaderProvider : IAuthorizationHeaderProvider
+ {
+ public Task CreateAuthorizationHeaderForAppAsync(
+ string scopes,
+ AuthorizationHeaderProviderOptions? downstreamApiOptions = null,
+ CancellationToken cancellationToken = default)
+ => Task.FromResult("Bearer ey");
+
+ public Task CreateAuthorizationHeaderForUserAsync(
+ IEnumerable scopes,
+ AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null,
+ ClaimsPrincipal? claimsPrincipal = null,
+ CancellationToken cancellationToken = default)
+ => Task.FromResult("Bearer ey");
+
+ public Task CreateAuthorizationHeaderAsync(
+ IEnumerable scopes,
+ AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null,
+ ClaimsPrincipal? claimsPrincipal = null,
+ CancellationToken cancellationToken = default)
+ => Task.FromResult("Bearer ey");
+ }
+
+ private class TestOptionsMonitor : IOptionsMonitor
+ {
+ public DownstreamApiOptions CurrentValue => new DownstreamApiOptions();
+ public DownstreamApiOptions Get(string? name) => new DownstreamApiOptions();
+ public DownstreamApiOptions Get(string name, string key) => new DownstreamApiOptions();
+ public IDisposable OnChange(Action listener)
+ => throw new NotImplementedException();
+ }
+ }
+}