From 6a04517b0e695431c17fd1c21e19480c825f7ea8 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 17 Jun 2026 15:13:47 +0100 Subject: [PATCH 1/3] Sidecar: gate AgentIdentity parameters behind per-route AllowOverrides flag AgentIdentity, AgentUsername, and AgentUserId query parameters were applied unconditionally on all sidecar endpoints, regardless of the per-route AllowOverrides configuration. This meant that even on unauthenticated routes where optionsOverride parameters were correctly ignored, agent identity parameters were still honoured. Move the AgentOverrides.SetOverrides() call inside the existing allowOverrides check in both AuthorizationHeaderEndpoint and DownstreamApiEndpoint, so agent identity parameters follow the same per-route gating as all other caller- supplied overrides. No change in behaviour for authenticated routes (AllowOverrides defaults to true). Unauthenticated routes (AllowOverrides defaults to false) will now correctly ignore agent identity parameters unless the operator explicitly opts in. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Endpoints/AuthorizationHeaderEndpoint.cs | 5 +- .../Endpoints/DownstreamApiEndpoint.cs | 5 +- .../Sidecar.Tests/AgentOverrideGatingTests.cs | 306 ++++++++++++++++++ 3 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 tests/E2E Tests/Sidecar.Tests/AgentOverrideGatingTests.cs diff --git a/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs b/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs index 0f96297ea..b90816a68 100644 --- a/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs +++ b/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs @@ -117,7 +117,10 @@ public static void AddAuthorizationHeaderRequestEndpoints(this WebApplication ap statusCode: StatusCodes.Status400BadRequest); } - AgentOverrides.SetOverrides(options, requestParameters.AgentIdentity, requestParameters.AgentUsername, requestParameters.AgentUserId); + if (allowOverrides) + { + AgentOverrides.SetOverrides(options, requestParameters.AgentIdentity, requestParameters.AgentUsername, requestParameters.AgentUserId); + } string authorizationHeader; diff --git a/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs b/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs index c241fc451..4f511d91f 100644 --- a/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs +++ b/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs @@ -117,7 +117,10 @@ private static async Task, ProblemHttpResult>> D statusCode: StatusCodes.Status400BadRequest); } - AgentOverrides.SetOverrides(options, requestParameters.AgentIdentity, requestParameters.AgentUsername, requestParameters.AgentUserId); + if (allowOverrides) + { + AgentOverrides.SetOverrides(options, requestParameters.AgentIdentity, requestParameters.AgentUsername, requestParameters.AgentUserId); + } HttpContent? content = null; diff --git a/tests/E2E Tests/Sidecar.Tests/AgentOverrideGatingTests.cs b/tests/E2E Tests/Sidecar.Tests/AgentOverrideGatingTests.cs new file mode 100644 index 000000000..d1ef249ed --- /dev/null +++ b/tests/E2E Tests/Sidecar.Tests/AgentOverrideGatingTests.cs @@ -0,0 +1,306 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Net; +using System.Net.Http.Headers; +using System.Security.Claims; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; +using Xunit; + +namespace Sidecar.Tests; + +/// +/// Verifies that AgentIdentity, AgentUsername, and AgentUserId +/// query parameters are subject to the per-route Sidecar:AllowOverrides gate +/// and are not applied on routes where overrides are disabled. +/// +public class AgentOverrideGatingTests : IClassFixture +{ + private readonly SidecarApiFactory _factory; + + // The key used by WithAgentUserIdentity / WithAgentIdentity to store the + // agent identity in AcquireTokenOptions.ExtraParameters. + private const string AgentIdentityKey = "IDWEB_AGENT_IDENTITY"; + + public AgentOverrideGatingTests(SidecarApiFactory factory) + { + _factory = factory; + } + + private HttpClient CreateClient( + Dictionary sidecarConfig, + OptionsCapturingAuthorizationHeaderProvider capture, + string apiName = "test-api") + { + return _factory.WithWebHostBuilder(builder => + { + builder.ConfigureAppConfiguration((_, config) => + { + config.AddInMemoryCollection(sidecarConfig); + }); + + builder.ConfigureServices(services => + { + TestAuthenticationHandler.AddAlwaysSucceedTestAuthentication(services); + services.AddSingleton(capture); + services.Configure(apiName, options => + { + options.BaseUrl = "https://api.example.com"; + options.RelativePath = "/test"; + options.Scopes = new[] { "User.Read" }; + }); + }); + }).CreateClient(); + } + + private static void AddBearer(HttpClient client) => + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "valid-test-token"); + + // ── AuthorizationHeader (authenticated) ────────────────────────────── + + [Fact] + public async Task AuthorizationHeader_OverridesAllowed_AppliesAgentIdentity() + { + var capture = new OptionsCapturingAuthorizationHeaderProvider(); + var client = CreateClient( + new Dictionary + { + { "Sidecar:AllowOverrides:GetAuthorizationHeader", "true" }, + }, + capture); + AddBearer(client); + + var response = await client.GetAsync( + "/AuthorizationHeader/test-api?AgentIdentity=agent-app-id&AgentUsername=testuser@contoso.com"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(capture.LastOptions); + Assert.NotNull(capture.LastOptions!.AcquireTokenOptions.ExtraParameters); + Assert.True(capture.LastOptions.AcquireTokenOptions.ExtraParameters!.ContainsKey(AgentIdentityKey)); + Assert.Equal("agent-app-id", capture.LastOptions.AcquireTokenOptions.ExtraParameters[AgentIdentityKey]); + } + + [Fact] + public async Task AuthorizationHeader_OverridesDisabled_IgnoresAgentIdentity() + { + var capture = new OptionsCapturingAuthorizationHeaderProvider(); + var client = CreateClient( + new Dictionary + { + { "Sidecar:AllowOverrides:GetAuthorizationHeader", "false" }, + }, + capture); + AddBearer(client); + + var response = await client.GetAsync( + "/AuthorizationHeader/test-api?AgentIdentity=agent-app-id&AgentUsername=testuser@contoso.com"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(capture.LastOptions); + // Agent identity should NOT have been applied. + var extras = capture.LastOptions!.AcquireTokenOptions.ExtraParameters; + Assert.True( + extras is null || !extras.ContainsKey(AgentIdentityKey), + "Agent identity should be ignored when overrides are disabled."); + } + + // ── AuthorizationHeaderUnauthenticated ──────────────────────────────── + + [Fact] + public async Task AuthorizationHeaderUnauthenticated_DefaultConfig_IgnoresAgentIdentity() + { + var capture = new OptionsCapturingAuthorizationHeaderProvider(); + // No explicit config — default is GetAuthorizationHeaderUnauthenticated = false. + var client = CreateClient(new Dictionary(), capture); + + var response = await client.GetAsync( + "/AuthorizationHeaderUnauthenticated/test-api?AgentIdentity=agent-app-id&AgentUsername=testuser@contoso.com"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(capture.LastOptions); + var extras = capture.LastOptions!.AcquireTokenOptions.ExtraParameters; + Assert.True( + extras is null || !extras.ContainsKey(AgentIdentityKey), + "Agent identity should be ignored on unauthenticated route by default."); + } + + [Fact] + public async Task AuthorizationHeaderUnauthenticated_OverridesEnabled_AppliesAgentIdentity() + { + var capture = new OptionsCapturingAuthorizationHeaderProvider(); + var client = CreateClient( + new Dictionary + { + { "Sidecar:AllowOverrides:GetAuthorizationHeaderUnauthenticated", "true" }, + }, + capture); + + var response = await client.GetAsync( + "/AuthorizationHeaderUnauthenticated/test-api?AgentIdentity=agent-app-id&AgentUsername=testuser@contoso.com"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(capture.LastOptions); + Assert.NotNull(capture.LastOptions!.AcquireTokenOptions.ExtraParameters); + Assert.True(capture.LastOptions.AcquireTokenOptions.ExtraParameters!.ContainsKey(AgentIdentityKey)); + } + + // ── DownstreamApi (authenticated) ──────────────────────────────────── + + [Fact] + public async Task DownstreamApi_OverridesDisabled_IgnoresAgentIdentity() + { + var capture = new OptionsCapturingAuthorizationHeaderProvider(); + var captureHandler = new CapturingHttpMessageHandler(HttpStatusCode.OK); + + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureAppConfiguration((_, config) => + { + config.AddInMemoryCollection(new Dictionary + { + { "Sidecar:AllowOverrides:CallDownstreamApi", "false" }, + }); + }); + + builder.ConfigureServices(services => + { + TestAuthenticationHandler.AddAlwaysSucceedTestAuthentication(services); + services.AddSingleton(capture); + services.Configure("test-api", options => + { + options.BaseUrl = "https://api.example.com"; + options.RelativePath = "/test"; + options.HttpMethod = HttpMethod.Get.Method; + options.Scopes = new[] { "User.Read" }; + }); + services.AddSingleton(new SingleHandlerHttpClientFactory(captureHandler)); + }); + }).CreateClient(); + AddBearer(client); + + var response = await client.PostAsync( + "/DownstreamApi/test-api?AgentIdentity=agent-app-id&AgentUsername=testuser@contoso.com", + content: null); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(capture.LastOptions); + var extras = capture.LastOptions!.AcquireTokenOptions.ExtraParameters; + Assert.True( + extras is null || !extras.ContainsKey(AgentIdentityKey), + "Agent identity should be ignored when overrides are disabled on DownstreamApi."); + } + + // ── DownstreamApiUnauthenticated ───────────────────────────────────── + + [Fact] + public async Task DownstreamApiUnauthenticated_DefaultConfig_IgnoresAgentIdentity() + { + var capture = new OptionsCapturingAuthorizationHeaderProvider(); + var captureHandler = new CapturingHttpMessageHandler(HttpStatusCode.OK); + + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureAppConfiguration((_, config) => + { + config.AddInMemoryCollection(new Dictionary()); + }); + + builder.ConfigureServices(services => + { + TestAuthenticationHandler.AddAlwaysSucceedTestAuthentication(services); + services.AddSingleton(capture); + services.Configure("test-api", options => + { + options.BaseUrl = "https://api.example.com"; + options.RelativePath = "/test"; + options.HttpMethod = HttpMethod.Get.Method; + options.Scopes = new[] { "User.Read" }; + }); + services.AddSingleton(new SingleHandlerHttpClientFactory(captureHandler)); + }); + }).CreateClient(); + + var response = await client.PostAsync( + "/DownstreamApiUnauthenticated/test-api?AgentIdentity=agent-app-id&AgentUsername=testuser@contoso.com", + content: null); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(capture.LastOptions); + var extras = capture.LastOptions!.AcquireTokenOptions.ExtraParameters; + Assert.True( + extras is null || !extras.ContainsKey(AgentIdentityKey), + "Agent identity should be ignored on unauthenticated DownstreamApi route by default."); + } + + // ── Helpers ────────────────────────────────────────────────────────── + + /// + /// Captures the passed to + /// so tests can assert on agent + /// identity fields. + /// + private sealed class OptionsCapturingAuthorizationHeaderProvider : IAuthorizationHeaderProvider + { + public AuthorizationHeaderProviderOptions? LastOptions { get; private set; } + + public Task CreateAuthorizationHeaderAsync( + IEnumerable scopes, + AuthorizationHeaderProviderOptions? options = null, + ClaimsPrincipal? claimsPrincipal = null, + CancellationToken cancellationToken = default) + { + LastOptions = options; + return Task.FromResult("Bearer test-token"); + } + + public Task CreateAuthorizationHeaderForAppAsync( + string scopes, + AuthorizationHeaderProviderOptions? downstreamApiOptions = null, + CancellationToken cancellationToken = default) + { + LastOptions = downstreamApiOptions; + return Task.FromResult("Bearer test-token"); + } + + public Task CreateAuthorizationHeaderForUserAsync( + IEnumerable scopes, + AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, + ClaimsPrincipal? claimsPrincipal = null, + CancellationToken cancellationToken = default) + { + LastOptions = authorizationHeaderProviderOptions; + return Task.FromResult("Bearer test-token"); + } + } + + private sealed class CapturingHttpMessageHandler : HttpMessageHandler + { + private readonly HttpStatusCode _statusCode; + + public CapturingHttpMessageHandler(HttpStatusCode statusCode) + { + _statusCode = statusCode; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage(_statusCode)); + } + } + + private sealed class SingleHandlerHttpClientFactory : IHttpClientFactory + { + private readonly HttpMessageHandler _handler; + + public SingleHandlerHttpClientFactory(HttpMessageHandler handler) + { + _handler = handler; + } + + public HttpClient CreateClient(string name) => new(_handler, disposeHandler: false); + } +} From 2f44f47141737441aa25c9803ae475eb55c002b5 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 17 Jun 2026 15:20:21 +0100 Subject: [PATCH 2/3] Sidecar: update endpoint descriptions and README for AllowOverrides clarity - Updated WithDescription strings on all four sidecar endpoints to clarify that agent identity parameters are subject to AllowOverrides gating. - Updated README to document agent identity parameter behavior under AllowOverrides and the runtime composition table. - Regenerated OpenAPI spec to reflect description changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Endpoints/AuthorizationHeaderEndpoint.cs | 6 +- .../Endpoints/DownstreamApiEndpoint.cs | 2 + .../Microsoft.Identity.Web.Sidecar.json | 76 +++++++++++-------- src/Microsoft.Identity.Web.Sidecar/README.md | 8 +- 4 files changed, 57 insertions(+), 35 deletions(-) diff --git a/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs b/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs index b90816a68..57fd04e9c 100644 --- a/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs +++ b/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs @@ -38,9 +38,10 @@ public static void AddAuthorizationHeaderRequestEndpoints(this WebApplication ap ProducesProblem(StatusCodes.Status401Unauthorized). WithSummary("Get an authorization header for a configured downstream API."). WithDescription( - "This endpoint will use the identity of the authenticated request to acquire an authorization header." + + "This endpoint will use the identity of the authenticated request to acquire an authorization header. " + "Use dotted query parameters prefixed with 'optionsOverride.' to override call settings with respect to the configuration. " + "Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:GetAuthorizationHeader' (default: true). " + + "Agent identity parameters (AgentIdentity, AgentUsername, AgentUserId) are also subject to this setting. " + "'optionsOverride.BaseUrl' is always ignored. " + "Examples:\n" + " ?optionsOverride.Scopes=User.Read&optionsOverride.Scopes=Mail.Read\n" + @@ -66,9 +67,10 @@ public static void AddAuthorizationHeaderRequestEndpoints(this WebApplication ap ProducesProblem(StatusCodes.Status401Unauthorized). WithSummary("Get an authorization header for a configured downstream API using this configured client credentials."). WithDescription( - "This endpoint will use the configured client credentials to acquire an authorization header." + + "This endpoint will use the configured client credentials to acquire an authorization header. " + "Use dotted query parameters prefixed with 'optionsOverride.' to override call settings with respect to the configuration. " + "Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:GetAuthorizationHeaderUnauthenticated' (default: false). " + + "Agent identity parameters (AgentIdentity, AgentUsername, AgentUserId) are also subject to this setting. " + "'optionsOverride.BaseUrl' is always ignored. " + "Examples:\n" + " ?optionsOverride.Scopes=User.Read&optionsOverride.Scopes=Mail.Read\n" + diff --git a/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs b/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs index 4f511d91f..74b8a6a82 100644 --- a/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs +++ b/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs @@ -42,6 +42,7 @@ public static void AddDownstreamApiRequestEndpoints(this WebApplication app) WithDescription( "Override downstream call options using dotted query parameters prefixed with 'optionsOverride.'. " + "Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:CallDownstreamApi' (default: true). " + + "Agent identity parameters (AgentIdentity, AgentUsername, AgentUserId) are also subject to this setting. " + "'optionsOverride.BaseUrl' is always ignored. " + "Examples:\n" + " ?optionsOverride.Scopes=User.Read\n" + @@ -69,6 +70,7 @@ public static void AddDownstreamApiRequestEndpoints(this WebApplication app) WithDescription( "Override downstream call options using dotted query parameters prefixed with 'optionsOverride.'. " + "Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:CallDownstreamApiUnauthenticated' (default: false). " + + "Agent identity parameters (AgentIdentity, AgentUsername, AgentUserId) are also subject to this setting. " + "'optionsOverride.BaseUrl' is always ignored. " + "Examples:\n" + " ?optionsOverride.Scopes=User.Read\n" + diff --git a/src/Microsoft.Identity.Web.Sidecar/OpenAPI/Microsoft.Identity.Web.Sidecar.json b/src/Microsoft.Identity.Web.Sidecar/OpenAPI/Microsoft.Identity.Web.Sidecar.json index 6966a9695..32f4860b1 100644 --- a/src/Microsoft.Identity.Web.Sidecar/OpenAPI/Microsoft.Identity.Web.Sidecar.json +++ b/src/Microsoft.Identity.Web.Sidecar/OpenAPI/Microsoft.Identity.Web.Sidecar.json @@ -1,5 +1,5 @@ { - "openapi": "3.0.1", + "openapi": "3.1.1", "info": { "title": "Microsoft.Identity.Web.Sidecar | v1", "version": "1.0.0" @@ -51,7 +51,7 @@ "Microsoft.Identity.Web.Sidecar" ], "summary": "Get an authorization header for a configured downstream API.", - "description": "This endpoint will use the identity of the authenticated request to acquire an authorization header.Use dotted query parameters prefixed with 'optionsOverride.' to override call settings with respect to the configuration. Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:GetAuthorizationHeader' (default: true). 'optionsOverride.BaseUrl' is always ignored. Examples:\n ?optionsOverride.Scopes=User.Read&optionsOverride.Scopes=Mail.Read\n ?optionsOverride.RequestAppToken=true&optionsOverride.Scopes=https://graph.microsoft.com/.default\n ?optionsOverride.AcquireTokenOptions.Tenant=GUID\nRepeat parameters like 'optionsOverride.Scopes' to add multiple scopes.", + "description": "This endpoint will use the identity of the authenticated request to acquire an authorization header. Use dotted query parameters prefixed with 'optionsOverride.' to override call settings with respect to the configuration. Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:GetAuthorizationHeader' (default: true). Agent identity parameters (AgentIdentity, AgentUsername, AgentUserId) are also subject to this setting. 'optionsOverride.BaseUrl' is always ignored. Examples:\n ?optionsOverride.Scopes=User.Read&optionsOverride.Scopes=Mail.Read\n ?optionsOverride.RequestAppToken=true&optionsOverride.Scopes=https://graph.microsoft.com/.default\n ?optionsOverride.AcquireTokenOptions.Tenant=GUID\nRepeat parameters like 'optionsOverride.Scopes' to add multiple scopes.", "operationId": "AuthorizationHeader", "parameters": [ { @@ -91,7 +91,6 @@ "name": "optionsOverride.Scopes", "in": "query", "description": "Repeatable. Each occurrence adds one scope. Example: optionsOverride.Scopes=User.Read", - "style": "form", "schema": { "type": "string" } @@ -156,9 +155,9 @@ { "name": "optionsOverride.AcquireTokenOptions.ForceRefresh", "in": "query", - "description": "boolean", + "description": "true = bypass token cache.", "schema": { - "type": "true = bypass token cache." + "type": "boolean" } }, { @@ -250,7 +249,7 @@ "Microsoft.Identity.Web.Sidecar" ], "summary": "Get an authorization header for a configured downstream API using this configured client credentials.", - "description": "This endpoint will use the configured client credentials to acquire an authorization header.Use dotted query parameters prefixed with 'optionsOverride.' to override call settings with respect to the configuration. Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:GetAuthorizationHeaderUnauthenticated' (default: false). 'optionsOverride.BaseUrl' is always ignored. Examples:\n ?optionsOverride.Scopes=User.Read&optionsOverride.Scopes=Mail.Read\n ?optionsOverride.RequestAppToken=true&optionsOverride.Scopes=https://graph.microsoft.com/.default\n ?optionsOverride.AcquireTokenOptions.Tenant=GUID\nRepeat parameters like 'optionsOverride.Scopes' to add multiple scopes.", + "description": "This endpoint will use the configured client credentials to acquire an authorization header. Use dotted query parameters prefixed with 'optionsOverride.' to override call settings with respect to the configuration. Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:GetAuthorizationHeaderUnauthenticated' (default: false). Agent identity parameters (AgentIdentity, AgentUsername, AgentUserId) are also subject to this setting. 'optionsOverride.BaseUrl' is always ignored. Examples:\n ?optionsOverride.Scopes=User.Read&optionsOverride.Scopes=Mail.Read\n ?optionsOverride.RequestAppToken=true&optionsOverride.Scopes=https://graph.microsoft.com/.default\n ?optionsOverride.AcquireTokenOptions.Tenant=GUID\nRepeat parameters like 'optionsOverride.Scopes' to add multiple scopes.", "operationId": "AuthorizationHeaderUnauthenticated", "parameters": [ { @@ -290,7 +289,6 @@ "name": "optionsOverride.Scopes", "in": "query", "description": "Repeatable. Each occurrence adds one scope. Example: optionsOverride.Scopes=User.Read", - "style": "form", "schema": { "type": "string" } @@ -355,9 +353,9 @@ { "name": "optionsOverride.AcquireTokenOptions.ForceRefresh", "in": "query", - "description": "boolean", + "description": "true = bypass token cache.", "schema": { - "type": "true = bypass token cache." + "type": "boolean" } }, { @@ -449,7 +447,7 @@ "Microsoft.Identity.Web.Sidecar" ], "summary": "Invoke a configured downstream API through the sidecar using the authenticated identity.", - "description": "Override downstream call options using dotted query parameters prefixed with 'optionsOverride.'. Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:CallDownstreamApi' (default: true). 'optionsOverride.BaseUrl' is always ignored. Examples:\n ?optionsOverride.Scopes=User.Read\n ?optionsOverride.Scopes=User.Read&optionsOverride.Scopes=Mail.Read\n ?optionsOverride.AcquireTokenOptions.Tenant=GUID\n ?optionsOverride.RequestAppToken=true&optionsOverride.Scopes=https://graph.microsoft.com/.default", + "description": "Override downstream call options using dotted query parameters prefixed with 'optionsOverride.'. Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:CallDownstreamApi' (default: true). Agent identity parameters (AgentIdentity, AgentUsername, AgentUserId) are also subject to this setting. 'optionsOverride.BaseUrl' is always ignored. Examples:\n ?optionsOverride.Scopes=User.Read\n ?optionsOverride.Scopes=User.Read&optionsOverride.Scopes=Mail.Read\n ?optionsOverride.AcquireTokenOptions.Tenant=GUID\n ?optionsOverride.RequestAppToken=true&optionsOverride.Scopes=https://graph.microsoft.com/.default", "operationId": "DownstreamApi", "parameters": [ { @@ -489,7 +487,6 @@ "name": "optionsOverride.Scopes", "in": "query", "description": "Repeatable. Each occurrence adds one scope. Example: optionsOverride.Scopes=User.Read", - "style": "form", "schema": { "type": "string" } @@ -554,9 +551,9 @@ { "name": "optionsOverride.AcquireTokenOptions.ForceRefresh", "in": "query", - "description": "boolean", + "description": "true = bypass token cache.", "schema": { - "type": "true = bypass token cache." + "type": "boolean" } }, { @@ -648,7 +645,7 @@ "Microsoft.Identity.Web.Sidecar" ], "summary": "Invoke a configured downstream API through the sidecar using the configured client credentials.", - "description": "Override downstream call options using dotted query parameters prefixed with 'optionsOverride.'. Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:CallDownstreamApiUnauthenticated' (default: false). 'optionsOverride.BaseUrl' is always ignored. Examples:\n ?optionsOverride.Scopes=User.Read\n ?optionsOverride.Scopes=User.Read&optionsOverride.Scopes=Mail.Read\n ?optionsOverride.AcquireTokenOptions.Tenant=GUID\n ?optionsOverride.RequestAppToken=true&optionsOverride.Scopes=https://graph.microsoft.com/.default", + "description": "Override downstream call options using dotted query parameters prefixed with 'optionsOverride.'. Whether overrides are honoured is controlled by 'Sidecar:AllowOverrides:CallDownstreamApiUnauthenticated' (default: false). Agent identity parameters (AgentIdentity, AgentUsername, AgentUserId) are also subject to this setting. 'optionsOverride.BaseUrl' is always ignored. Examples:\n ?optionsOverride.Scopes=User.Read\n ?optionsOverride.Scopes=User.Read&optionsOverride.Scopes=Mail.Read\n ?optionsOverride.AcquireTokenOptions.Tenant=GUID\n ?optionsOverride.RequestAppToken=true&optionsOverride.Scopes=https://graph.microsoft.com/.default", "operationId": "DownstreamApiUnauthenticated", "parameters": [ { @@ -688,7 +685,6 @@ "name": "optionsOverride.Scopes", "in": "query", "description": "Repeatable. Each occurrence adds one scope. Example: optionsOverride.Scopes=User.Read", - "style": "form", "schema": { "type": "string" } @@ -753,9 +749,9 @@ { "name": "optionsOverride.AcquireTokenOptions.ForceRefresh", "in": "query", - "description": "boolean", + "description": "true = bypass token cache.", "schema": { - "type": "true = bypass token cache." + "type": "boolean" } }, { @@ -864,7 +860,11 @@ "type": "object", "properties": { "statusCode": { - "type": "integer", + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], "format": "int32" }, "headers": { @@ -877,8 +877,10 @@ } }, "content": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] } } }, @@ -887,25 +889,37 @@ "type": "object", "properties": { "type": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "title": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "status": { - "type": "integer", - "format": "int32", - "nullable": true + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "null", + "integer", + "string" + ], + "format": "int32" }, "detail": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] }, "instance": { - "type": "string", - "nullable": true + "type": [ + "null", + "string" + ] } } }, diff --git a/src/Microsoft.Identity.Web.Sidecar/README.md b/src/Microsoft.Identity.Web.Sidecar/README.md index afc79fb90..4b621911b 100644 --- a/src/Microsoft.Identity.Web.Sidecar/README.md +++ b/src/Microsoft.Identity.Web.Sidecar/README.md @@ -119,13 +119,17 @@ Complete documentation is provided [here](./OpenAPI/Microsoft.Identity.Web.Sidec All token-acquisition endpoints accept dotted query parameters prefixed with `optionsOverride.`; they merge into a `DownstreamApis` profile through [`BindableDownstreamApiOptions`](Models/BindableDownstreamApiOptions.cs). +Whether overrides are honoured is controlled by the per-route `Sidecar:AllowOverrides` configuration flags. Authenticated routes allow overrides by default; unauthenticated routes ignore them unless the operator explicitly opts in. See [`SidecarOptions`](Configuration/SidecarOptions.cs) for details. + +`optionsOverride.BaseUrl` is always ignored regardless of the override flag. + Examples: - `?optionsOverride.Scopes=User.Read&optionsOverride.Scopes=Mail.Read` - `?optionsOverride.RequestAppToken=true` - `?optionsOverride.AcquireTokenOptions.Tenant=` - `?optionsOverride.RelativePath=me/messages` -Agent impersonation hints: +Agent identity parameters are also subject to the per-route override flag: - `AgentIdentity=` - `AgentUsername=upn@contoso.com` - `AgentUserId=` @@ -148,5 +152,5 @@ Agent impersonation hints: | Authentication & authorization | [`Program`](Program.cs) wires `AddMicrosoftIdentityWebApi`, optional scope enforcement, and agent identity overrides. | | Endpoints | [`ValidateRequestEndpoints`](Endpoints/ValidateRequestEndpoints.cs), [`AuthorizationHeaderEndpoint`](Endpoints/AuthorizationHeaderEndpoint.cs), and [`DownstreamApiEndpoint`](Endpoints/DownstreamApiEndpoint.cs). | | Downstream API | [`BindableDownstreamApiOptions`](Models/BindableDownstreamApiOptions.cs) merges per-request overrides into per call `DownstreamApis` configuration. | -| Agent Identities | [`AgentOverrides`](AgentOverrides.cs) binds agent identity, userPrincipalName, or user object ID when present. | +| Agent Identities | [`AgentOverrides`](AgentOverrides.cs) binds agent identity, userPrincipalName, or user object ID when present and allowed by the per-route override flag. | From b208e57b21097ac0f1937c2768ef6fd9d49da095 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Wed, 17 Jun 2026 15:28:49 +0100 Subject: [PATCH 3/3] Sidecar: emit warning when agent identity params are ignored When agent identity query parameters are provided on a route where AllowOverrides is disabled, emit a warning log that names the route and the configuration key needed to re-enable agent identity overrides. This avoids silent behaviour changes for existing deployments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Endpoints/AuthorizationHeaderEndpoint.cs | 4 ++++ .../Endpoints/DownstreamApiEndpoint.cs | 4 ++++ src/Microsoft.Identity.Web.Sidecar/Logging/Logging.cs | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs b/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs index 57fd04e9c..f4cc032e5 100644 --- a/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs +++ b/src/Microsoft.Identity.Web.Sidecar/Endpoints/AuthorizationHeaderEndpoint.cs @@ -123,6 +123,10 @@ public static void AddAuthorizationHeaderRequestEndpoints(this WebApplication ap { AgentOverrides.SetOverrides(options, requestParameters.AgentIdentity, requestParameters.AgentUsername, requestParameters.AgentUserId); } + else if (requestParameters.AgentIdentity is not null || requestParameters.AgentUsername is not null || requestParameters.AgentUserId is not null) + { + logger.AgentIdentityOverridesIgnored(routeName); + } string authorizationHeader; diff --git a/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs b/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs index 74b8a6a82..d6e645190 100644 --- a/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs +++ b/src/Microsoft.Identity.Web.Sidecar/Endpoints/DownstreamApiEndpoint.cs @@ -123,6 +123,10 @@ private static async Task, ProblemHttpResult>> D { AgentOverrides.SetOverrides(options, requestParameters.AgentIdentity, requestParameters.AgentUsername, requestParameters.AgentUserId); } + else if (requestParameters.AgentIdentity is not null || requestParameters.AgentUsername is not null || requestParameters.AgentUserId is not null) + { + logger.AgentIdentityOverridesIgnored(routeName); + } HttpContent? content = null; diff --git a/src/Microsoft.Identity.Web.Sidecar/Logging/Logging.cs b/src/Microsoft.Identity.Web.Sidecar/Logging/Logging.cs index 9a7002dc1..167c1e6bb 100644 --- a/src/Microsoft.Identity.Web.Sidecar/Logging/Logging.cs +++ b/src/Microsoft.Identity.Web.Sidecar/Logging/Logging.cs @@ -32,4 +32,11 @@ public static partial class LoggerMessageExtensions Message = "Caller-supplied 'optionsOverride.BaseUrl' was ignored. The downstream BaseUrl is fixed by the host configuration and cannot be overridden by the caller.", EventName = "BaseUrlOverrideIgnored")] public static partial void BaseUrlOverrideIgnored(this ILogger logger); + + [LoggerMessage( + EventId = 5, + Level = LogLevel.Warning, + Message = "Caller-supplied agent identity parameters were ignored on route '{RouteName}' because overrides are not allowed for it by configuration. To enable agent identity overrides, set 'Sidecar:AllowOverrides:{RouteName}' to true.", + EventName = "AgentIdentityOverridesIgnored")] + public static partial void AgentIdentityOverridesIgnored(this ILogger logger, string routeName); }