Skip to content

Commit 7282233

Browse files
Copilotjmprieur
andauthored
Add MicrosoftIdentityMessageHandler for flexible HttpClient authentication (#3503)
* Initial plan * Initial analysis - Create MicrosoftIdentityMessageHandler implementation plan Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Implement core MicrosoftIdentityMessageHandler functionality Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Add basic tests and verify core functionality works Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Change MicrosoftIdentityMessageHandlerOptions to inherit from AuthorizationHeaderProviderOptions Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Add section 7 to AgentIdentities README for MicrosoftIdentityMessageHandler integration Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Fix AgentIdentities README: restore section 6 and properly add section 7 Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Enhance documentation and WWW-Authenticate challenge handling logic Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Compile regex for extracting claims from WWW-Authenticate headers for better performance Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> * Use MSAL WWW-Authenticate parser and enhance challenge handling based on code review feedback Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jmprieur <13203188+jmprieur@users.noreply.github.com> Co-authored-by: Jean-Marc Prieur <jmprieur@microsoft.com>
1 parent 5789ae6 commit 7282233

12 files changed

Lines changed: 1161 additions & 4 deletions

src/Microsoft.Identity.Web.AgentIdentities/README.AgentIdentities.md

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,13 +317,13 @@ var userResponseByOid = await downstreamApi.GetForUserAsync<string>(
317317

318318
To call Azure SDKs, use the MicrosoftIdentityAzureCredential class from the Microsoft.Identity.Web.Azure NuGet package.
319319

320-
Install the Microsoft.Identity.Web.GraphServiceClient which handles authentication for the Graph SDK
320+
Install the Microsoft.Identity.Web.Azure package:
321321

322322
```bash
323-
dotnet dotnet add package Microsoft.Identity.Web.Azure
323+
dotnet add package Microsoft.Identity.Web.Azure
324324
```
325325

326-
Add the support for Microsoft Graph in your service collection.
326+
Add the support for Azure token credential in your service collection:
327327

328328
```bash
329329
services.AddMicrosoftIdentityAzureTokenCredential();
@@ -334,6 +334,106 @@ You can now get a `MicrosoftIdentityTokenCredential` from the service provider.
334334

335335
See [Readme-azure](../../README-Azure.md)
336336

337+
### 7. HttpClient with MicrosoftIdentityMessageHandler Integration
338+
339+
For scenarios where you want to use HttpClient directly with flexible authentication options, you can use the `MicrosoftIdentityMessageHandler` from the Microsoft.Identity.Web.TokenAcquisition package.
340+
341+
Note: The Microsoft.Identity.Web.TokenAcquisition package is already referenced by Microsoft.Identity.Web.AgentIdentities.
342+
343+
#### Using Agent Identity with MicrosoftIdentityMessageHandler:
344+
345+
```csharp
346+
// Configure HttpClient with MicrosoftIdentityMessageHandler in DI
347+
services.AddHttpClient("MyApiClient", client =>
348+
{
349+
client.BaseAddress = new Uri("https://myapi.domain.com");
350+
})
351+
.AddHttpMessageHandler(serviceProvider => new MicrosoftIdentityMessageHandler(
352+
serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>(),
353+
new MicrosoftIdentityMessageHandlerOptions
354+
{
355+
Scopes = { "https://myapi.domain.com/.default" }
356+
}));
357+
358+
// Usage in your service or controller
359+
public class MyService
360+
{
361+
private readonly HttpClient _httpClient;
362+
363+
public MyService(IHttpClientFactory httpClientFactory)
364+
{
365+
_httpClient = httpClientFactory.CreateClient("MyApiClient");
366+
}
367+
368+
public async Task<string> CallApiWithAgentIdentity(string agentIdentity)
369+
{
370+
// Create request with agent identity authentication
371+
var request = new HttpRequestMessage(HttpMethod.Get, "/api/data")
372+
.WithAuthenticationOptions(options =>
373+
{
374+
options.WithAgentIdentity(agentIdentity);
375+
options.RequestAppToken = true;
376+
});
377+
378+
var response = await _httpClient.SendAsync(request);
379+
response.EnsureSuccessStatusCode();
380+
return await response.Content.ReadAsStringAsync();
381+
}
382+
}
383+
```
384+
385+
#### Using Agent User Identity with MicrosoftIdentityMessageHandler:
386+
387+
```csharp
388+
public async Task<string> CallApiWithAgentUserIdentity(string agentIdentity, string userUpn)
389+
{
390+
// Create request with agent user identity authentication
391+
var request = new HttpRequestMessage(HttpMethod.Get, "/api/userdata")
392+
.WithAuthenticationOptions(options =>
393+
{
394+
options.WithAgentUserIdentity(agentIdentity, userUpn);
395+
options.Scopes.Add("https://myapi.domain.com/user.read");
396+
});
397+
398+
var response = await _httpClient.SendAsync(request);
399+
response.EnsureSuccessStatusCode();
400+
return await response.Content.ReadAsStringAsync();
401+
}
402+
```
403+
404+
#### Manual HttpClient Configuration:
405+
406+
You can also configure the handler manually for more control:
407+
408+
```csharp
409+
// Get the authorization header provider
410+
IAuthorizationHeaderProvider headerProvider =
411+
serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>();
412+
413+
// Create the handler with default options
414+
var handler = new MicrosoftIdentityMessageHandler(
415+
headerProvider,
416+
new MicrosoftIdentityMessageHandlerOptions
417+
{
418+
Scopes = { "https://graph.microsoft.com/.default" }
419+
});
420+
421+
// Create HttpClient with the handler
422+
using var httpClient = new HttpClient(handler);
423+
424+
// Make requests with per-request authentication options
425+
var request = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/applications")
426+
.WithAuthenticationOptions(options =>
427+
{
428+
options.WithAgentIdentity(agentIdentity);
429+
options.RequestAppToken = true;
430+
});
431+
432+
var response = await httpClient.SendAsync(request);
433+
```
434+
435+
The `MicrosoftIdentityMessageHandler` provides a flexible, composable way to add authentication to your HttpClient-based code while maintaining full compatibility with existing Microsoft Identity Web extension methods for agent identities.
436+
337437
## Prerequisites
338438

339439
### Microsoft Entra ID Configuration

src/Microsoft.Identity.Web.TokenAcquisition/GlobalSuppressions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@
1717
[assembly: SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Existing public API", Scope = "member", Target = "~M:Microsoft.Identity.Web.ITokenAcquisition.ReplyForbiddenWithWwwAuthenticateHeader(System.Collections.Generic.IEnumerable{System.String},Microsoft.Identity.Client.MsalUiRequiredException,System.String,Microsoft.AspNetCore.Http.HttpResponse)")]
1818
[assembly: SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Existing public API", Scope = "member", Target = "~M:Microsoft.Identity.Web.TokenAcquirerFactory.GetTokenAcquirer(System.String)~Microsoft.Identity.Abstractions.ITokenAcquirer")]
1919
[assembly: SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Existing public API", Scope = "member", Target = "~M:Microsoft.Identity.Web.TokenAcquirerFactory.GetTokenAcquirer(System.String,System.String,System.Collections.Generic.IEnumerable{Microsoft.Identity.Abstractions.CredentialDescription},System.String)~Microsoft.Identity.Abstractions.ITokenAcquirer")]
20+
[assembly: SuppressMessage("ApiDesign", "RS0016:Symbol is not part of the declared API", Justification = "Protected serialization constructor for .NET Framework/Standard 2.0 compatibility", Scope = "member", Target = "~M:Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext)")]
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Net.Http;
7+
using Microsoft.Identity.Abstractions;
8+
9+
namespace Microsoft.Identity.Web
10+
{
11+
/// <summary>
12+
/// Extension methods for <see cref="HttpRequestMessage"/> to configure per-request authentication options
13+
/// when using <see cref="MicrosoftIdentityMessageHandler"/>.
14+
/// </summary>
15+
/// <remarks>
16+
/// These extension methods enable flexible per-request authentication configuration that can override
17+
/// or supplement the default options configured in the message handler. The methods support both
18+
/// modern .NET (using HttpRequestMessage.Options) and legacy frameworks
19+
/// (using HttpRequestMessage.Properties).
20+
/// </remarks>
21+
/// <example>
22+
/// <para>Setting authentication options with an object:</para>
23+
/// <code>
24+
/// var request = new HttpRequestMessage(HttpMethod.Get, "/api/data")
25+
/// .WithAuthenticationOptions(new MicrosoftIdentityMessageHandlerOptions
26+
/// {
27+
/// Scopes = { "custom.scope" }
28+
/// });
29+
/// </code>
30+
///
31+
/// <para>Configuring authentication options with a delegate:</para>
32+
/// <code>
33+
/// var request = new HttpRequestMessage(HttpMethod.Get, "/api/data")
34+
/// .WithAuthenticationOptions(options =>
35+
/// {
36+
/// options.Scopes.Add("https://graph.microsoft.com/.default");
37+
/// options.WithAgentIdentity("agent-guid");
38+
/// options.RequestAppToken = true;
39+
/// });
40+
/// </code>
41+
/// </example>
42+
public static class HttpRequestMessageAuthenticationExtensions
43+
{
44+
private const string AuthOptionsKey = "Microsoft.Identity.AuthenticationOptions";
45+
46+
/// <summary>
47+
/// Sets authentication options for the HTTP request.
48+
/// </summary>
49+
/// <param name="request">The HTTP request message to configure.</param>
50+
/// <param name="options">The authentication options to apply to this request.</param>
51+
/// <returns>The same request message for method chaining.</returns>
52+
/// <exception cref="ArgumentNullException">
53+
/// Thrown when <paramref name="request"/> or <paramref name="options"/> is <see langword="null"/>.
54+
/// </exception>
55+
/// <example>
56+
/// <code>
57+
/// var options = new MicrosoftIdentityMessageHandlerOptions
58+
/// {
59+
/// Scopes = { "https://graph.microsoft.com/.default" }
60+
/// };
61+
/// options.WithAgentIdentity("my-agent-guid");
62+
///
63+
/// var request = new HttpRequestMessage(HttpMethod.Get, "/me")
64+
/// .WithAuthenticationOptions(options);
65+
/// </code>
66+
/// </example>
67+
/// <remarks>
68+
/// This method will override any existing authentication options set on the request.
69+
/// The options object can be further configured with extension methods from other Microsoft Identity Web packages.
70+
/// </remarks>
71+
public static HttpRequestMessage WithAuthenticationOptions(
72+
this HttpRequestMessage request, MicrosoftIdentityMessageHandlerOptions options)
73+
{
74+
if (request == null) throw new ArgumentNullException(nameof(request));
75+
if (options == null) throw new ArgumentNullException(nameof(options));
76+
77+
#if NET5_0_OR_GREATER
78+
request.Options.Set(new HttpRequestOptionsKey<MicrosoftIdentityMessageHandlerOptions>(AuthOptionsKey), options);
79+
#else
80+
// Use Properties dictionary for older frameworks
81+
request.Properties[AuthOptionsKey] = options;
82+
#endif
83+
return request;
84+
}
85+
86+
/// <summary>
87+
/// Configures authentication options for the HTTP request using a delegate.
88+
/// </summary>
89+
/// <param name="request">The HTTP request message to configure.</param>
90+
/// <param name="configure">A delegate that configures the authentication options.</param>
91+
/// <returns>The same request message for method chaining.</returns>
92+
/// <exception cref="ArgumentNullException">
93+
/// Thrown when <paramref name="request"/> or <paramref name="configure"/> is <see langword="null"/>.
94+
/// </exception>
95+
/// <example>
96+
/// <code>
97+
/// var request = new HttpRequestMessage(HttpMethod.Get, "/api/users")
98+
/// .WithAuthenticationOptions(options =>
99+
/// {
100+
/// options.Scopes.Add("https://myapi.domain.com/user.read");
101+
/// options.WithAgentIdentity("agent-application-id");
102+
/// options.RequestAppToken = true;
103+
/// });
104+
/// </code>
105+
/// </example>
106+
/// <remarks>
107+
/// <para>
108+
/// If the request already has authentication options configured, the delegate will receive
109+
/// the existing options object to modify. Otherwise, a new <see cref="MicrosoftIdentityMessageHandlerOptions"/>
110+
/// instance will be created and passed to the delegate.
111+
/// </para>
112+
/// <para>
113+
/// This method is particularly useful when you need to apply extension methods from other
114+
/// Microsoft Identity Web packages, such as agent identity methods.
115+
/// </para>
116+
/// </remarks>
117+
public static HttpRequestMessage WithAuthenticationOptions(
118+
this HttpRequestMessage request, Action<MicrosoftIdentityMessageHandlerOptions> configure)
119+
{
120+
if (request == null) throw new ArgumentNullException(nameof(request));
121+
if (configure == null) throw new ArgumentNullException(nameof(configure));
122+
123+
var options = request.GetAuthenticationOptions() ?? new MicrosoftIdentityMessageHandlerOptions();
124+
125+
configure(options);
126+
127+
#if NET5_0_OR_GREATER
128+
request.Options.Set(new HttpRequestOptionsKey<MicrosoftIdentityMessageHandlerOptions>(AuthOptionsKey), options);
129+
#else
130+
// Use Properties dictionary for older frameworks
131+
request.Properties[AuthOptionsKey] = options;
132+
#endif
133+
return request;
134+
}
135+
136+
/// <summary>
137+
/// Gets the authentication options that have been set for the HTTP request.
138+
/// </summary>
139+
/// <param name="request">The HTTP request message to examine.</param>
140+
/// <returns>
141+
/// The <see cref="MicrosoftIdentityMessageHandlerOptions"/> if previously set using
142+
/// <see cref="WithAuthenticationOptions(HttpRequestMessage, MicrosoftIdentityMessageHandlerOptions)"/>
143+
/// or <see cref="WithAuthenticationOptions(HttpRequestMessage, Action{MicrosoftIdentityMessageHandlerOptions})"/>,
144+
/// otherwise <see langword="null"/>.
145+
/// </returns>
146+
/// <exception cref="ArgumentNullException">
147+
/// Thrown when <paramref name="request"/> is <see langword="null"/>.
148+
/// </exception>
149+
/// <example>
150+
/// <code>
151+
/// var request = new HttpRequestMessage(HttpMethod.Get, "/api/data")
152+
/// .WithAuthenticationOptions(options => options.Scopes.Add("custom.scope"));
153+
///
154+
/// var options = request.GetAuthenticationOptions();
155+
/// if (options != null)
156+
/// {
157+
/// Console.WriteLine($"Request has {options.Scopes.Count} scopes configured.");
158+
/// }
159+
/// </code>
160+
/// </example>
161+
/// <remarks>
162+
/// This method is primarily used internally by <see cref="MicrosoftIdentityMessageHandler"/>
163+
/// but can also be useful for debugging or conditional logic based on authentication configuration.
164+
/// </remarks>
165+
public static MicrosoftIdentityMessageHandlerOptions? GetAuthenticationOptions(this HttpRequestMessage request)
166+
{
167+
if (request == null) throw new ArgumentNullException(nameof(request));
168+
169+
#if NET5_0_OR_GREATER
170+
request.Options.TryGetValue(new HttpRequestOptionsKey<MicrosoftIdentityMessageHandlerOptions>(AuthOptionsKey), out var options);
171+
return options;
172+
#else
173+
// Use Properties dictionary for older frameworks
174+
return request.Properties.TryGetValue(AuthOptionsKey, out var options)
175+
? options as MicrosoftIdentityMessageHandlerOptions
176+
: null;
177+
#endif
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)