Skip to content

Commit 381f0c5

Browse files
committed
Add remote HTTP server mode with OAuth authentication and downstream token acquisition
- Add RunAsRemoteHttpService and OutgoingAuthStrategy options to enable MCP server to run as a remote HTTP service - Introduce ITokenProvider abstraction for dependency injection of downstream authentication tokens and credentials - Add support for On-Behalf-Of (OBO) token flow and hosting environment identity modes for outgoing authentication - Implement OAuth Protected Resource Metadata endpoint at /.well-known/oauth-protected-resource with WWW-Authenticate challenge - Add Visual Studio launch profile for debugging remote MCP server with Microsoft.Identity.Web configuration - Modernize HTTP host creation using WebApplicationBuilder with authentication and authorization middleware
1 parent 539a5a9 commit 381f0c5

File tree

31 files changed

+764
-87
lines changed

31 files changed

+764
-87
lines changed

Directory.Packages.props

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.9" />
6969
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.9" />
7070
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
71+
<PackageVersion Include="Microsoft.Identity.Abstractions" Version="9.5.0" />
72+
<PackageVersion Include="Microsoft.Identity.Web" Version="3.14.0" />
73+
<PackageVersion Include="Microsoft.Identity.Web.Azure" Version="3.14.0" />
7174
<PackageVersion Include="Microsoft.HybridRow" Version="1.1.0-preview3" />
7275
<PackageVersion Include="Microsoft.Azure.Cosmos.Aot" Version="0.1.4-preview.2" />
7376
<PackageVersion Include="Microsoft.Azure.Mcp.AzTypes.Internal.Compact" Version="0.2.802" />
@@ -78,7 +81,7 @@
7881
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
7982
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.1.25451.107" />
8083
<PackageVersion Include="System.Formats.Asn1" Version="9.0.9" />
81-
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.11.0" />
84+
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
8285
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0-rc.1.25451.107" />
8386
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.0-rc.1.25451.107" />
8487
<PackageVersion Include="System.Numerics.Tensors" Version="9.0.0" />

