Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand Down Expand Up @@ -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**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ builder.AddAgent<MyBot, CustomAdapter>();

## 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).

---

Expand Down
12 changes: 6 additions & 6 deletions samples/dotnet/Agent Framework/AgentFrameworkWeather.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
Expand All @@ -13,14 +13,14 @@

<ItemGroup>
<!-- Agents SDK Pacakges -->
<PackageReference Include="Microsoft.Agents.Authentication.Msal" Version="1.6.*" />
<PackageReference Include="Microsoft.Agents.Hosting.AspNetCore" Version="1.6.*" />
<PackageReference Include="Microsoft.Agents.Authentication.Msal" Version="1.7.*" />
<PackageReference Include="Microsoft.Agents.Hosting.AspNetCore" Version="1.7.*" />

<!-- Agent Framework Packages -->
<PackageReference Include="AdaptiveCards" Version="3.1.0" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.7.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.10.0" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.12.0" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI"
Version="10.7.0" />
<PackageReference Include="Microsoft.Extensions.AI" Version="10.7.0" />
Expand All @@ -31,8 +31,8 @@
<!-- Open Telemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.16.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.16.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.16.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />

<!-- OpenTelemetry Exporters -->
Expand Down
106 changes: 89 additions & 17 deletions samples/dotnet/Agent Framework/AspNetExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,6 +24,26 @@ public static class AspNetExtensions
{
private static readonly ConcurrentDictionary<string, ConfigurationManager<OpenIdConnectConfiguration>> _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);
}

/// <summary>
/// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests.
/// This overload is designed for use with <c>AddAgentAuthorization</c>.
/// </summary>
/// <param name="builder">The host application builder.</param>
/// <param name="tokenValidationSectionName">
/// Name of the configuration section to read <see cref="TokenValidationOptions"/> from. Defaults to <c>"TokenValidation"</c>.
/// </param>
public static void AddAgentAspNetAuthentication(this IHostApplicationBuilder builder, string tokenValidationSectionName = "TokenValidation")
{
builder.Services.AddAgentAspNetAuthentication(builder.Configuration, tokenValidationSectionName);
}

/// <summary>
/// Adds JWT bearer token validation for Azure Bot Service and agent-to-agent requests, reading settings from configuration.
/// </summary>
Expand All @@ -33,8 +54,7 @@ public static class AspNetExtensions
/// </param>
/// <remarks>
/// <para>
/// If the configuration section is absent or contains <c>"Enabled": false</c>, 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 <see cref="ArgumentException"/> is thrown.
/// </para>
/// <para>
/// Minimum configuration for Azure Public cloud:
Expand Down Expand Up @@ -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<TokenValidationOptions>()!);
Expand All @@ -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)
Expand All @@ -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 =
[
Expand Down Expand Up @@ -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 =>
Expand All @@ -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 =>
Expand All @@ -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 <c>TokenValidation</c> configuration section by <see cref="AddAgentAspNetAuthentication(IServiceCollection, IConfiguration, string)"/>.
/// </summary>
/// <remarks>
/// An <c>Enabled</c> key may also appear in the same configuration section. When set to <c>false</c>,
/// authentication is disabled entirely and this class is not read. This key is not a property of
/// <see cref="TokenValidationOptions"/> because it is evaluated before deserialization.
/// </remarks>
public class TokenValidationOptions
{
/// <summary>
Expand All @@ -281,6 +324,7 @@ public class TokenValidationOptions
/// tenant-specific issuer URLs built from <see cref="AuthenticationConstants.ValidTokenIssuerUrlTemplateV1"/>
/// and <see cref="AuthenticationConstants.ValidGovernmentTokenIssuerUrlTemplateV2"/>.
/// For China or other clouds all issuers must be set explicitly since there is no corresponding <c>IsChina</c> flag.
/// See also <see cref="AzureBotServiceOnly"/> to default this to just the BotFramework issuer.
/// </summary>
public IList<string>? ValidIssuers { get; set; }

Expand Down Expand Up @@ -308,6 +352,25 @@ public class TokenValidationOptions
/// </summary>
public bool IsGov { get; set; } = false;

/// <summary>
/// Restrict the agent to accept only Azure Bot Service (BotFramework) traffic. Defaults to <c>false</c>.
/// When <c>true</c> and <see cref="ValidIssuers"/> is not set explicitly, <see cref="ValidIssuers"/> is
/// defaulted to just the BotFramework token issuer
/// (<see cref="AuthenticationConstants.BotFrameworkTokenIssuer"/>, or
/// <see cref="AuthenticationConstants.GovBotFrameworkTokenIssuer"/> when <see cref="IsGov"/> is <c>true</c>).
/// Entra ID and agent-to-agent callers are then rejected at issuer validation, so the
/// <see cref="AllowedCallers"/> check does not apply.
/// <para>
/// Note: this targets the legacy BotFramework issuer (<c>https://api.botframework.com</c>). As Azure Bot
/// Service migrates channels to send Entra ID tokens (see <see cref="AzureBotServiceTokenHandling"/>), those
/// tokens are issued by the Bot Service Entra tenants rather than the BotFramework issuer; if you rely on
/// that path, set <see cref="ValidIssuers"/> explicitly instead. Keep <see cref="AzureBotServiceTokenHandling"/>
/// <c>true</c> when using this option. For China or other sovereign clouds, set <see cref="ValidIssuers"/>
/// explicitly since there is no corresponding flag.
/// </para>
/// </summary>
public bool AzureBotServiceOnly { get; set; } = false;

/// <summary>
/// OpenID Connect metadata URL used to validate tokens issued by Azure Bot Service. Optional.
/// When omitted, defaults to <see cref="AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl"/> when
Expand Down Expand Up @@ -338,5 +401,14 @@ public class TokenValidationOptions
/// How frequently the OpenID Connect metadata is refreshed from the identity provider. Defaults to 12 hours.
/// </summary>
public TimeSpan? OpenIdMetadataRefresh { get; set; }

/// <summary>
/// List of Application IDs (Client IDs) that are allowed to call this agent. Optional.
/// When empty or containing <c>"*"</c>, any caller is accepted.
/// When populated with specific App IDs, the <c>azp</c> or <c>appid</c> 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 <see cref="AzureBotServiceOnly"/>.
/// </summary>
public IList<string>? AllowedCallers { get; set; }
}
}
Loading
Loading