diff --git a/agent-plugins/agents-for-net/skills/agents-sdk-dotnet/SKILL.md b/agent-plugins/agents-for-net/skills/agents-sdk-dotnet/SKILL.md index c78a9a34..14f4de4c 100644 --- a/agent-plugins/agents-for-net/skills/agents-sdk-dotnet/SKILL.md +++ b/agent-plugins/agents-for-net/skills/agents-sdk-dotnet/SKILL.md @@ -188,7 +188,7 @@ Put in `appsettings.Development.json` (not `appsettings.json`): ## Quick Start -**Prerequisite:** Copy [`AspNetExtensions.cs`](https://github.com/microsoft/Agents/blob/main/samples/dotnet/quickstart/AspNetExtensions.cs) into your project. This provides `AddAgentAspNetAuthentication` for JWT token validation. +**Prerequisite:** Copy [`AspNetExtensions.cs`](https://github.com/microsoft/Agents-for-net/blob/main/src/samples/Shared/AspNetExtensions.cs) into your project. This provides `AddAgentAspNetAuthentication` for JWT token validation. **Program.cs:** @@ -752,14 +752,14 @@ If `TokenValidation:Enabled` is `true` with no valid credentials configured, eve **7. Missing AspNetExtensions.cs for AddAgentAspNetAuthentication** -`AddAgentAspNetAuthentication` is NOT built into the SDK packages — it's a helper extension that must be copied into your project from the quickstart samples. +`AddAgentAspNetAuthentication` is NOT built into the SDK packages — it's a helper extension that must be copied into your project from the shared samples. ```csharp // ERROR — CS1061: 'IServiceCollection' does not contain a definition for 'AddAgentAspNetAuthentication' builder.Services.AddAgentAspNetAuthentication(builder.Configuration); // FIX — Copy AspNetExtensions.cs from the samples repo into your project: -// https://github.com/microsoft/Agents/blob/main/samples/dotnet/quickstart/AspNetExtensions.cs +// https://github.com/microsoft/Agents-for-net/blob/main/src/samples/Shared/AspNetExtensions.cs ``` **8. Missing `Microsoft.Agents.Builder.State` using for ITurnState** diff --git a/agent-plugins/agents-for-net/skills/bf-to-agents-sdk-dotnet-migration/SKILL.md b/agent-plugins/agents-for-net/skills/bf-to-agents-sdk-dotnet-migration/SKILL.md index 7b031e88..28af6374 100644 --- a/agent-plugins/agents-for-net/skills/bf-to-agents-sdk-dotnet-migration/SKILL.md +++ b/agent-plugins/agents-for-net/skills/bf-to-agents-sdk-dotnet-migration/SKILL.md @@ -395,7 +395,7 @@ builder.AddAgent(); ## Files to Add -Every migrated project needs `AspNetExtensions.cs` — the `AddAgentAspNetAuthentication()` extension method is **not in any NuGet package**; it is a sample-provided file. Copy it from the Agents SDK quickstart sample (https://github.com/microsoft/Agents/blob/main/samples/dotnet/quickstart/AspNetExtensions.cs). +Every migrated project needs `AspNetExtensions.cs` — the `AddAgentAspNetAuthentication()` extension method is **not in any NuGet package**; it is a sample-provided file. Copy it from the Agents SDK shared samples (https://github.com/microsoft/Agents-for-net/blob/main/src/samples/Shared/AspNetExtensions.cs). --- diff --git a/samples/dotnet/Agent Framework/AgentFrameworkWeather.csproj b/samples/dotnet/Agent Framework/AgentFrameworkWeather.csproj index ec4da8c9..9a657687 100644 --- a/samples/dotnet/Agent Framework/AgentFrameworkWeather.csproj +++ b/samples/dotnet/Agent Framework/AgentFrameworkWeather.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -13,14 +13,14 @@ - - + + - + @@ -31,8 +31,8 @@ - - + + diff --git a/samples/dotnet/Agent Framework/AspNetExtensions.cs b/samples/dotnet/Agent Framework/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/Agent Framework/AspNetExtensions.cs +++ b/samples/dotnet/Agent Framework/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/Agent Framework/Program.cs b/samples/dotnet/Agent Framework/Program.cs index 6dfdf10e..eb166f1a 100644 --- a/samples/dotnet/Agent Framework/Program.cs +++ b/samples/dotnet/Agent Framework/Program.cs @@ -1,92 +1,92 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using AgentFrameworkWeather; -using AgentFrameworkWeather.Agent; -using Azure; -using Azure.AI.OpenAI; -using Microsoft.Agents.Builder; -using Microsoft.Agents.Core; -using Microsoft.Agents.Hosting.AspNetCore; -using Microsoft.Agents.Storage; -using Microsoft.Agents.Storage.Transcript; -using Microsoft.Extensions.AI; -using System.Reflection; - -var builder = WebApplication.CreateBuilder(args); - -builder.Configuration.AddUserSecrets(Assembly.GetExecutingAssembly()); -builder.Services.AddControllers(); -builder.Services.AddHttpClient("WebClient", client => client.Timeout = TimeSpan.FromSeconds(600)); -builder.Services.AddHttpContextAccessor(); - +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AgentFrameworkWeather; +using AgentFrameworkWeather.Agent; +using Azure; +using Azure.AI.OpenAI; +using Microsoft.Agents.Builder; +using Microsoft.Agents.Core; +using Microsoft.Agents.Hosting.AspNetCore; +using Microsoft.Agents.Storage; +using Microsoft.Agents.Storage.Transcript; +using Microsoft.Extensions.AI; +using System.Reflection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration.AddUserSecrets(Assembly.GetExecutingAssembly()); +builder.Services.AddControllers(); +builder.Services.AddHttpClient("WebClient", client => client.Timeout = TimeSpan.FromSeconds(600)); +builder.Services.AddHttpContextAccessor(); + // Configure defaults for Aspire dashboard builder.ConfigureOtelProviders(); - -builder.Logging.AddConsole(); - -// Add AspNet token validation -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); - -// Register IStorage. For development, MemoryStorage is suitable. -// For production Agents, persisted storage should be used so -// that state survives Agent restarts, and operate correctly -// in a cluster of Agent instances. -builder.Services.AddSingleton(); - -// Add the bot (which is transient) -builder.AddAgent(); - -// Register IChatClient with correct types -builder.Services.AddSingleton(sp => { - - var confSvc = sp.GetRequiredService(); - var endpoint = confSvc["AIServices:AzureOpenAI:Endpoint"] ?? string.Empty; - var apiKey = confSvc["AIServices:AzureOpenAI:ApiKey"] ?? string.Empty; - var deployment = confSvc["AIServices:AzureOpenAI:DeploymentName"] ?? string.Empty; - - // Validate OpenWeatherAPI key. - var openWeatherApiKey = confSvc["OpenWeatherApiKey"] ?? string.Empty; - - AssertionHelpers.ThrowIfNullOrEmpty(endpoint, "AIServices:AzureOpenAI:Endpoint configuration is missing and required."); - AssertionHelpers.ThrowIfNullOrEmpty(apiKey, "AIServices:AzureOpenAI:ApiKey configuration is missing and required."); - AssertionHelpers.ThrowIfNullOrEmpty(deployment, "AIServices:AzureOpenAI:DeploymentName configuration is missing and required."); - AssertionHelpers.ThrowIfNullOrEmpty(openWeatherApiKey, "OpenWeatherApiKey configuration is missing and required."); - - // Convert endpoint to Uri - var endpointUri = new Uri(endpoint); - - // Convert apiKey to ApiKeyCredential - var apiKeyCredential = new AzureKeyCredential(apiKey); - - // Create and return the AzureOpenAIClient's ChatClient - return new AzureOpenAIClient(endpointUri, apiKeyCredential).GetChatClient(deployment).AsIChatClient(); -}); - -// Uncomment to add transcript logging middleware to log all conversations to files -builder.Services.AddSingleton([new TranscriptLoggerMiddleware(new FileTranscriptLogger())]); - -var app = builder.Build(); - -app.UseRouting(); -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); - -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !(app.Environment.IsDevelopment() || app.Environment.EnvironmentName == "Playground")); - -if (app.Environment.IsDevelopment() || app.Environment.EnvironmentName == "Playground") -{ - app.UseDeveloperExceptionPage(); + +builder.Logging.AddConsole(); + +// Register IStorage. For development, MemoryStorage is suitable. +// For production Agents, persisted storage should be used so +// that state survives Agent restarts, and operate correctly +// in a cluster of Agent instances. +builder.Services.AddSingleton(); + +// Add the bot (which is transient) and configure AspNet token validation. +// Authorization (and therefore required auth on the mapped endpoints) is enabled +// for all environments except Development and Playground. +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization( + b => b.AddAgentAspNetAuthentication(), + forceEnable: !(builder.Environment.IsDevelopment() || builder.Environment.EnvironmentName == "Playground")); + +// Register IChatClient with correct types +builder.Services.AddSingleton(sp => { + + var confSvc = sp.GetRequiredService(); + var endpoint = confSvc["AIServices:AzureOpenAI:Endpoint"] ?? string.Empty; + var apiKey = confSvc["AIServices:AzureOpenAI:ApiKey"] ?? string.Empty; + var deployment = confSvc["AIServices:AzureOpenAI:DeploymentName"] ?? string.Empty; + + // Validate OpenWeatherAPI key. + var openWeatherApiKey = confSvc["OpenWeatherApiKey"] ?? string.Empty; + + AssertionHelpers.ThrowIfNullOrEmpty(endpoint, "AIServices:AzureOpenAI:Endpoint configuration is missing and required."); + AssertionHelpers.ThrowIfNullOrEmpty(apiKey, "AIServices:AzureOpenAI:ApiKey configuration is missing and required."); + AssertionHelpers.ThrowIfNullOrEmpty(deployment, "AIServices:AzureOpenAI:DeploymentName configuration is missing and required."); + AssertionHelpers.ThrowIfNullOrEmpty(openWeatherApiKey, "OpenWeatherApiKey configuration is missing and required."); + + // Convert endpoint to Uri + var endpointUri = new Uri(endpoint); + + // Convert apiKey to ApiKeyCredential + var apiKeyCredential = new AzureKeyCredential(apiKey); + + // Create and return the AzureOpenAIClient's ChatClient + return new AzureOpenAIClient(endpointUri, apiKeyCredential).GetChatClient(deployment).AsIChatClient(); +}); + +// Uncomment to add transcript logging middleware to log all conversations to files +builder.Services.AddSingleton([new TranscriptLoggerMiddleware(new FileTranscriptLogger())]); + +var app = builder.Build(); + +// Add the authentication and authorization middleware to the request pipeline +// (with routing enabled so the controllers below can be mapped). +app.UseAgents(useRouting: true); + +// Map the default agent endpoints: GET "/" and the agent message endpoints. +// Authorization is required automatically when AddAgentAuthorization enabled it above. +app.MapDefaultAgentEndpoints(); + +if (app.Environment.IsDevelopment() || app.Environment.EnvironmentName == "Playground") +{ + app.UseDeveloperExceptionPage(); app.MapControllers().AllowAnonymous(); -} -else -{ - app.MapControllers(); -} - +} +else +{ + app.MapControllers(); +} + app.Run(); \ No newline at end of file diff --git a/samples/dotnet/Agent Framework/README.md b/samples/dotnet/Agent Framework/README.md index a264fb9e..a731af38 100644 --- a/samples/dotnet/Agent Framework/README.md +++ b/samples/dotnet/Agent Framework/README.md @@ -114,7 +114,7 @@ Open `appsettings.json` and replace the placeholder values (`----`) in the `AISe | Section | Purpose in This Sample | |---|---| -| `TokenValidation.Enabled: false` | Token validation is disabled so the agent runs without auth in the local `Development` environment. Tokens should be validated when deploying to Teams or Copilot. | +| `TokenValidation` | Token validation is disabled in the local `Development` environment (determined by `AddAgentAuthorization` and the `forceEnable` argument) and enforced automatically in other environments. Set `Audiences` and `TenantId` when deploying to Teams or Copilot. | | `Connections.BotServiceConnection` | Configures the outbound connection to Azure Bot Framework services. Required for Teams / Copilot deployment; not needed for the local Playground. | | `AgentApplicationOptions` | Controls typing indicators and mention normalization behavior. | @@ -210,7 +210,7 @@ To deploy the agent to Teams or Microsoft 365 Copilot, you need a registered Azu 1. Register an **Azure Bot** resource in the Azure portal and note the **App ID** (Client ID) and **Tenant ID**. 2. Update `appsettings.json` (or environment variables / Key Vault) with the correct `TokenValidation.Audiences`, `TokenValidation.TenantId`, and `Connections.BotServiceConnection.Settings.ClientId` values. -3. Set `TokenValidation.Enabled` to `true`. +3. Run the app outside the `Development` environment (for example, set `ASPNETCORE_ENVIRONMENT=Production`) so token validation is enforced. 4. Deploy the ASP.NET Core application to Azure App Service (or any HTTPS-accessible host) and update the Azure Bot messaging endpoint to point to your deployment. ### App Package — Configure `appPackage/manifest.json` diff --git a/samples/dotnet/Agent Framework/appsettings.Playground.json b/samples/dotnet/Agent Framework/appsettings.Playground.json index 4fe6bebe..0c0bc717 100644 --- a/samples/dotnet/Agent Framework/appsettings.Playground.json +++ b/samples/dotnet/Agent Framework/appsettings.Playground.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "---" // this is the Client ID used for the Azure Bot ], diff --git a/samples/dotnet/Agent Framework/appsettings.json b/samples/dotnet/Agent Framework/appsettings.json index 0161ea42..259ccbab 100644 --- a/samples/dotnet/Agent Framework/appsettings.json +++ b/samples/dotnet/Agent Framework/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "{{ClientId}}" // this is the Client ID used for the Azure Bot ], diff --git a/samples/dotnet/auto-signin/AspNetExtensions.cs b/samples/dotnet/auto-signin/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/auto-signin/AspNetExtensions.cs +++ b/samples/dotnet/auto-signin/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/auto-signin/AutoSignIn.csproj b/samples/dotnet/auto-signin/AutoSignIn.csproj index 30b352b6..453abd51 100644 --- a/samples/dotnet/auto-signin/AutoSignIn.csproj +++ b/samples/dotnet/auto-signin/AutoSignIn.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -7,8 +7,8 @@ - - + + diff --git a/samples/dotnet/auto-signin/Program.cs b/samples/dotnet/auto-signin/Program.cs index 87431638..7e9d453c 100644 --- a/samples/dotnet/auto-signin/Program.cs +++ b/samples/dotnet/auto-signin/Program.cs @@ -6,15 +6,14 @@ using Microsoft.Agents.Storage; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient(); - // Add the AgentApplication, which contains the logic for responding to // user messages. -builder.AddAgent(); +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so @@ -22,21 +21,12 @@ // in a cluster of Agent instances. builder.Services.AddSingleton(); -// Add AspNet token validation for Azure Bot Service and Entra. Authentication is -// configured in the appsettings.json "TokenValidation" section. -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); - WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); app.Run(); diff --git a/samples/dotnet/auto-signin/README.md b/samples/dotnet/auto-signin/README.md index 92ecd2aa..4cb8e437 100644 --- a/samples/dotnet/auto-signin/README.md +++ b/samples/dotnet/auto-signin/README.md @@ -115,13 +115,13 @@ The sample uses the bot OAuth capabilities in [Azure Bot Service](https://docs.b - Note that if running this in Teams and SSO is setup, you shouldn't see any "sign in" prompts. This is true in this sample since we are only requesting a basic set of scopes that Teams doesn't require additional consent for. ## Enabling JWT token validation -1. By default, the AspNet token validation is disabled in order to support local debugging. -1. Enable by updating appsettings +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot. ```json "TokenValidation": { - "Enabled": true, "Audiences": [ - "{{ClientId}}" // this is the Client ID used for the Azure Bot + "{{ClientId}}" ], "TenantId": "{{TenantId}}" }, diff --git a/samples/dotnet/auto-signin/appsettings.json b/samples/dotnet/auto-signin/appsettings.json index 0e1e8e99..575dc75c 100644 --- a/samples/dotnet/auto-signin/appsettings.json +++ b/samples/dotnet/auto-signin/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "{{ClientId}}" // this is the Client ID used for the Azure Bot ], diff --git a/samples/dotnet/azure-ai-streaming/AspNetExtensions.cs b/samples/dotnet/azure-ai-streaming/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/azure-ai-streaming/AspNetExtensions.cs +++ b/samples/dotnet/azure-ai-streaming/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/azure-ai-streaming/Program.cs b/samples/dotnet/azure-ai-streaming/Program.cs index 74dc1edd..7d7887e4 100644 --- a/samples/dotnet/azure-ai-streaming/Program.cs +++ b/samples/dotnet/azure-ai-streaming/Program.cs @@ -14,8 +14,6 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient(); - builder.Services.AddTransient(sp => { return new AzureOpenAIClient( @@ -26,7 +24,9 @@ // Add the AgentApplication, which contains the logic for responding to // user messages. -builder.AddAgent(); +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so @@ -34,21 +34,12 @@ // in a cluster of Agent instances. builder.Services.AddSingleton(); -// Add AspNet token validation for Azure Bot Service and Entra. Authentication is -// configured in the appsettings.json "TokenValidation" section. -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); - WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); app.Run(); diff --git a/samples/dotnet/azure-ai-streaming/README.md b/samples/dotnet/azure-ai-streaming/README.md index 6cb12761..54902821 100644 --- a/samples/dotnet/azure-ai-streaming/README.md +++ b/samples/dotnet/azure-ai-streaming/README.md @@ -95,13 +95,13 @@ This is a sample of a simple Agent that is hosted on an Asp.net core web service 1. After a short period of time, the agent shows up in Microsoft Teams and Microsoft 365 Copilot. ## Enabling JWT token validation -1. By default, the AspNet token validation is disabled in order to support local debugging. -1. Enable by updating appsettings +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot. ```json "TokenValidation": { - "Enabled": true, "Audiences": [ - "{{ClientId}}" // this is the Client ID used for the Azure Bot + "{{ClientId}}" ], "TenantId": "{{TenantId}}" }, diff --git a/samples/dotnet/azure-ai-streaming/StreamingMessageAgent.csproj b/samples/dotnet/azure-ai-streaming/StreamingMessageAgent.csproj index a685342a..e40692f0 100644 --- a/samples/dotnet/azure-ai-streaming/StreamingMessageAgent.csproj +++ b/samples/dotnet/azure-ai-streaming/StreamingMessageAgent.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -11,8 +11,8 @@ - - + + diff --git a/samples/dotnet/azure-ai-streaming/appsettings.json b/samples/dotnet/azure-ai-streaming/appsettings.json index 720d61f6..393e9298 100644 --- a/samples/dotnet/azure-ai-streaming/appsettings.json +++ b/samples/dotnet/azure-ai-streaming/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "{{ClientId}}" // this is the Client ID used for the Azure Bot ], diff --git a/samples/dotnet/copilot-sdk/AspNetExtensions.cs b/samples/dotnet/copilot-sdk/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/copilot-sdk/AspNetExtensions.cs +++ b/samples/dotnet/copilot-sdk/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/copilot-sdk/CopilotSdk.csproj b/samples/dotnet/copilot-sdk/CopilotSdk.csproj index 45e92936..9c401677 100644 --- a/samples/dotnet/copilot-sdk/CopilotSdk.csproj +++ b/samples/dotnet/copilot-sdk/CopilotSdk.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,9 +8,9 @@ - - - + + + \ No newline at end of file diff --git a/samples/dotnet/copilot-sdk/DungeonScribeAgent.cs b/samples/dotnet/copilot-sdk/DungeonScribeAgent.cs index 351765b3..55d7cc21 100644 --- a/samples/dotnet/copilot-sdk/DungeonScribeAgent.cs +++ b/samples/dotnet/copilot-sdk/DungeonScribeAgent.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using GitHub.Copilot.SDK; +using GitHub.Copilot; using CopilotSdk.Tools; using Microsoft.Agents.Builder; using Microsoft.Agents.Builder.App; @@ -103,7 +103,7 @@ private async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnState var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); bool anyDeltas = false; - using var subscription = session.On(evt => + using var subscription = session.On(evt => { switch (evt) { diff --git a/samples/dotnet/copilot-sdk/Program.cs b/samples/dotnet/copilot-sdk/Program.cs index 96f1a67d..ab5359b8 100644 --- a/samples/dotnet/copilot-sdk/Program.cs +++ b/samples/dotnet/copilot-sdk/Program.cs @@ -10,11 +10,11 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient(); - // Add the AgentApplication, which contains the logic for responding to // user messages. -builder.AddAgent(); +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so @@ -22,21 +22,12 @@ // in a cluster of Agent instances. builder.Services.AddSingleton(); -// Add AspNet token validation for Azure Bot Service and Entra. Authentication is -// configured in the appsettings.json "TokenValidation" section. -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); - WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); app.Run(); diff --git a/samples/dotnet/copilot-sdk/README.md b/samples/dotnet/copilot-sdk/README.md index 6f383d9a..f2c183ac 100644 --- a/samples/dotnet/copilot-sdk/README.md +++ b/samples/dotnet/copilot-sdk/README.md @@ -131,13 +131,13 @@ This sample demonstrates how to: - keep lightweight conversation-scoped state for inventory tracking ## Enabling JWT token validation -1. By default, the AspNet token validation is disabled in order to support local debugging. -1. Enable by updating appsettings +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot. ```json "TokenValidation": { - "Enabled": true, "Audiences": [ - "{{ClientId}}" // this is the Client ID used for the Azure Bot + "{{ClientId}}" ], "TenantId": "{{TenantId}}" }, diff --git a/samples/dotnet/copilot-sdk/appsettings.json b/samples/dotnet/copilot-sdk/appsettings.json index ea574831..99bb027a 100644 --- a/samples/dotnet/copilot-sdk/appsettings.json +++ b/samples/dotnet/copilot-sdk/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "{{ClientId}}" ], diff --git a/samples/dotnet/copilotstudio-client/CopilotStudioClient.csproj b/samples/dotnet/copilotstudio-client/CopilotStudioClient.csproj index 057a5cc8..45157953 100644 --- a/samples/dotnet/copilotstudio-client/CopilotStudioClient.csproj +++ b/samples/dotnet/copilotstudio-client/CopilotStudioClient.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,8 +8,8 @@ - - + + diff --git a/samples/dotnet/copilotstudio-skill/AspNetExtensions.cs b/samples/dotnet/copilotstudio-skill/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/copilotstudio-skill/AspNetExtensions.cs +++ b/samples/dotnet/copilotstudio-skill/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/copilotstudio-skill/CopilotStudioEchoSkill.csproj b/samples/dotnet/copilotstudio-skill/CopilotStudioEchoSkill.csproj index b966b1a0..8c117e84 100644 --- a/samples/dotnet/copilotstudio-skill/CopilotStudioEchoSkill.csproj +++ b/samples/dotnet/copilotstudio-skill/CopilotStudioEchoSkill.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -7,8 +7,8 @@ - - + + \ No newline at end of file diff --git a/samples/dotnet/copilotstudio-skill/Program.cs b/samples/dotnet/copilotstudio-skill/Program.cs index 3b56879d..39c9f54c 100644 --- a/samples/dotnet/copilotstudio-skill/Program.cs +++ b/samples/dotnet/copilotstudio-skill/Program.cs @@ -6,15 +6,14 @@ using Microsoft.Agents.Storage; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient(); - // Add the AgentApplication, which contains the logic for responding to // messages from Copilot Studio. -builder.AddAgent(); +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so @@ -22,21 +21,12 @@ // in a cluster of Agent instances. builder.Services.AddSingleton(); -// Add AspNet token validation for Azure Bot Service and Entra. Authentication is -// configured in the appsettings.json "TokenValidation" section. -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); - WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); app.Run(); diff --git a/samples/dotnet/copilotstudio-skill/README.md b/samples/dotnet/copilotstudio-skill/README.md index 923c1579..ab4f992c 100644 --- a/samples/dotnet/copilotstudio-skill/README.md +++ b/samples/dotnet/copilotstudio-skill/README.md @@ -80,13 +80,13 @@ This sample is intended to introduce you to: - Test the agent in Copilot Studio. ## Enabling JWT token validation -1. By default, the AspNet token validation is disabled in order to support local debugging. -1. Enable by updating appsettings +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot. ```json "TokenValidation": { - "Enabled": true, "Audiences": [ - "{{ClientId}}" // this is the Client ID used for the Azure Bot + "{{ClientId}}" ], "TenantId": "{{TenantId}}" }, diff --git a/samples/dotnet/copilotstudio-skill/appsettings.json b/samples/dotnet/copilotstudio-skill/appsettings.json index a3da0fee..e2742cf3 100644 --- a/samples/dotnet/copilotstudio-skill/appsettings.json +++ b/samples/dotnet/copilotstudio-skill/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "{{ClientId}}" // this is the Client ID used for the Azure Bot ], diff --git a/samples/dotnet/genesys-handoff/AspNetExtensions.cs b/samples/dotnet/genesys-handoff/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/genesys-handoff/AspNetExtensions.cs +++ b/samples/dotnet/genesys-handoff/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/genesys-handoff/GenesysHandoff.csproj b/samples/dotnet/genesys-handoff/GenesysHandoff.csproj index ef87ac2a..83362715 100644 --- a/samples/dotnet/genesys-handoff/GenesysHandoff.csproj +++ b/samples/dotnet/genesys-handoff/GenesysHandoff.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/samples/dotnet/genesys-handoff/Program.cs b/samples/dotnet/genesys-handoff/Program.cs index daf9c0bb..ffc89afd 100644 --- a/samples/dotnet/genesys-handoff/Program.cs +++ b/samples/dotnet/genesys-handoff/Program.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using System; using System.Threading; @@ -68,24 +67,17 @@ // Add the AgentApplication, which contains the logic for responding to // user messages. -builder.AddAgent(); - -// Add AspNet token validation for Azure Bot Service and Entra. Authentication is -// configured in the appsettings.json "TokenValidation" section. -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); // This receives outbound proactive messages from Genesys to be sent to users var genesysOutboundRoute = app.MapPost("/api/outbound", async (HttpRequest request, HttpResponse response, IChannelAdapter channelAdapter, GenesysWebhookHandler webhookHandler, CancellationToken cancellationToken) => diff --git a/samples/dotnet/genesys-handoff/README.md b/samples/dotnet/genesys-handoff/README.md index b48ff677..d48f4a9f 100644 --- a/samples/dotnet/genesys-handoff/README.md +++ b/samples/dotnet/genesys-handoff/README.md @@ -346,11 +346,10 @@ Follow the guide for [configuring your .NET agent to use OAuth](https://learn.mi ### 4.4. Update Token Validation -Update appsettings.json with the `TokenValidation` section to secure your bot endpoint. Set the `Audiences` to your Azure Bot App ID (the Application (client) ID from section 4.1) and `TenantId` to the Tenant ID of the app registration: +By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. Update appsettings.json with the `TokenValidation` section to secure your bot endpoint. Set the `Audiences` to your Azure Bot App ID (the Application (client) ID from section 4.1) and `TenantId` to the Tenant ID of the app registration: ```json "TokenValidation": { - "Enabled": true, "Audiences": [ "{{ClientID}}" // App ID from Azure Bot registration (section 4.1) ], diff --git a/samples/dotnet/genesys-handoff/appsettings.json b/samples/dotnet/genesys-handoff/appsettings.json index f8a28abe..c5b60a01 100644 --- a/samples/dotnet/genesys-handoff/appsettings.json +++ b/samples/dotnet/genesys-handoff/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": true, "Audiences": [ "{{ClientID}}" ], diff --git a/samples/dotnet/multiagent/AspNetExtensions.cs b/samples/dotnet/multiagent/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/multiagent/AspNetExtensions.cs +++ b/samples/dotnet/multiagent/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/multiagent/MultiAgent.csproj b/samples/dotnet/multiagent/MultiAgent.csproj index d327a8a8..b135e440 100644 --- a/samples/dotnet/multiagent/MultiAgent.csproj +++ b/samples/dotnet/multiagent/MultiAgent.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -15,8 +15,8 @@ - - + + diff --git a/samples/dotnet/multiagent/Program.cs b/samples/dotnet/multiagent/Program.cs index 081e0576..a8b32499 100644 --- a/samples/dotnet/multiagent/Program.cs +++ b/samples/dotnet/multiagent/Program.cs @@ -5,17 +5,16 @@ using Microsoft.Agents.Storage; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using MultiAgent; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient(); - // Add the AgentApplication, which contains the logic for responding to // user messages. -builder.AddAgent(); -builder.AddAgent(); +builder.AddAgentDefaults() + .AddAgent() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so @@ -23,15 +22,12 @@ // in a cluster of Agent instances. builder.Services.AddSingleton(); -// Configure the HTTP request pipeline. - WebApplication app = builder.Build(); -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); app.Run(); diff --git a/samples/dotnet/multiagent/README.md b/samples/dotnet/multiagent/README.md index a1672940..7628ce82 100644 --- a/samples/dotnet/multiagent/README.md +++ b/samples/dotnet/multiagent/README.md @@ -116,5 +116,19 @@ This demonstrates an Agent that implements multiple AgentApplication instances. 1. Select **Test in WebChat** on either Azure Bot +## Enabling JWT token validation +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId1}}, {{ClientId2}}, and {{TenantId}} with the values from your Azure Bots. + ```json + "TokenValidation": { + "Audiences": [ + "{{ClientId1}}", + "{{ClientId2}}" + ], + "TenantId": "{{TenantId}}" + }, + ``` + ## Further reading To learn more about building Agents, see our [Microsoft 365 Agents SDK](https://github.com/microsoft/agents) repo. \ No newline at end of file diff --git a/samples/dotnet/named-pipe-agent/NamedPipeAgent.csproj b/samples/dotnet/named-pipe-agent/NamedPipeAgent.csproj index 0931861f..75a150ff 100644 --- a/samples/dotnet/named-pipe-agent/NamedPipeAgent.csproj +++ b/samples/dotnet/named-pipe-agent/NamedPipeAgent.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/samples/dotnet/obo-authorization/AspNetExtensions.cs b/samples/dotnet/obo-authorization/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/obo-authorization/AspNetExtensions.cs +++ b/samples/dotnet/obo-authorization/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/obo-authorization/OBOAuthorization.csproj b/samples/dotnet/obo-authorization/OBOAuthorization.csproj index 21912aaa..9eccdbd2 100644 --- a/samples/dotnet/obo-authorization/OBOAuthorization.csproj +++ b/samples/dotnet/obo-authorization/OBOAuthorization.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -7,9 +7,9 @@ - - - + + + diff --git a/samples/dotnet/obo-authorization/Program.cs b/samples/dotnet/obo-authorization/Program.cs index 83ebefc1..9c5efd23 100644 --- a/samples/dotnet/obo-authorization/Program.cs +++ b/samples/dotnet/obo-authorization/Program.cs @@ -9,21 +9,21 @@ using Microsoft.Agents.Storage; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using System.Net.Http; using System.Threading.Tasks; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient(); - // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so // that state survives Agent restarts, and operates correctly // in a cluster of Agent instances. builder.Services.AddSingleton(); +// Add agent hosting defaults (AgentApplicationOptions, channel adapter, etc.). +builder.AddAgentDefaults(); + // Add the AgentApplication, which contains the logic for responding to // user messages. builder.AddAgent(sp => @@ -109,19 +109,14 @@ CopilotClient GetClient(AgentApplication app, ITurnContext turnContext) // Add AspNet token validation for Azure Bot Service and Entra. Authentication is // configured in the appsettings.json "TokenValidation" section. -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); +builder.AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); app.Run(); diff --git a/samples/dotnet/obo-authorization/README.md b/samples/dotnet/obo-authorization/README.md index 44d02248..44d6b6f8 100644 --- a/samples/dotnet/obo-authorization/README.md +++ b/samples/dotnet/obo-authorization/README.md @@ -90,13 +90,13 @@ This Agent has been created using [Microsoft 365 Agents Framework](https://githu 1. After a short period of time, the agent shows up in Microsoft Teams and Microsoft 365 Copilot. ## Enabling JWT token validation -1. By default, the AspNet token validation is disabled in order to support local debugging. -1. Enable by updating appsettings +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot. ```json "TokenValidation": { - "Enabled": true, "Audiences": [ - "{{ClientId}}" // this is the Client ID used for the Azure Bot + "{{ClientId}}" ], "TenantId": "{{TenantId}}" }, diff --git a/samples/dotnet/obo-authorization/appsettings.json b/samples/dotnet/obo-authorization/appsettings.json index 03fa5332..f1895b5e 100644 --- a/samples/dotnet/obo-authorization/appsettings.json +++ b/samples/dotnet/obo-authorization/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "{{ClientId}}" // this is the Client ID used for the Azure Bot ], diff --git a/samples/dotnet/otel/AgentOtelExtension.cs b/samples/dotnet/otel/AgentOtelExtension.cs index 13c622ba..7187a286 100644 --- a/samples/dotnet/otel/AgentOtelExtension.cs +++ b/samples/dotnet/otel/AgentOtelExtension.cs @@ -12,7 +12,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Agents.Core.Telemetry; -using System.Net.Http.Headers; using Microsoft.AspNetCore.Http; namespace Otel @@ -155,7 +154,7 @@ private static void ExtractHeadersForOTEL(System.Diagnostics.Activity activity, return; } var headerList = request.Where(h => !string.Equals(h.Key, "Authorization", StringComparison.OrdinalIgnoreCase)) - .Select(h => $"{h.Key}={string.Join(",", h.Value)}") + .Select(h => $"{h.Key}={string.Join(",", h.Value!)}") .ToArray(); if (headerList is { Length: > 0 }) diff --git a/samples/dotnet/otel/AspNetExtensions.cs b/samples/dotnet/otel/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/otel/AspNetExtensions.cs +++ b/samples/dotnet/otel/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/otel/Otel.csproj b/samples/dotnet/otel/Otel.csproj index c565cc56..45d9833c 100644 --- a/samples/dotnet/otel/Otel.csproj +++ b/samples/dotnet/otel/Otel.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -19,23 +19,23 @@ - - - - - + + + + + - - - - + + + + - - + + - + diff --git a/samples/dotnet/otel/Program.cs b/samples/dotnet/otel/Program.cs index b365c194..0d823f64 100644 --- a/samples/dotnet/otel/Program.cs +++ b/samples/dotnet/otel/Program.cs @@ -5,7 +5,6 @@ using Microsoft.Agents.Storage; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Otel; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -13,11 +12,11 @@ // Configure defaults for Aspire dashboard builder.ConfigureOtelProviders(); -builder.Services.AddHttpClient(); - // Add the AgentApplication, which contains the logic for responding to // user messages. -builder.AddAgent(); +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so @@ -25,22 +24,12 @@ // in a cluster of Agent instances. builder.Services.AddSingleton(); -// Add AspNet token validation for Azure Bot Service and Entra. Authentication is -// configured in the appsettings.json "TokenValidation" section. -builder.Services.AddControllers(); -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); - WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); app.Run(); diff --git a/samples/dotnet/otel/README.md b/samples/dotnet/otel/README.md index 0ac92a81..c77e2a80 100644 --- a/samples/dotnet/otel/README.md +++ b/samples/dotnet/otel/README.md @@ -140,6 +140,19 @@ Then set `APPLICATIONINSIGHTS_CONNECTION_STRING` to your Application Insights co - **Metrics** — `agent.routes.executed.count` and `agent.message.processing.duration` - **Logs** — welcome and message handling log records emitted by the sample +## Enabling JWT token validation +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot. + ```json + "TokenValidation": { + "Audiences": [ + "{{ClientId}}" + ], + "TenantId": "{{TenantId}}" + }, + ``` + ## Further reading - [OpenTelemetry .NET](https://opentelemetry.io/docs/languages/net/) diff --git a/samples/dotnet/otel/appsettings.json b/samples/dotnet/otel/appsettings.json index 9ba681bb..bdbafcf4 100644 --- a/samples/dotnet/otel/appsettings.json +++ b/samples/dotnet/otel/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": true, "Audiences": [ "{{ClientId}}" // this is the Client ID used for the Azure Bot ], diff --git a/samples/dotnet/proactive/AspNetExtensions.cs b/samples/dotnet/proactive/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/proactive/AspNetExtensions.cs +++ b/samples/dotnet/proactive/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/proactive/Proactive.csproj b/samples/dotnet/proactive/Proactive.csproj index d327a8a8..b135e440 100644 --- a/samples/dotnet/proactive/Proactive.csproj +++ b/samples/dotnet/proactive/Proactive.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -15,8 +15,8 @@ - - + + diff --git a/samples/dotnet/proactive/Program.cs b/samples/dotnet/proactive/Program.cs index 39a37315..96518aa2 100644 --- a/samples/dotnet/proactive/Program.cs +++ b/samples/dotnet/proactive/Program.cs @@ -10,11 +10,11 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient(); - // Add the AgentApplication, which contains the logic for responding to // user messages. -builder.AddAgent(); +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so @@ -22,22 +22,13 @@ // in a cluster of Agent instances. builder.Services.AddSingleton(); -// Add AspNet token validation for Azure Bot Service and Entra. Authentication is -// configured in the appsettings.json "TokenValidation" section. -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); - WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); // Map the endpoints for proactive messages. This is required to receive external Http // requests for AgentApplication.Proactive at /proactive. diff --git a/samples/dotnet/proactive/README.md b/samples/dotnet/proactive/README.md index 1074a6bb..7aa64248 100644 --- a/samples/dotnet/proactive/README.md +++ b/samples/dotnet/proactive/README.md @@ -117,13 +117,13 @@ This is a sample of a simple Agent that is hosted on an Asp.net core web service 1. You will see the message "This is OnContinueConversation" in the chat. This is the same code that #3 above hit. ## Enabling JWT token validation -1. By default, the AspNet token validation is disabled in order to support local debugging. -1. Enable by updating appsettings +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot. ```json "TokenValidation": { - "Enabled": false, "Audiences": [ - "{{ClientId}}" // this is the Client ID used for the Azure Bot + "{{ClientId}}" ], "TenantId": "{{TenantId}}" }, diff --git a/samples/dotnet/proactive/appsettings.json b/samples/dotnet/proactive/appsettings.json index aa5d74cd..6681e32e 100644 --- a/samples/dotnet/proactive/appsettings.json +++ b/samples/dotnet/proactive/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "{{ClientId}}" // this is the Client ID used for the Azure Bot ], diff --git a/samples/dotnet/quickstart/AspNetExtensions.cs b/samples/dotnet/quickstart/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/quickstart/AspNetExtensions.cs +++ b/samples/dotnet/quickstart/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/quickstart/Program.cs b/samples/dotnet/quickstart/Program.cs index d1e68302..c66dea61 100644 --- a/samples/dotnet/quickstart/Program.cs +++ b/samples/dotnet/quickstart/Program.cs @@ -6,15 +6,14 @@ using Microsoft.Agents.Storage; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient(); - // Add the AgentApplication, which contains the logic for responding to // user messages. -builder.AddAgent(); +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so @@ -22,21 +21,12 @@ // in a cluster of Agent instances. builder.Services.AddSingleton(); -// Add AspNet token validation for Azure Bot Service and Entra. Authentication is -// configured in the appsettings.json "TokenValidation" section. -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); - WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); app.Run(); diff --git a/samples/dotnet/quickstart/QuickStart.csproj b/samples/dotnet/quickstart/QuickStart.csproj index 64b87bf0..35bb44b4 100644 --- a/samples/dotnet/quickstart/QuickStart.csproj +++ b/samples/dotnet/quickstart/QuickStart.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,8 +8,8 @@ - - + + diff --git a/samples/dotnet/quickstart/README.md b/samples/dotnet/quickstart/README.md index ccd08a77..d37a5866 100644 --- a/samples/dotnet/quickstart/README.md +++ b/samples/dotnet/quickstart/README.md @@ -81,13 +81,13 @@ This Agent Sample is intended to introduce you the basic operation of the Micros 1. After a short period of time, the agent shows up in Microsoft Teams and Microsoft 365 Copilot. ## Enabling JWT token validation -1. By default, the AspNet token validation is disabled in order to support local debugging. -1. Enable by updating appsettings +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot. ```json "TokenValidation": { - "Enabled": true, "Audiences": [ - "{{ClientId}}" // this is the Client ID used for the Azure Bot + "{{ClientId}}" ], "TenantId": "{{TenantId}}" }, diff --git a/samples/dotnet/quickstart/appsettings.json b/samples/dotnet/quickstart/appsettings.json index bb1bd0ba..a2952d3e 100644 --- a/samples/dotnet/quickstart/appsettings.json +++ b/samples/dotnet/quickstart/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "{{ClientId}}" ], diff --git a/samples/dotnet/retrieval-agent/AspNetExtensions.cs b/samples/dotnet/retrieval-agent/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/retrieval-agent/AspNetExtensions.cs +++ b/samples/dotnet/retrieval-agent/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/retrieval-agent/Program.cs b/samples/dotnet/retrieval-agent/Program.cs index 2db764ec..c8614f69 100644 --- a/samples/dotnet/retrieval-agent/Program.cs +++ b/samples/dotnet/retrieval-agent/Program.cs @@ -6,14 +6,11 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.SemanticKernel; using RetrievalAgent; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient(); - // Register Semantic Kernel builder.Services.AddKernel(); @@ -40,7 +37,9 @@ // Add the AgentApplication, which contains the logic for responding to // user messages. -builder.AddAgent(); +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so @@ -48,22 +47,13 @@ // in a cluster of Agent instances. builder.Services.AddSingleton(); -// Add AspNet token validation for Azure Bot Service and Entra. Authentication is -// configured in the appsettings.json "TokenValidation" section. -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); - WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); app.Run(); diff --git a/samples/dotnet/retrieval-agent/README.md b/samples/dotnet/retrieval-agent/README.md index f913136e..fab69153 100644 --- a/samples/dotnet/retrieval-agent/README.md +++ b/samples/dotnet/retrieval-agent/README.md @@ -81,7 +81,6 @@ This Agent Sample is intended to introduce you to the Copilot Retrieval API Grou ```json "TokenValidation": { - "Enabled": true, "Audiences": [ "{{ClientId}}" // this is the Client ID used for the Azure Bot ], @@ -129,5 +128,18 @@ This Agent Sample is intended to introduce you to the Copilot Retrieval API Grou 4. I haven't seen a demo for the Pricing Analytics session. Can you send a mail to Adele Vance requesting for a Demo run this Friday? +## Enabling JWT token validation +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot. + ```json + "TokenValidation": { + "Audiences": [ + "{{ClientId}}" + ], + "TenantId": "{{TenantId}}" + }, + ``` + ## Further reading To learn more about building Agents, see [Microsoft 365 Agents SDK](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/). \ No newline at end of file diff --git a/samples/dotnet/retrieval-agent/RetrievalAgent.csproj b/samples/dotnet/retrieval-agent/RetrievalAgent.csproj index b70303f1..3d035180 100644 --- a/samples/dotnet/retrieval-agent/RetrievalAgent.csproj +++ b/samples/dotnet/retrieval-agent/RetrievalAgent.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -26,9 +26,21 @@ - - - + + + + + + + + + + diff --git a/samples/dotnet/retrieval-agent/appsettings.json b/samples/dotnet/retrieval-agent/appsettings.json index 15cdc831..b0ce7869 100644 --- a/samples/dotnet/retrieval-agent/appsettings.json +++ b/samples/dotnet/retrieval-agent/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "{{ClientId}}" // this is the Client ID used for the Azure Bot ], diff --git a/samples/dotnet/semantic-kernel-multiturn/AspNetExtensions.cs b/samples/dotnet/semantic-kernel-multiturn/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/semantic-kernel-multiturn/AspNetExtensions.cs +++ b/samples/dotnet/semantic-kernel-multiturn/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/semantic-kernel-multiturn/Program.cs b/samples/dotnet/semantic-kernel-multiturn/Program.cs index 57a150f6..1c4af7cc 100644 --- a/samples/dotnet/semantic-kernel-multiturn/Program.cs +++ b/samples/dotnet/semantic-kernel-multiturn/Program.cs @@ -6,14 +6,11 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.SemanticKernel; using SemanticKernelMultiturn; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient(); - // Register Semantic Kernel builder.Services.AddKernel(); @@ -40,7 +37,9 @@ // Add the AgentApplication, which contains the logic for responding to // user messages. -builder.AddAgent(); +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so @@ -48,21 +47,12 @@ // in a cluster of Agent instances. builder.Services.AddSingleton(); -// Add AspNet token validation for Azure Bot Service and Entra. Authentication is -// configured in the appsettings.json "TokenValidation" section. -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); - WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); app.Run(); diff --git a/samples/dotnet/semantic-kernel-multiturn/README.md b/samples/dotnet/semantic-kernel-multiturn/README.md index f6adde7f..1c97f707 100644 --- a/samples/dotnet/semantic-kernel-multiturn/README.md +++ b/samples/dotnet/semantic-kernel-multiturn/README.md @@ -102,13 +102,13 @@ This Agent Sample is intended to introduce you the basics of integrating Semanti 1. After a short period of time, the agent shows up in Microsoft Teams and Microsoft 365 Copilot. ## Enabling JWT token validation -1. By default, the AspNet token validation is disabled in order to support local debugging. -1. Enable by updating appsettings +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot. ```json "TokenValidation": { - "Enabled": true, "Audiences": [ - "{{ClientId}}" // this is the Client ID used for the Azure Bot + "{{ClientId}}" ], "TenantId": "{{TenantId}}" }, diff --git a/samples/dotnet/semantic-kernel-multiturn/SemanticKernelMultiturn.csproj b/samples/dotnet/semantic-kernel-multiturn/SemanticKernelMultiturn.csproj index fd715fbb..d02fa590 100644 --- a/samples/dotnet/semantic-kernel-multiturn/SemanticKernelMultiturn.csproj +++ b/samples/dotnet/semantic-kernel-multiturn/SemanticKernelMultiturn.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -22,8 +22,8 @@ - - + + diff --git a/samples/dotnet/semantic-kernel-multiturn/appsettings.json b/samples/dotnet/semantic-kernel-multiturn/appsettings.json index 76f8ed17..ad7470bb 100644 --- a/samples/dotnet/semantic-kernel-multiturn/appsettings.json +++ b/samples/dotnet/semantic-kernel-multiturn/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "{{ClientId}}" // this is the Client ID used for the Azure Bot ], diff --git a/samples/dotnet/slackagent/AspNetExtensions.cs b/samples/dotnet/slackagent/AspNetExtensions.cs index e9c0b413..8dca728e 100644 --- a/samples/dotnet/slackagent/AspNetExtensions.cs +++ b/samples/dotnet/slackagent/AspNetExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; @@ -23,6 +24,26 @@ public static class AspNetExtensions { private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + private static bool IsBotFrameworkIssuer(string issuer) + { + return AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) + || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests. + /// This overload is designed for use with AddAgentAuthorization. + /// + /// The host application builder. + /// + /// Name of the configuration section to read from. Defaults to "TokenValidation". + /// + public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation") + { + builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName); + } + /// /// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration. /// @@ -33,8 +54,7 @@ public static class AspNetExtensions /// /// /// - /// If the configuration section is absent or contains "Enabled": false, authentication is not configured and - /// all requests will be treated as unauthenticated. This is useful for local development only. + /// If the configuration section is absent, an is thrown. /// /// /// Minimum configuration for Azure Public cloud: @@ -69,12 +89,9 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + if (!tokenValidationSection.Exists()) { - // Noop if TokenValidation section missing or disabled. - System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); - services.AddControllers(); - return; + throw new ArgumentException($"Configuration section '{tokenValidationSectionName}' is missing. Token validation requires a valid configuration section."); } services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); @@ -88,7 +105,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) { AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); - services.AddControllers(); // Must have at least one Audience. if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) @@ -108,7 +124,20 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services // If ValidIssuers is empty, default for ABS Public Cloud if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) { - if (validationOptions.IsGov) + if (validationOptions.AzureBotServiceOnly) + { + // Accept only Azure Bot Service (BotFramework) tokens. Entra ID / agent-to-agent + // callers are rejected at issuer validation (ValidateIssuer) before reaching the + // AllowedCallers check. Requires AzureBotServiceTokenHandling to remain true so + // these tokens are validated against the ABS OpenID metadata. + validationOptions.ValidIssuers = + [ + validationOptions.IsGov + ? AuthenticationConstants.GovBotFrameworkTokenIssuer + : AuthenticationConstants.BotFrameworkTokenIssuer + ]; + } + else if (validationOptions.IsGov) { validationOptions.ValidIssuers = [ @@ -208,9 +237,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services string issuer = token.Issuer; if (validationOptions.AzureBotServiceTokenHandling - && (AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.GovBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase) - || AuthenticationConstants.ChinaBotFrameworkTokenIssuer.Equals(issuer, StringComparison.OrdinalIgnoreCase))) + && IsBotFrameworkIssuer(issuer)) { // Use the Azure Bot authority for this configuration manager context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key => @@ -237,6 +264,27 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services OnTokenValidated = context => { + // AllowedCallers check for non-BotFramework tokens. + // BotFramework tokens (from ABS) are excluded since they use service-level issuers. + var issuer = context.Principal?.FindFirst("iss")?.Value; + bool isBotFrameworkToken = validationOptions.AzureBotServiceTokenHandling + && issuer != null && IsBotFrameworkIssuer(issuer); + + if (!isBotFrameworkToken + && validationOptions.AllowedCallers != null + && validationOptions.AllowedCallers.Count > 0 + && !validationOptions.AllowedCallers.Any(c => c.Equals("*", StringComparison.Ordinal))) + { + // azp (v2 tokens) or appid (v1 tokens) + var callerAppId = context.Principal?.FindFirst("azp")?.Value + ?? context.Principal?.FindFirst("appid")?.Value; + + if (string.IsNullOrEmpty(callerAppId) || !validationOptions.AllowedCallers.Any(c => c.Equals(callerAppId, StringComparison.OrdinalIgnoreCase))) + { + context.Fail($"Caller App ID '{callerAppId}' is not in the AllowedCallers list."); + } + } + return Task.CompletedTask; }, OnForbidden = context => @@ -255,11 +303,6 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services /// Settings that control JWT bearer token validation for Azure Bot Service and agent-to-agent requests. /// Read from the TokenValidation configuration section by . /// - /// - /// An Enabled key may also appear in the same configuration section. When set to false, - /// authentication is disabled entirely and this class is not read. This key is not a property of - /// because it is evaluated before deserialization. - /// public class TokenValidationOptions { /// @@ -281,6 +324,7 @@ public class TokenValidationOptions /// tenant-specific issuer URLs built from /// and . /// For China or other clouds all issuers must be set explicitly since there is no corresponding IsChina flag. + /// See also to default this to just the BotFramework issuer. /// public IList? ValidIssuers { get; set; } @@ -308,6 +352,25 @@ public class TokenValidationOptions /// public bool IsGov { get; set; } = false; + /// + /// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to false. + /// When true and is not set explicitly, is + /// defaulted to just the BotFramework token issuer + /// (, or + /// when is true). + /// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the + /// check does not apply. + /// + /// Note: this targets the legacy BotFramework issuer (https://api.botframework.com). As Azure Bot + /// Service migrates channels to send Entra ID tokens (see ), those + /// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on + /// that path, set explicitly instead. Keep + /// true when using this option. For China or other sovereign clouds, set + /// explicitly since there is no corresponding flag. + /// + /// + public bool AzureBotServiceOnly { get; set; } = false; + /// /// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional. /// When omitted, defaults to when @@ -338,5 +401,14 @@ public class TokenValidationOptions /// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours. /// public TimeSpan? OpenIdMetadataRefresh { get; set; } + + /// + /// List of Application IDs (Client IDs) that are allowed to call this agent. Optional. + /// When empty or containing "*", any caller is accepted. + /// When populated with specific App IDs, the azp or appid claim in the inbound token + /// must match one of the listed values. This check applies only to non-BotFramework tokens. + /// To accept only Azure Bot Service (BotFramework) traffic, use . + /// + public IList? AllowedCallers { get; set; } } } diff --git a/samples/dotnet/slackagent/Program.cs b/samples/dotnet/slackagent/Program.cs index 6e404f18..9fb4355f 100644 --- a/samples/dotnet/slackagent/Program.cs +++ b/samples/dotnet/slackagent/Program.cs @@ -5,19 +5,15 @@ using Microsoft.Agents.Storage; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using SlackAgent; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient(); - -// Add AgentApplicationOptions from appsettings section "AgentApplication". -builder.AddAgentApplicationOptions(); - // Add the AgentApplication, which contains the logic for responding to // user messages. -builder.AddAgent(); +builder.AddAgentDefaults() + .AddAgent() + .AddAgentAuthorization(b => b.AddAgentAspNetAuthentication()); // Register IStorage. For development, MemoryStorage is suitable. // For production Agents, persisted storage should be used so @@ -25,24 +21,12 @@ // in a cluster of Agent instances. builder.Services.AddSingleton(); -// Configure the HTTP request pipeline. - -// Add AspNet token validation for Azure Bot Service and Entra. Authentication is -// configured in the appsettings.json "TokenValidation" section. -builder.Services.AddControllers(); -builder.Services.AddAgentAspNetAuthentication(builder.Configuration); - WebApplication app = builder.Build(); -// Enable AspNet authentication and authorization -app.UseAuthentication(); -app.UseAuthorization(); - -// Map GET "/" -app.MapAgentRootEndpoint(); +// Add the authentication and authorization middleware to the request pipeline. +app.UseAgents(); -// Map the endpoints for all agents using the [AgentInterface] attribute. -// If there is a single IAgent/AgentApplication, the endpoints will be mapped to (e.g. "/api/message"). -app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment()); +// Map the default agent endpoints: GET "/" and the agent message endpoints. +app.MapDefaultAgentEndpoints(); app.Run(); diff --git a/samples/dotnet/slackagent/README.md b/samples/dotnet/slackagent/README.md index ba807035..7dd1763d 100644 --- a/samples/dotnet/slackagent/README.md +++ b/samples/dotnet/slackagent/README.md @@ -41,13 +41,13 @@ This is a sample of a simple slack Agent that is hosted on an Asp.net core web s 1. Follow the instructions in this doc to create a Slack App and connect it to your Agent: https://learn.microsoft.com/en-us/azure/bot-service/bot-service-channel-connect-slack?view=azure-bot-service-4.0 ## Enabling JWT token validation -1. By default, the AspNet token validation is disabled in order to support local debugging. -1. Enable by updating appsettings +1. By default, token validation is disabled in Development mode. This is determined by `AddAgentAuthorization` and the `forceEnable` argument. + +1. Updating appsettings and replace {{ClientId}} and {{TenantId}} with the values from your Azure Bot. ```json "TokenValidation": { - "Enabled": true, "Audiences": [ - "{{ClientId}}" // this is the Client ID used for the Azure Bot + "{{ClientId}}" ], "TenantId": "{{TenantId}}" }, diff --git a/samples/dotnet/slackagent/SlackAgent.csproj b/samples/dotnet/slackagent/SlackAgent.csproj index 0ba610da..7ac712a0 100644 --- a/samples/dotnet/slackagent/SlackAgent.csproj +++ b/samples/dotnet/slackagent/SlackAgent.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,9 +8,9 @@ - - - + + + diff --git a/samples/dotnet/slackagent/appsettings.json b/samples/dotnet/slackagent/appsettings.json index 316f5bad..06273ad6 100644 --- a/samples/dotnet/slackagent/appsettings.json +++ b/samples/dotnet/slackagent/appsettings.json @@ -1,6 +1,5 @@ { "TokenValidation": { - "Enabled": false, "Audiences": [ "{{ClientId}}" // this is the Client ID used for the Azure Bot ],