-
Notifications
You must be signed in to change notification settings - Fork 245
Labels
Spec'denhancementNew feature or requestNew feature or requestfeature requestneeds-spectoken acquisition
Description
Summary
Introduce a new HTTP Delegating Handler (MicrosoftIdentityMessageHandler) in the Microsoft.Identity.Web.TokenAcquisition NuGet package that enables authentication for outgoing HTTP requests using only IAuthorizationHeaderProvider and AuthorizationHeaderProviderOptions. This will be an alternative to DownstreamApi, allowing developers to reuse existing HttpClient-based code and benefit from the authentication logic.
Motivation and goals
- Enable authentication of outgoing HTTP requests in a way that's composable and decoupled from DownstreamApi if they wish to (Based on customer research and feedback)
- Allow developers to easily migrate existing HttpClient codebases to use Entra ID authentication.
- Easier integration to Aspire.
- Provide flexible per-client and per-request authentication options using
AuthorizationHeaderProviderOptions. - Factorize and reuse logic for handling
WWW-Authenticateheaders (challenge/refresh scenarios) betweem this new handler and DownstreamApi. - Use modern .NET APIs such as
HttpRequestMessage.Optionsfor per-request configuration. - Support async-first API surface for token acquisition and header provisioning.
- Leave serialization/deserialization to
HttpClientResponseextension methods as .NET provides them now.
In scope
- Implementation of
MicrosoftIdentityMessageHandler : DelegatingHandler. - Usage of
IAuthorizationHeaderProviderandAuthorizationHeaderProviderOptions. - Extension methods for
HttpRequestMessageto set authentication options via object or delegate. - Documentation and examples of per-client and per-request configuration.
- Factorized logic for handling authentication challenges.
- Logging
Out of scope
- Serialization/deserialization of HTTP response content.
- Building a new token acquisition stack—use existing abstractions only.
- Deep integration with DownstreamApi; this is intended as an alternative. Customers will have the choice.
- Custom resilience, metrics (document best practices only).
Risks / unknowns
- Ensuring that the handler correctly propagates and manages exceptions, so error handling remains idiomatic for HttpClient users.
- Potential for misuse if developers don't set required scopes or options—should fail fast with clear actionable exceptions.
- Challenge handling logic may need to be robust for various downstream API behaviors.
Examples
DI Setup:
builder.Services.AddHttpClient("ContosoApi", c => { /* base address */ })
.AddHttpMessageHandler(sp => new MicrosoftIdentityMessageHandler(
sp.GetRequiredService<IAuthorizationHeaderProvider>(),
defaultOptions: new AuthorizationHeaderProviderOptions { Scopes = ["api://contoso-api/.default"] }
));Per-request override:
var req = new HttpRequestMessage(HttpMethod.Get, "/todos")
.WithAuthenticationOptions(o => o.Scopes = ["custom.scope"]);
await _client.SendAsync(req, ct);Manual instantiation:
var client = new HttpClient(new MicrosoftIdentityMessageHandler(provider));
var req = new HttpRequestMessage(HttpMethod.Get, "/users?$top=1")
.WithAuthenticationOptions(o => { o.Scopes = ["https://graph.microsoft.com/.default"]; o.RequestAppToken = true; });Error handling:
try
{
var response = await _client.SendAsync(request, ct);
response.EnsureSuccessStatusCode();
}
catch (MicrosoftIdentityAuthenticationException ex)
{
// Handle token acquisition/challenge failures
}All the usual extension methods still work:
var client = new HttpClient(new MicrosoftIdentityMessageHandler(provider));
var req = new HttpRequestMessage(HttpMethod.Get, "/users?$top=1")
.WithAuthenticationOptions(o => o.AcquireTokenOptions.ManagedIdentity = "GUID") );var client = new HttpClient(new MicrosoftIdentityMessageHandler(provider));
var req = new HttpRequestMessage(HttpMethod.Get, "/users?$top=1")
.WithAuthenticationOptions(o => o.WithUserAgentIdentity("GUID", "upn")) );Implementation Details
1. MicrosoftIdentityMessageHandler Skeleton
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Abstractions;
using Microsoft.Extensions.Logging;
public class MicrosoftIdentityMessageHandler : DelegatingHandler
{
private readonly IAuthorizationHeaderProvider _headerProvider;
private readonly AuthorizationHeaderProviderOptions? _defaultOptions;
private readonly ILogger<MicrosoftIdentityMessageHandler>? _logger;
public MicrosoftIdentityMessageHandler(
IAuthorizationHeaderProvider headerProvider,
AuthorizationHeaderProviderOptions? defaultOptions = null,
ILogger<MicrosoftIdentityMessageHandler>? logger = null)
{
_headerProvider = headerProvider ?? throw new ArgumentNullException(nameof(headerProvider));
_defaultOptions = defaultOptions;
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// Get per-request options or use default
var options = request.GetAuthenticationOptions() ?? _defaultOptions;
if (options == null || options.Scopes == null || options.Scopes.Length == 0)
{
throw new MicrosoftIdentityAuthenticationException("Authentication scopes must be configured (either defaults or per-request).");
}
// Acquire authorization header
var authHeader = await _headerProvider.GetAuthorizationHeaderForRequestAsync(options, cancellationToken);
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authHeader);
// Optional: Logging
_logger?.LogDebug("Added Authorization header for scopes: {Scopes}", options.Scopes);
var response = await base.SendAsync(request, cancellationToken);
// Handle WWW-Authenticate challenge if present (factorized logic)
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized &&
response.Headers.WwwAuthenticate.ToString().Contains("Bearer"))
{
// Optionally: Retry logic, refresh token, etc.
// Factorize this logic so it can be shared with DownstreamApi
_logger?.LogWarning("Received WWW-Authenticate challenge, consider refreshing token or handling challenge.");
}
return response;
}
}2. Extension Methods for HttpRequestMessage
using System;
using System.Net.Http;
using Microsoft.Identity.Abstractions;
public static class HttpRequestMessageAuthenticationExtensions
{
private static readonly HttpRequestOptionsKey<AuthorizationHeaderProviderOptions> AuthOptionsKey =
new HttpRequestOptionsKey<AuthorizationHeaderProviderOptions>("Microsoft.Identity.AuthenticationOptions");
public static HttpRequestMessage WithAuthenticationOptions(
this HttpRequestMessage request, AuthorizationHeaderProviderOptions options)
{
request.Options.Set(AuthOptionsKey, options);
return request;
}
public static HttpRequestMessage WithAuthenticationOptions(
this HttpRequestMessage request, Action<AuthorizationHeaderProviderOptions> configure)
{
var options = request.Options.TryGetValue(AuthOptionsKey, out var existingOptions)
? existingOptions
: new AuthorizationHeaderProviderOptions();
configure(options);
request.Options.Set(AuthOptionsKey, options);
return request;
}
public static AuthorizationHeaderProviderOptions? GetAuthenticationOptions(this HttpRequestMessage request)
{
request.Options.TryGetValue(AuthOptionsKey, out var options);
return options;
}
}3. Exception Type
using System;
public class MicrosoftIdentityAuthenticationException : Exception
{
public MicrosoftIdentityAuthenticationException(string message) : base(message) { }
public MicrosoftIdentityAuthenticationException(string message, Exception inner) : base(message, inner) { }
}4. Notes
- Async-first: All header acquisition is async-first.
- Logging: Optional, uses injected
ILogger<MicrosoftIdentityMessageHandler>. - Challenge Handling: Basic skeleton included; logic can be extended/abstracted for reuse.
- Per-request Option Storage: Uses
HttpRequestMessage.Options(type-safe). - Extension methods: Support both object and delegate configuration.
- Exception propagation: Handler throws actionable exceptions; error handling remains idiomatic for HttpClient.
Copilot
Metadata
Metadata
Assignees
Labels
Spec'denhancementNew feature or requestNew feature or requestfeature requestneeds-spectoken acquisition