Skip to content

Commit bdf69b3

Browse files
Copilotjmprieur
andauthored
Add AddMicrosoftIdentityMessageHandler extension methods for IHttpClientBuilder (#3649)
* Initial plan * Add MicrosoftIdentityHttpClientBuilderExtensions with 4 overloads and unit tests Co-authored-by: jmprieur <[email protected]> * Add comprehensive documentation for AddMicrosoftIdentityMessageHandler extension methods Co-authored-by: jmprieur <[email protected]> * Update expectedWarningCount to 52 in test-aot.ps1 Co-authored-by: jmprieur <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: jmprieur <[email protected]> Co-authored-by: Jean-Marc Prieur <[email protected]>
1 parent 697d10f commit bdf69b3

File tree

10 files changed

+1182
-1
lines changed

10 files changed

+1182
-1
lines changed

build/test-aot.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ foreach ($line in $($publishOutput -split "`r`n"))
2323
}
2424

2525
Write-Host "Actual warning count is: ", $actualWarningCount
26-
$expectedWarningCount = 50
26+
$expectedWarningCount = 52
2727

2828
if ($LastExitCode -ne 0)
2929
{
Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
# Calling Custom APIs with MicrosoftIdentityMessageHandler
2+
3+
This guide explains how to use `MicrosoftIdentityMessageHandler` for HttpClient integration to call custom downstream APIs with automatic Microsoft Identity authentication.
4+
5+
## Table of Contents
6+
7+
- [Overview](#overview)
8+
- [MicrosoftIdentityMessageHandler - For HttpClient Integration](#microsoftidentitymessagehandler---for-httpclient-integration)
9+
- [Before: Manual Setup](#before-manual-setup)
10+
- [After: Using Extension Methods](#after-using-extension-methods)
11+
- [Configuration Examples](#configuration-examples)
12+
- [Per-Request Options](#per-request-options)
13+
- [Advanced Scenarios](#advanced-scenarios)
14+
15+
## Overview
16+
17+
`MicrosoftIdentityMessageHandler` is a `DelegatingHandler` that automatically adds authorization headers to outgoing HTTP requests. The new `AddMicrosoftIdentityMessageHandler` extension methods make it easy to configure HttpClient instances with automatic Microsoft Identity authentication.
18+
19+
## MicrosoftIdentityMessageHandler - For HttpClient Integration
20+
21+
### Before: Manual Setup
22+
23+
Previously, you had to manually configure the message handler:
24+
25+
```csharp
26+
// In Program.cs or Startup.cs
27+
services.AddHttpClient("MyApiClient", client =>
28+
{
29+
client.BaseAddress = new Uri("https://api.example.com");
30+
})
31+
.AddHttpMessageHandler(serviceProvider => new MicrosoftIdentityMessageHandler(
32+
serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>(),
33+
new MicrosoftIdentityMessageHandlerOptions
34+
{
35+
Scopes = { "https://api.example.com/.default" }
36+
}));
37+
```
38+
39+
### After: Using Extension Methods
40+
41+
Now you can use the convenient extension methods:
42+
43+
#### 1. Parameterless Overload (Per-Request Configuration)
44+
45+
Use this when you want to configure authentication options on a per-request basis:
46+
47+
```csharp
48+
services.AddHttpClient("FlexibleClient")
49+
.AddMicrosoftIdentityMessageHandler();
50+
51+
// Later, in a service:
52+
var request = new HttpRequestMessage(HttpMethod.Get, "/api/data")
53+
.WithAuthenticationOptions(options =>
54+
{
55+
options.Scopes.Add("https://api.example.com/.default");
56+
});
57+
58+
var response = await httpClient.SendAsync(request);
59+
```
60+
61+
#### 2. Options Instance Overload
62+
63+
Use this when you have a pre-configured options object:
64+
65+
```csharp
66+
var options = new MicrosoftIdentityMessageHandlerOptions
67+
{
68+
Scopes = { "https://graph.microsoft.com/.default" }
69+
};
70+
options.WithAgentIdentity("agent-application-id");
71+
72+
services.AddHttpClient("GraphClient", client =>
73+
{
74+
client.BaseAddress = new Uri("https://graph.microsoft.com");
75+
})
76+
.AddMicrosoftIdentityMessageHandler(options);
77+
```
78+
79+
#### 3. Action Delegate Overload (Inline Configuration)
80+
81+
Use this for inline configuration - the most common scenario:
82+
83+
```csharp
84+
services.AddHttpClient("MyApiClient", client =>
85+
{
86+
client.BaseAddress = new Uri("https://api.example.com");
87+
})
88+
.AddMicrosoftIdentityMessageHandler(options =>
89+
{
90+
options.Scopes.Add("https://api.example.com/.default");
91+
options.RequestAppToken = true;
92+
});
93+
```
94+
95+
#### 4. IConfiguration Overload (Configuration from appsettings.json)
96+
97+
Use this to configure from appsettings.json:
98+
99+
**appsettings.json:**
100+
```json
101+
{
102+
"DownstreamApi": {
103+
"Scopes": ["https://api.example.com/.default"]
104+
},
105+
"GraphApi": {
106+
"Scopes": ["https://graph.microsoft.com/.default", "User.Read"]
107+
}
108+
}
109+
```
110+
111+
**Program.cs:**
112+
```csharp
113+
services.AddHttpClient("DownstreamApiClient", client =>
114+
{
115+
client.BaseAddress = new Uri("https://api.example.com");
116+
})
117+
.AddMicrosoftIdentityMessageHandler(
118+
configuration.GetSection("DownstreamApi"),
119+
"DownstreamApi");
120+
121+
services.AddHttpClient("GraphClient", client =>
122+
{
123+
client.BaseAddress = new Uri("https://graph.microsoft.com");
124+
})
125+
.AddMicrosoftIdentityMessageHandler(
126+
configuration.GetSection("GraphApi"),
127+
"GraphApi");
128+
```
129+
130+
### Configuration Examples
131+
132+
#### Example 1: Simple Web API Client
133+
134+
```csharp
135+
// Configure in Program.cs
136+
services.AddHttpClient("WeatherApiClient", client =>
137+
{
138+
client.BaseAddress = new Uri("https://api.weather.com");
139+
})
140+
.AddMicrosoftIdentityMessageHandler(options =>
141+
{
142+
options.Scopes.Add("https://api.weather.com/.default");
143+
});
144+
145+
// Use in a controller or service
146+
public class WeatherService
147+
{
148+
private readonly HttpClient _httpClient;
149+
150+
public WeatherService(IHttpClientFactory factory)
151+
{
152+
_httpClient = factory.CreateClient("WeatherApiClient");
153+
}
154+
155+
public async Task<WeatherForecast> GetForecastAsync(string city)
156+
{
157+
var response = await _httpClient.GetAsync($"/forecast/{city}");
158+
response.EnsureSuccessStatusCode();
159+
return await response.Content.ReadFromJsonAsync<WeatherForecast>();
160+
}
161+
}
162+
```
163+
164+
#### Example 2: Multiple API Clients
165+
166+
```csharp
167+
// Configure multiple clients in Program.cs
168+
services.AddHttpClient("ApiClient1")
169+
.AddMicrosoftIdentityMessageHandler(options =>
170+
{
171+
options.Scopes.Add("https://api1.example.com/.default");
172+
});
173+
174+
services.AddHttpClient("ApiClient2")
175+
.AddMicrosoftIdentityMessageHandler(options =>
176+
{
177+
options.Scopes.Add("https://api2.example.com/.default");
178+
options.RequestAppToken = true;
179+
});
180+
181+
// Use in a service
182+
public class MultiApiService
183+
{
184+
private readonly HttpClient _client1;
185+
private readonly HttpClient _client2;
186+
187+
public MultiApiService(IHttpClientFactory factory)
188+
{
189+
_client1 = factory.CreateClient("ApiClient1");
190+
_client2 = factory.CreateClient("ApiClient2");
191+
}
192+
193+
public async Task<string> GetFromBothApisAsync()
194+
{
195+
var data1 = await _client1.GetStringAsync("/data");
196+
var data2 = await _client2.GetStringAsync("/data");
197+
return $"{data1} | {data2}";
198+
}
199+
}
200+
```
201+
202+
#### Example 3: Configuration from appsettings.json with Complex Options
203+
204+
**appsettings.json:**
205+
```json
206+
{
207+
"DownstreamApis": {
208+
"CustomerApi": {
209+
"Scopes": ["api://customer-api/.default"]
210+
},
211+
"OrderApi": {
212+
"Scopes": ["api://order-api/.default"]
213+
},
214+
"InventoryApi": {
215+
"Scopes": ["api://inventory-api/.default"]
216+
}
217+
}
218+
}
219+
```
220+
221+
**Program.cs:**
222+
```csharp
223+
var downstreamApis = configuration.GetSection("DownstreamApis");
224+
225+
services.AddHttpClient("CustomerApiClient", client =>
226+
{
227+
client.BaseAddress = new Uri("https://customer-api.example.com");
228+
})
229+
.AddMicrosoftIdentityMessageHandler(
230+
downstreamApis.GetSection("CustomerApi"),
231+
"CustomerApi");
232+
233+
services.AddHttpClient("OrderApiClient", client =>
234+
{
235+
client.BaseAddress = new Uri("https://order-api.example.com");
236+
})
237+
.AddMicrosoftIdentityMessageHandler(
238+
downstreamApis.GetSection("OrderApi"),
239+
"OrderApi");
240+
241+
services.AddHttpClient("InventoryApiClient", client =>
242+
{
243+
client.BaseAddress = new Uri("https://inventory-api.example.com");
244+
})
245+
.AddMicrosoftIdentityMessageHandler(
246+
downstreamApis.GetSection("InventoryApi"),
247+
"InventoryApi");
248+
```
249+
250+
## Per-Request Options
251+
252+
You can override default options on a per-request basis using the `WithAuthenticationOptions` extension method:
253+
254+
```csharp
255+
// Configure client with default options
256+
services.AddHttpClient("ApiClient")
257+
.AddMicrosoftIdentityMessageHandler(options =>
258+
{
259+
options.Scopes.Add("https://api.example.com/.default");
260+
});
261+
262+
// Override for specific requests
263+
public class MyService
264+
{
265+
private readonly HttpClient _httpClient;
266+
267+
public MyService(IHttpClientFactory factory)
268+
{
269+
_httpClient = factory.CreateClient("ApiClient");
270+
}
271+
272+
public async Task<string> GetSensitiveDataAsync()
273+
{
274+
// Override scopes for this specific request
275+
var request = new HttpRequestMessage(HttpMethod.Get, "/api/sensitive")
276+
.WithAuthenticationOptions(options =>
277+
{
278+
options.Scopes.Clear();
279+
options.Scopes.Add("https://api.example.com/sensitive.read");
280+
options.RequestAppToken = true;
281+
});
282+
283+
var response = await _httpClient.SendAsync(request);
284+
response.EnsureSuccessStatusCode();
285+
return await response.Content.ReadAsStringAsync();
286+
}
287+
}
288+
```
289+
290+
## Advanced Scenarios
291+
292+
### Agent Identity
293+
294+
Use agent identity when your application needs to act on behalf of another application:
295+
296+
```csharp
297+
services.AddHttpClient("AgentClient")
298+
.AddMicrosoftIdentityMessageHandler(options =>
299+
{
300+
options.Scopes.Add("https://graph.microsoft.com/.default");
301+
options.WithAgentIdentity("agent-application-id");
302+
options.RequestAppToken = true;
303+
});
304+
```
305+
306+
### Composing with Other Handlers
307+
308+
You can chain multiple handlers in the pipeline:
309+
310+
```csharp
311+
services.AddHttpClient("ApiClient")
312+
.AddMicrosoftIdentityMessageHandler(options =>
313+
{
314+
options.Scopes.Add("https://api.example.com/.default");
315+
})
316+
.AddHttpMessageHandler<LoggingHandler>()
317+
.AddHttpMessageHandler<RetryHandler>();
318+
```
319+
320+
### WWW-Authenticate Challenge Handling
321+
322+
`MicrosoftIdentityMessageHandler` automatically handles WWW-Authenticate challenges for Conditional Access scenarios:
323+
324+
```csharp
325+
// No additional code needed - automatic handling
326+
services.AddHttpClient("ProtectedApiClient")
327+
.AddMicrosoftIdentityMessageHandler(options =>
328+
{
329+
options.Scopes.Add("https://api.example.com/.default");
330+
});
331+
332+
// The handler will automatically:
333+
// 1. Detect 401 responses with WWW-Authenticate challenges
334+
// 2. Extract required claims from the challenge
335+
// 3. Acquire a new token with the additional claims
336+
// 4. Retry the request with the new token
337+
```
338+
339+
### Error Handling
340+
341+
```csharp
342+
public class MyService
343+
{
344+
private readonly HttpClient _httpClient;
345+
private readonly ILogger<MyService> _logger;
346+
347+
public MyService(IHttpClientFactory factory, ILogger<MyService> logger)
348+
{
349+
_httpClient = factory.CreateClient("ApiClient");
350+
_logger = logger;
351+
}
352+
353+
public async Task<string> GetDataWithErrorHandlingAsync()
354+
{
355+
try
356+
{
357+
var response = await _httpClient.GetAsync("/api/data");
358+
response.EnsureSuccessStatusCode();
359+
return await response.Content.ReadAsStringAsync();
360+
}
361+
catch (MicrosoftIdentityAuthenticationException authEx)
362+
{
363+
_logger.LogError(authEx, "Authentication failed: {Message}", authEx.Message);
364+
throw;
365+
}
366+
catch (HttpRequestException httpEx)
367+
{
368+
_logger.LogError(httpEx, "HTTP request failed: {Message}", httpEx.Message);
369+
throw;
370+
}
371+
}
372+
}
373+
```
374+
375+
## Summary
376+
377+
The `AddMicrosoftIdentityMessageHandler` extension methods provide a clean, flexible way to configure HttpClient with automatic Microsoft Identity authentication:
378+
379+
- **Parameterless**: For per-request configuration flexibility
380+
- **Options instance**: For pre-configured options objects
381+
- **Action delegate**: For inline configuration (most common)
382+
- **IConfiguration**: For configuration from appsettings.json
383+
384+
Choose the overload that best fits your scenario and enjoy automatic authentication for your downstream API calls!

0 commit comments

Comments
 (0)