core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceCollectionExtensions.cs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,7 @@ public static IServiceCollection AddAzureMcpServer(this IServiceCollection servi
6464

6565
// Register tool loader strategies
6666
services.AddSingleton<CommandFactoryToolLoader>();
67-
services.AddSingleton(sp =>
68-
{
69-
return new RegistryToolLoader(
70-
sp.GetRequiredService<RegistryDiscoveryStrategy>(),
71-
sp.GetRequiredService<IOptions<ToolLoaderOptions>>(),
72-
sp.GetRequiredService<ILogger<RegistryToolLoader>>()
73-
);
74-
});
67+
services.AddSingleton<RegistryToolLoader>();
7568

7669
services.AddSingleton<SingleProxyToolLoader>();
7770
services.AddSingleton<CompositeToolLoader>();
@@ -216,7 +209,7 @@ public static IServiceCollection AddAzureMcpServer(this IServiceCollection servi
216209

217210
var mcpServerBuilder = services.AddMcpServer();
218211

219-
if (serviceStartOptions.EnableInsecureTransports)
212+
if (serviceStartOptions.EnableInsecureTransports || serviceStartOptions.RunAsRemoteHttpService)
220213
{
221214
mcpServerBuilder.WithHttpTransport();
222215
}

core/Azure.Mcp.Core/src/Areas/Server/Commands/ServiceStartCommand.cs

Lines changed: 203 additions & 34 deletions
Large diffs are not rendered by default.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Azure.Mcp.Core.Areas.Server.Models;
7+
8+
/// <summary>
9+
/// OAuth 2.0 protected resource metadata response model.
10+
/// </summary>
11+
public sealed class OAuthProtectedResourceMetadata
12+
{
13+
[JsonPropertyName("resource")]
14+
public required string Resource { get; init; }
15+
16+
[JsonPropertyName("authorization_servers")]
17+
public required string[] AuthorizationServers { get; init; }
18+
19+
[JsonPropertyName("scopes_supported")]
20+
public required string[] ScopesSupported { get; init; }
21+
22+
[JsonPropertyName("bearer_methods_supported")]
23+
public required string[] BearerMethodsSupported { get; init; }
24+
25+
[JsonPropertyName("resource_documentation")]
26+
public required string ResourceDocumentation { get; init; }
27+
28+
[JsonPropertyName("resource_signing_alg_values_supported")]
29+
public required string[] ResourceSigningAlgValuesSupported { get; init; }
30+
}
31+
32+
/// <summary>
33+
/// JSON serializer context for AOT-safe serialization.
34+
/// </summary>
35+
[JsonSerializable(typeof(OAuthProtectedResourceMetadata))]
36+
internal partial class OAuthMetadataJsonContext : JsonSerializerContext
37+
{
38+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Azure.Identity;
2+
3+
namespace Azure.Mcp.Core.Areas.Server.Options;
4+
5+
public enum OutgoingAuthenticationTypes
6+
{
7+
/// <summary>
8+
/// The value is not set and is in a default state. This is valid for
9+
/// any non-remote MCP server scenarios.
10+
/// </summary>
11+
NotSet = 0,
12+
13+
/// <summary>
14+
/// Outgoing requests will use the hosting environment's identity resolving
15+
/// in the same way as <see cref="DefaultAzureCredential"/>. This is valid
16+
/// for all hosting scenarios. This means all outgoing requests will use the
17+
/// same identity regardless of the incoming authenticate request identity,
18+
/// if any.
19+
/// </summary>
20+
UseHostingEnvironmentIdentity = 1,
21+
22+
/// <summary>
23+
/// Outgoing requests will be authenticated based on exchanging the incoming
24+
/// request's access token for a new access token valid for the downstream
25+
/// service. This is only valid for remote MCP server scenarios.
26+
/// </summary>
27+
UseOnBehalfOf = 2
28+
}

core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceOptionDefinitions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public static class ServiceOptionDefinitions
1313
public const string DebugName = "debug";
1414
public const string EnableInsecureTransportsName = "enable-insecure-transports";
1515
public const string InsecureDisableElicitationName = "insecure-disable-elicitation";
16+
public const string RunAsRemoteHttpServiceName = "run-as-remote-http-service";
17+
public const string OutgoingAuthStrategyName = "outgoing-auth-strategy";
1618

1719
public static readonly Option<string> Transport = new($"--{TransportName}")
1820
{
@@ -83,4 +85,20 @@ public static class ServiceOptionDefinitions
8385
Description = "Disable elicitation (user confirmation) before allowing high risk commands to run, such as returning Secrets (passwords) from KeyVault.",
8486
DefaultValueFactory = _ => false
8587
};
88+
89+
public static readonly Option<bool> RunAsRemoteHttpService = new(
90+
$"--{RunAsRemoteHttpServiceName}")
91+
{
92+
Required = false,
93+
Description = "Run the server as a remote HTTP service requiring authentication and authorization.",
94+
DefaultValueFactory = _ => false
95+
};
96+
97+
public static readonly Option<OutgoingAuthenticationTypes> OutgoingAuthStrategy = new(
98+
$"--{OutgoingAuthStrategyName}")
99+
{
100+
Required = false,
101+
Description = "Outgoing authentication strategy for Azure service requests. Valid values: NotSet, UseHostingEnvironmentIdentity, UseOnBehalfOf.",
102+
DefaultValueFactory = _ => OutgoingAuthenticationTypes.NotSet
103+
};
86104
}

core/Azure.Mcp.Core/src/Areas/Server/Options/ServiceStartOptions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,18 @@ public class ServiceStartOptions
6464
/// </summary>
6565
[JsonPropertyName("insecureDisableElicitation")]
6666
public bool InsecureDisableElicitation { get; set; } = false;
67+
68+
/// <summary>
69+
/// Gets or sets whether the server should run as a remote HTTP service.
70+
/// When true, the server will require authentication and authorization.
71+
/// </summary>
72+
[JsonPropertyName("runAsRemoteHttpService")]
73+
public bool RunAsRemoteHttpService { get; set; } = false;
74+
75+
/// <summary>
76+
/// Gets or sets the outgoing authentication strategy for Azure service requests.
77+
/// Determines whether to use hosting environment identity or on-behalf-of flow.
78+
/// </summary>
79+
[JsonPropertyName("outgoingAuthStrategy")]
80+
public OutgoingAuthenticationTypes OutgoingAuthStrategy { get; set; } = OutgoingAuthenticationTypes.NotSet;
6781
}

core/Azure.Mcp.Core/src/Azure.Mcp.Core.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
<PackageReference Include="Azure.ResourceManager" />
2222
<PackageReference Include="Azure.ResourceManager.ResourceGraph" />
2323
<PackageReference Include="Microsoft.Extensions.Azure" />
24+
<PackageReference Include="Microsoft.Identity.Abstractions" />
25+
<PackageReference Include="Microsoft.Identity.Web" />
26+
<PackageReference Include="Microsoft.Identity.Web.Azure" />
2427
<PackageReference Include="ModelContextProtocol.AspNetCore" />
2528
<PackageReference Include="System.Linq.AsyncEnumerable" />
2629
</ItemGroup>
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Mcp.Core.Areas.Server.Options;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
8+
using Microsoft.Identity.Web;
9+
10+
namespace Azure.Mcp.Core.Services.Azure.Authentication;
11+
12+
/// <summary>
13+
/// Extension methods for configuring Azure authentication services.
14+
/// </summary>
15+
public static class AuthenticationServiceCollectionExtensions
16+
{
17+
/// <summary>
18+
/// Adds <see cref="IAzureTokenCredentialProvider"/> to the service collection based on the
19+
/// specified <see cref="ServiceStartOptions"/>.
20+
/// </summary>
21+
/// <param name="services">The service collection.</param>
22+
/// <param name="serverOptions">The server options containing the authentication strategy.</param>
23+
/// <param name="configuration">Optional configuration required for On-Behalf-Of authentication.</param>
24+
/// <returns>The service collection.</returns>
25+
/// <exception cref="InvalidOperationException">
26+
/// Thrown when the authentication strategy is unsupported or when configuration is required but not provided.
27+
/// </exception>
28+
/// <remarks>
29+
/// This method registers the appropriate <see cref="IAzureTokenCredentialProvider"/> implementation
30+
/// based on the <see cref="ServiceStartOptions.OutgoingAuthStrategy"/>:
31+
/// <list type="bullet">
32+
/// <item><description><see cref="OutgoingAuthenticationTypes.UseHostingEnvironmentIdentity"/> - registers <see cref="SingleIdentityTokenCredentialProvider"/></description></item>
33+
/// <item><description><see cref="OutgoingAuthenticationTypes.UseOnBehalfOf"/> - registers <see cref="HttpOnBehalfOfTokenCredentialProvider"/> (requires configuration)</description></item>
34+
/// </list>
35+
/// </remarks>
36+
public static IServiceCollection AddAzureTokenCredentialProvider(
37+
this IServiceCollection services,
38+
ServiceStartOptions serverOptions,
39+
IConfiguration? configuration = null)
40+
{
41+
42+
// ASSUMPTION: validation has already occurred and defaults have been applied in ServiceStartCommand.BindOptions
43+
if (serverOptions.OutgoingAuthStrategy == OutgoingAuthenticationTypes.UseHostingEnvironmentIdentity)
44+
{
45+
services.AddSingleIdentityTokenCredentialProvider();
46+
}
47+
else if (serverOptions.OutgoingAuthStrategy == OutgoingAuthenticationTypes.UseOnBehalfOf)
48+
{
49+
if (configuration == null)
50+
{
51+
throw new InvalidOperationException("Configuration is required for On-Behalf-Of authentication strategy.");
52+
}
53+
services.AddHttpOnBehalfOfTokenCredentialProvider(configuration);
54+
}
55+
else
56+
{
57+
throw new InvalidOperationException($"Unsupported outgoing authentication strategy '{serverOptions.OutgoingAuthStrategy}'.");
58+
}
59+
60+
return services;
61+
}
62+
63+
/// <summary>
64+
/// Adds <see cref="SingleIdentityTokenCredentialProvider"/> as a
65+
/// <see cref="IAzureTokenCredentialProvider"/> with lifetime <see cref="ServiceLifetime.Singleton"/>
66+
/// into the service collection.
67+
/// </summary>
68+
/// <param name="services">The service collection.</param>
69+
/// <returns>The service collection.</returns>
70+
/// <remarks>
71+
/// <para>
72+
/// This method registers the single identity token credential provider which uses the hosting
73+
/// environment's identity (e.g., a Managed Identity or a user principal using Azure CLI, Visual
74+
/// Studio, etc.) via <see cref="CustomChainedCredential"/>.
75+
/// </para>
76+
/// <para>
77+
/// This method is idempotent - calling it multiple times will not register duplicate services.
78+
/// </para>
79+
/// </remarks>
80+
public static IServiceCollection AddSingleIdentityTokenCredentialProvider(this IServiceCollection services)
81+
{
82+
services.TryAddSingleton<IAzureTokenCredentialProvider, SingleIdentityTokenCredentialProvider>();
83+
return services;
84+
}
85+
86+
/// <summary>
87+
/// Adds <see cref="HttpOnBehalfOfTokenCredentialProvider"/> as a
88+
/// <see cref="IAzureTokenCredentialProvider"/> with lifetime <see cref="ServiceLifetime.Singleton"/>
89+
/// into the service collection, along with all required dependencies.
90+
/// </summary>
91+
/// <param name="services">The service collection.</param>
92+
/// <param name="configuration">The application configuration containing Azure AD settings.</param>
93+
/// <returns>The service collection.</returns>
94+
/// <remarks>
95+
/// <para>
96+
/// This method follows the dependency graph pattern by registering all required dependencies:
97+
/// </para>
98+
/// <list type="bullet">
99+
/// <item><description><see cref="Microsoft.AspNetCore.Http.IHttpContextAccessor"/> - for accessing the current HTTP context</description></item>
100+
/// <item><description>Microsoft Identity Web authentication services - for token acquisition</description></item>
101+
/// <item><description>In-memory token caches - for caching acquired tokens</description></item>
102+
/// <item><description><see cref="MicrosoftIdentityTokenCredential"/> - for Azure SDK integration</description></item>
103+
/// </list>
104+
/// <para>
105+
/// Note: This method will override any existing <see cref="IAzureTokenCredentialProvider"/> registration
106+
/// since On-Behalf-Of authentication requires specific configuration and cannot be idempotent in the same
107+
/// way as the single identity provider.
108+
/// </para>
109+
/// </remarks>
110+
private static IServiceCollection AddHttpOnBehalfOfTokenCredentialProvider(
111+
this IServiceCollection services,
112+
IConfiguration configuration)
113+
{
114+
// Dependencies - directly in constructor.
115+
services.AddHttpContextAccessor();
116+
117+
// Dependencies - indirectly required to get MicrosoftIdentityTokenCredential.
118+
services.AddMicrosoftIdentityWebAppAuthentication(configuration)
119+
.EnableTokenAcquisitionToCallDownstreamApi()
120+
.AddInMemoryTokenCaches();
121+
services.AddMicrosoftIdentityAzureTokenCredential();
122+
123+
// Register the OBO token provider. This uses AddSingleton (not TryAdd) to override
124+
// any default registration, since OBO is an explicit configuration choice.
125+
services.AddSingleton<IAzureTokenCredentialProvider, HttpOnBehalfOfTokenCredentialProvider>();
126+
return services;
127+
}
128+
}

core/Azure.Mcp.Core/src/Services/Azure/Authentication/CustomChainedCredential.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ namespace Azure.Mcp.Core.Services.Azure.Authentication;
2828
///
2929
/// After the credential chain, Interactive Browser Authentication with Identity Broker is always added as the final fallback.
3030
/// </remarks>
31-
public class CustomChainedCredential(string? tenantId = null, ILogger<CustomChainedCredential>? logger = null) : TokenCredential
31+
internal class CustomChainedCredential(string? tenantId = null, ILogger<CustomChainedCredential>? logger = null) : TokenCredential
3232
{
3333
private TokenCredential? _credential;
3434
private readonly ILogger<CustomChainedCredential>? _logger = logger;

0 commit comments

Comments
 (0)