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
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Identity.Web.Sidecar.Configuration;

/// <summary>
/// Top-level configuration for the sidecar host. Bound from the
/// <c>Sidecar</c> configuration section.
/// </summary>
public class SidecarOptions
{
/// <summary>
/// Per-route gating for caller-supplied <c>optionsOverride.*</c> query
/// parameters. When the corresponding flag is <c>false</c>, any
/// <c>optionsOverride.*</c> parameters supplied by the caller are
/// ignored on that route and a warning is logged.
/// </summary>
public AllowOverridesOptions AllowOverrides { get; set; } = new();
}

/// <summary>
/// Per-route flags controlling whether the sidecar will honour
/// <c>optionsOverride.*</c> query parameters.
/// </summary>
public class AllowOverridesOptions
{
/// <summary>
/// Allow overrides on <c>GET /AuthorizationHeader/{apiName}</c>.
/// Defaults to <c>true</c>.
/// </summary>
public bool GetAuthorizationHeader { get; set; } = true;

/// <summary>
/// Allow overrides on <c>GET /AuthorizationHeaderUnauthenticated/{apiName}</c>.
/// Defaults to <c>false</c>.
/// </summary>
public bool GetAuthorizationHeaderUnauthenticated { get; set; }

/// <summary>
/// Allow overrides on <c>POST /DownstreamApi/{apiName}</c>.
/// Defaults to <c>true</c>.
/// </summary>
public bool CallDownstreamApi { get; set; } = true;

/// <summary>
/// Allow overrides on <c>POST /DownstreamApiUnauthenticated/{apiName}</c>.
/// Defaults to <c>false</c>.
/// </summary>
public bool CallDownstreamApiUnauthenticated { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ public static DownstreamApiOptions MergeOptions(DownstreamApiOptions left, Downs
res.RequestAppToken = right.RequestAppToken;
}

if (!string.IsNullOrEmpty(right.BaseUrl))
{
res.BaseUrl = right.BaseUrl;
}
// BaseUrl from the override is intentionally not merged. The downstream
// BaseUrl is fixed by host configuration and cannot be overridden through
// the optionsOverride channel.

if (!string.IsNullOrEmpty(right.RelativePath))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,69 @@
using Microsoft.Extensions.Options;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web.Sidecar.Configuration;
using Microsoft.Identity.Web.Sidecar.Logging;
using Microsoft.Identity.Web.Sidecar.Models;

namespace Microsoft.Identity.Web.Sidecar.Endpoints;

public static class AuthorizationHeaderEndpoint
{
internal const string AuthenticatedRouteName = "AuthorizationHeader";
internal const string UnauthenticatedRouteName = "AuthorizationHeaderUnauthenticated";

public static void AddAuthorizationHeaderRequestEndpoints(this WebApplication app)
{
app.MapGet("/AuthorizationHeader/{apiName}", AuthorizationHeaderAsync).
WithName("AuthorizationHeader").
app.MapGet("/AuthorizationHeader/{apiName}",
(HttpContext httpContext, [Description("The downstream API to acquire an authorization header for.")][FromRoute] string apiName,
[AsParameters] AuthorizationHeaderRequest requestParameters,
BindableDownstreamApiOptions optionsOverride,
[FromServices] IAuthorizationHeaderProvider headerProvider,
[FromServices] IOptionsMonitor<DownstreamApiOptions> optionsMonitor,
[FromServices] IOptions<SidecarOptions> sidecarOptions,
[FromServices] ILogger<Program> logger,
CancellationToken cancellationToken) =>
AuthorizationHeaderAsync(
httpContext, apiName, requestParameters, optionsOverride, headerProvider, optionsMonitor,
sidecarOptions.Value.AllowOverrides.GetAuthorizationHeader, AuthenticatedRouteName, logger, cancellationToken)).
WithName(AuthenticatedRouteName).
RequireAuthorization().
ProducesProblem(StatusCodes.Status400BadRequest).
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." +
"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\n" +
"Repeat parameters like 'optionsOverride.Scopes' to add multiple scopes.");

app.MapGet("/AuthorizationHeaderUnauthenticated/{apiName}", AuthorizationHeaderAsync).
WithName("AuthorizationHeaderUnauthenticated").
app.MapGet("/AuthorizationHeaderUnauthenticated/{apiName}",
(HttpContext httpContext, [Description("The downstream API to acquire an authorization header for.")][FromRoute] string apiName,
[AsParameters] AuthorizationHeaderRequest requestParameters,
BindableDownstreamApiOptions optionsOverride,
[FromServices] IAuthorizationHeaderProvider headerProvider,
[FromServices] IOptionsMonitor<DownstreamApiOptions> optionsMonitor,
[FromServices] IOptions<SidecarOptions> sidecarOptions,
[FromServices] ILogger<Program> logger,
CancellationToken cancellationToken) =>
AuthorizationHeaderAsync(
httpContext, apiName, requestParameters, optionsOverride, headerProvider, optionsMonitor,
sidecarOptions.Value.AllowOverrides.GetAuthorizationHeaderUnauthenticated, UnauthenticatedRouteName, logger, cancellationToken)).
WithName(UnauthenticatedRouteName).
AllowAnonymous().
ProducesProblem(StatusCodes.Status400BadRequest).
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." +
"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" +
Expand All @@ -49,14 +79,14 @@ public static void AddAuthorizationHeaderRequestEndpoints(this WebApplication ap

private static async Task<Results<Ok<Models.AuthorizationHeaderResult>, ProblemHttpResult>> AuthorizationHeaderAsync(
HttpContext httpContext,
[Description("The downstream API to acquire an authorization header for.")]
[FromRoute]
string apiName,
[AsParameters] AuthorizationHeaderRequest requestParameters,
AuthorizationHeaderRequest requestParameters,
BindableDownstreamApiOptions optionsOverride,
[FromServices] IAuthorizationHeaderProvider headerProvider,
[FromServices] IOptionsMonitor<DownstreamApiOptions> optionsMonitor,
[FromServices] ILogger<Program> logger,
IAuthorizationHeaderProvider headerProvider,
IOptionsMonitor<DownstreamApiOptions> optionsMonitor,
bool allowOverrides,
string routeName,
ILogger<Program> logger,
CancellationToken cancellationToken)
{
DownstreamApiOptions? options = optionsMonitor.Get(apiName);
Expand All @@ -70,7 +100,14 @@ public static void AddAuthorizationHeaderRequestEndpoints(this WebApplication ap

if (optionsOverride.HasAny)
{
options = DownstreamApiOptionsMerger.MergeOptions(options, optionsOverride);
if (allowOverrides)
{
options = DownstreamApiOptionsMerger.MergeOptions(options, optionsOverride);
}
else
{
logger.OverridesIgnored(routeName);
}
}

if (options.Scopes is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,67 @@
using Microsoft.Extensions.Options;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web.Sidecar.Configuration;
using Microsoft.Identity.Web.Sidecar.Logging;
using Microsoft.Identity.Web.Sidecar.Models;

namespace Microsoft.Identity.Web.Sidecar.Endpoints;

public static class DownstreamApiEndpoint
{
internal const string AuthenticatedRouteName = "DownstreamApi";
internal const string UnauthenticatedRouteName = "DownstreamApiUnauthenticated";

public static void AddDownstreamApiRequestEndpoints(this WebApplication app)
{
app.MapPost("/DownstreamApi/{apiName}", DownstreamApiAsync).
WithName("DownstreamApi").
app.MapPost("/DownstreamApi/{apiName}",
(HttpContext httpContext, [Description("The downstream API to call")][FromRoute] string apiName,
[AsParameters] DownstreamApiRequest requestParameters,
BindableDownstreamApiOptions optionsOverride,
[FromServices] IDownstreamApi downstreamApi,
[FromServices] IOptionsMonitor<DownstreamApiOptions> optionsMonitor,
[FromServices] IOptions<SidecarOptions> sidecarOptions,
[FromServices] ILogger<Program> logger,
CancellationToken cancellationToken) =>
DownstreamApiAsync(
httpContext, apiName, requestParameters, optionsOverride, downstreamApi, optionsMonitor,
sidecarOptions.Value.AllowOverrides.CallDownstreamApi, AuthenticatedRouteName, logger, cancellationToken)).
WithName(AuthenticatedRouteName).
RequireAuthorization().
ProducesProblem(StatusCodes.Status400BadRequest).
ProducesProblem(StatusCodes.Status401Unauthorized).
WithSummary("Invoke a configured downstream API through the sidecar using the authenticated identity.").
WithDescription(
"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");

app.MapPost("/DownstreamApiUnauthenticated/{apiName}", DownstreamApiAsync).
WithName("DownstreamApiUnauthenticated").
app.MapPost("/DownstreamApiUnauthenticated/{apiName}",
(HttpContext httpContext, [Description("The downstream API to call")][FromRoute] string apiName,
[AsParameters] DownstreamApiRequest requestParameters,
BindableDownstreamApiOptions optionsOverride,
[FromServices] IDownstreamApi downstreamApi,
[FromServices] IOptionsMonitor<DownstreamApiOptions> optionsMonitor,
[FromServices] IOptions<SidecarOptions> sidecarOptions,
[FromServices] ILogger<Program> logger,
CancellationToken cancellationToken) =>
DownstreamApiAsync(
httpContext, apiName, requestParameters, optionsOverride, downstreamApi, optionsMonitor,
sidecarOptions.Value.AllowOverrides.CallDownstreamApiUnauthenticated, UnauthenticatedRouteName, logger, cancellationToken)).
WithName(UnauthenticatedRouteName).
AllowAnonymous().
ProducesProblem(StatusCodes.Status400BadRequest).
ProducesProblem(StatusCodes.Status401Unauthorized).
WithSummary("Invoke a configured downstream API through the sidecar using the configured client credentials.").
WithDescription(
"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" +
Expand All @@ -49,14 +79,14 @@ public static void AddDownstreamApiRequestEndpoints(this WebApplication app)

private static async Task<Results<Ok<DownstreamApiResult>, ProblemHttpResult>> DownstreamApiAsync(
HttpContext httpContext,
[Description("The downstream API to call")]
[FromRoute]
string apiName,
[AsParameters] DownstreamApiRequest requestParameters,
DownstreamApiRequest requestParameters,
BindableDownstreamApiOptions optionsOverride,
[FromServices] IDownstreamApi downstreamApi,
[FromServices] IOptionsMonitor<DownstreamApiOptions> optionsMonitor,
[FromServices] ILogger<Program> logger,
IDownstreamApi downstreamApi,
IOptionsMonitor<DownstreamApiOptions> optionsMonitor,
bool allowOverrides,
string routeName,
ILogger<Program> logger,
CancellationToken cancellationToken)
{
DownstreamApiOptions? options = optionsMonitor.Get(apiName);
Expand All @@ -70,7 +100,14 @@ private static async Task<Results<Ok<DownstreamApiResult>, ProblemHttpResult>> D

if (optionsOverride.HasAny)
{
options = DownstreamApiOptionsMerger.MergeOptions(options, optionsOverride);
if (allowOverrides)
{
options = DownstreamApiOptionsMerger.MergeOptions(options, optionsOverride);
}
else
{
logger.OverridesIgnored(routeName);
}
}

if (options.Scopes is null || !options.Scopes.Any())
Expand Down
14 changes: 14 additions & 0 deletions src/Microsoft.Identity.Web.Sidecar/Logging/Logging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,18 @@ public static partial class LoggerMessageExtensions
Message = "An error occurred while parsing the token.",
EventName = "ValidateRequest_UnableToParseToken")]
public static partial void UnableToParseToken(this ILogger logger, Exception? exception);

[LoggerMessage(
EventId = 3,
Level = LogLevel.Warning,
Message = "Caller-supplied 'optionsOverride.*' parameters were ignored on route '{RouteName}' because overrides are not allowed for it by configuration.",
EventName = "OverridesIgnored")]
public static partial void OverridesIgnored(this ILogger logger, string routeName);

[LoggerMessage(
EventId = 4,
Level = LogLevel.Warning,
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
using System.Reflection;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web.Sidecar.Logging;

namespace Microsoft.Identity.Web.Sidecar.Models;

Expand All @@ -28,8 +31,25 @@ public BindableDownstreamApiOptions()
public static ValueTask<BindableDownstreamApiOptions?> BindAsync(HttpContext ctx, ParameterInfo parameter)
{
var paramName = parameter.Name ?? "optionsOverride";
bool hasAny = ctx.Request.Query.Keys.Any(k =>
k.StartsWith(paramName + ".", StringComparison.OrdinalIgnoreCase));
var prefix = paramName + ".";
var baseUrlKey = paramName + ".BaseUrl";

bool hasAny = false;
bool hasNonBaseUrlOverride = false;
foreach (var k in ctx.Request.Query.Keys)
{
if (!k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
continue;
}

hasAny = true;
if (!k.Equals(baseUrlKey, StringComparison.OrdinalIgnoreCase))
{
hasNonBaseUrlOverride = true;
break;
}
}

var result = new BindableDownstreamApiOptions();

Expand All @@ -38,7 +58,10 @@ public BindableDownstreamApiOptions()
return ValueTask.FromResult<BindableDownstreamApiOptions?>(result);
}

result.HasAny = true;
// HasAny only reflects non-BaseUrl overrides because BaseUrl is always
// ignored. This avoids a misleading "OverridesIgnored" warning at the
// route layer when the caller only supplied an (already-rejected) BaseUrl.
result.HasAny = hasNonBaseUrlOverride;

var query = ctx.Request.Query;

Expand Down Expand Up @@ -103,7 +126,12 @@ public BindableDownstreamApiOptions()
}
else if (path.Equals("BaseUrl", StringComparison.OrdinalIgnoreCase))
{
result.BaseUrl = values.LastOrDefault();
// Caller-supplied BaseUrl is unconditionally rejected: the downstream
// BaseUrl is fixed by the host configuration and cannot be overridden
// through optionsOverride. Log a warning and ignore the value.
var loggerFactory = ctx.RequestServices.GetService<ILoggerFactory>();
loggerFactory?.CreateLogger(typeof(BindableDownstreamApiOptions).FullName!)
.BaseUrlOverrideIgnored();
}
else if (path.Equals("RelativePath", StringComparison.OrdinalIgnoreCase))
{
Expand Down
Loading
Loading