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
106 changes: 103 additions & 3 deletions src/Microsoft.Identity.Web.AgentIdentities/README.AgentIdentities.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,13 @@ var userResponseByOid = await downstreamApi.GetForUserAsync<string>(

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

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

```bash
dotnet dotnet add package Microsoft.Identity.Web.Azure
dotnet add package Microsoft.Identity.Web.Azure
```

Add the support for Microsoft Graph in your service collection.
Add the support for Azure token credential in your service collection:

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

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

### 7. HttpClient with MicrosoftIdentityMessageHandler Integration

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.

Note: The Microsoft.Identity.Web.TokenAcquisition package is already referenced by Microsoft.Identity.Web.AgentIdentities.

#### Using Agent Identity with MicrosoftIdentityMessageHandler:

```csharp
// Configure HttpClient with MicrosoftIdentityMessageHandler in DI
services.AddHttpClient("MyApiClient", client =>
{
client.BaseAddress = new Uri("https://myapi.domain.com");
})
.AddHttpMessageHandler(serviceProvider => new MicrosoftIdentityMessageHandler(
serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>(),
new MicrosoftIdentityMessageHandlerOptions
{
Scopes = { "https://myapi.domain.com/.default" }
}));

// Usage in your service or controller
public class MyService
{
private readonly HttpClient _httpClient;

public MyService(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("MyApiClient");
}

public async Task<string> CallApiWithAgentIdentity(string agentIdentity)
{
// Create request with agent identity authentication
var request = new HttpRequestMessage(HttpMethod.Get, "/api/data")
.WithAuthenticationOptions(options =>
{
options.WithAgentIdentity(agentIdentity);
options.RequestAppToken = true;
});

var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
```

#### Using Agent User Identity with MicrosoftIdentityMessageHandler:

```csharp
public async Task<string> CallApiWithAgentUserIdentity(string agentIdentity, string userUpn)
{
// Create request with agent user identity authentication
var request = new HttpRequestMessage(HttpMethod.Get, "/api/userdata")
.WithAuthenticationOptions(options =>
{
options.WithAgentUserIdentity(agentIdentity, userUpn);
options.Scopes.Add("https://myapi.domain.com/user.read");
});

var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
```

#### Manual HttpClient Configuration:

You can also configure the handler manually for more control:

```csharp
// Get the authorization header provider
IAuthorizationHeaderProvider headerProvider =
serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>();

// Create the handler with default options
var handler = new MicrosoftIdentityMessageHandler(
headerProvider,
new MicrosoftIdentityMessageHandlerOptions
{
Scopes = { "https://graph.microsoft.com/.default" }
});

// Create HttpClient with the handler
using var httpClient = new HttpClient(handler);

// Make requests with per-request authentication options
var request = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/applications")
.WithAuthenticationOptions(options =>
{
options.WithAgentIdentity(agentIdentity);
options.RequestAppToken = true;
});

var response = await httpClient.SendAsync(request);
```

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.

## Prerequisites

### Microsoft Entra ID Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
[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)")]
[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")]
[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")]
[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)")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Net.Http;
using Microsoft.Identity.Abstractions;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Extension methods for <see cref="HttpRequestMessage"/> to configure per-request authentication options
/// when using <see cref="MicrosoftIdentityMessageHandler"/>.
/// </summary>
/// <remarks>
/// These extension methods enable flexible per-request authentication configuration that can override
/// or supplement the default options configured in the message handler. The methods support both
/// modern .NET (using HttpRequestMessage.Options) and legacy frameworks
/// (using HttpRequestMessage.Properties).
/// </remarks>
/// <example>
/// <para>Setting authentication options with an object:</para>
/// <code>
/// var request = new HttpRequestMessage(HttpMethod.Get, "/api/data")
/// .WithAuthenticationOptions(new MicrosoftIdentityMessageHandlerOptions
/// {
/// Scopes = { "custom.scope" }
/// });
/// </code>
///
/// <para>Configuring authentication options with a delegate:</para>
/// <code>
/// var request = new HttpRequestMessage(HttpMethod.Get, "/api/data")
/// .WithAuthenticationOptions(options =>
/// {
/// options.Scopes.Add("https://graph.microsoft.com/.default");
/// options.WithAgentIdentity("agent-guid");
/// options.RequestAppToken = true;
/// });
/// </code>
/// </example>
public static class HttpRequestMessageAuthenticationExtensions
{
private const string AuthOptionsKey = "Microsoft.Identity.AuthenticationOptions";

/// <summary>
/// Sets authentication options for the HTTP request.
/// </summary>
/// <param name="request">The HTTP request message to configure.</param>
/// <param name="options">The authentication options to apply to this request.</param>
/// <returns>The same request message for method chaining.</returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="request"/> or <paramref name="options"/> is <see langword="null"/>.
/// </exception>
/// <example>
/// <code>
/// var options = new MicrosoftIdentityMessageHandlerOptions
/// {
/// Scopes = { "https://graph.microsoft.com/.default" }
/// };
/// options.WithAgentIdentity("my-agent-guid");
///
/// var request = new HttpRequestMessage(HttpMethod.Get, "/me")
/// .WithAuthenticationOptions(options);
/// </code>
/// </example>
/// <remarks>
/// This method will override any existing authentication options set on the request.
/// The options object can be further configured with extension methods from other Microsoft Identity Web packages.
/// </remarks>
public static HttpRequestMessage WithAuthenticationOptions(
this HttpRequestMessage request, MicrosoftIdentityMessageHandlerOptions options)
{
if (request == null) throw new ArgumentNullException(nameof(request));
if (options == null) throw new ArgumentNullException(nameof(options));

#if NET5_0_OR_GREATER
request.Options.Set(new HttpRequestOptionsKey<MicrosoftIdentityMessageHandlerOptions>(AuthOptionsKey), options);
#else
// Use Properties dictionary for older frameworks
request.Properties[AuthOptionsKey] = options;
#endif
return request;
}

/// <summary>
/// Configures authentication options for the HTTP request using a delegate.
/// </summary>
/// <param name="request">The HTTP request message to configure.</param>
/// <param name="configure">A delegate that configures the authentication options.</param>
/// <returns>The same request message for method chaining.</returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="request"/> or <paramref name="configure"/> is <see langword="null"/>.
/// </exception>
/// <example>
/// <code>
/// var request = new HttpRequestMessage(HttpMethod.Get, "/api/users")
/// .WithAuthenticationOptions(options =>
/// {
/// options.Scopes.Add("https://myapi.domain.com/user.read");
/// options.WithAgentIdentity("agent-application-id");
/// options.RequestAppToken = true;
/// });
/// </code>
/// </example>
/// <remarks>
/// <para>
/// If the request already has authentication options configured, the delegate will receive
/// the existing options object to modify. Otherwise, a new <see cref="MicrosoftIdentityMessageHandlerOptions"/>
/// instance will be created and passed to the delegate.
/// </para>
/// <para>
/// This method is particularly useful when you need to apply extension methods from other
/// Microsoft Identity Web packages, such as agent identity methods.
/// </para>
/// </remarks>
public static HttpRequestMessage WithAuthenticationOptions(
this HttpRequestMessage request, Action<MicrosoftIdentityMessageHandlerOptions> configure)
{
if (request == null) throw new ArgumentNullException(nameof(request));
if (configure == null) throw new ArgumentNullException(nameof(configure));

var options = request.GetAuthenticationOptions() ?? new MicrosoftIdentityMessageHandlerOptions();

configure(options);

#if NET5_0_OR_GREATER
request.Options.Set(new HttpRequestOptionsKey<MicrosoftIdentityMessageHandlerOptions>(AuthOptionsKey), options);
#else
// Use Properties dictionary for older frameworks
request.Properties[AuthOptionsKey] = options;
#endif
return request;
}

/// <summary>
/// Gets the authentication options that have been set for the HTTP request.
/// </summary>
/// <param name="request">The HTTP request message to examine.</param>
/// <returns>
/// The <see cref="MicrosoftIdentityMessageHandlerOptions"/> if previously set using
/// <see cref="WithAuthenticationOptions(HttpRequestMessage, MicrosoftIdentityMessageHandlerOptions)"/>
/// or <see cref="WithAuthenticationOptions(HttpRequestMessage, Action{MicrosoftIdentityMessageHandlerOptions})"/>,
/// otherwise <see langword="null"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="request"/> is <see langword="null"/>.
/// </exception>
/// <example>
/// <code>
/// var request = new HttpRequestMessage(HttpMethod.Get, "/api/data")
/// .WithAuthenticationOptions(options => options.Scopes.Add("custom.scope"));
///
/// var options = request.GetAuthenticationOptions();
/// if (options != null)
/// {
/// Console.WriteLine($"Request has {options.Scopes.Count} scopes configured.");
/// }
/// </code>
/// </example>
/// <remarks>
/// This method is primarily used internally by <see cref="MicrosoftIdentityMessageHandler"/>
/// but can also be useful for debugging or conditional logic based on authentication configuration.
/// </remarks>
public static MicrosoftIdentityMessageHandlerOptions? GetAuthenticationOptions(this HttpRequestMessage request)
{
if (request == null) throw new ArgumentNullException(nameof(request));

#if NET5_0_OR_GREATER
request.Options.TryGetValue(new HttpRequestOptionsKey<MicrosoftIdentityMessageHandlerOptions>(AuthOptionsKey), out var options);
return options;
#else
// Use Properties dictionary for older frameworks
return request.Properties.TryGetValue(AuthOptionsKey, out var options)
? options as MicrosoftIdentityMessageHandlerOptions
: null;
#endif
}
}
}
Loading
Loading