From d1ea1002b8f3bb0b1ff197bb58450a65881943ce Mon Sep 17 00:00:00 2001 From: rido-min Date: Thu, 27 Feb 2025 11:23:10 -0800 Subject: [PATCH 1/8] ignore launchSettings.json --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d8c7fbe..29cd3b3 100644 --- a/.gitignore +++ b/.gitignore @@ -395,3 +395,4 @@ dist/ devTools/ node_modules/ +launchSettings.json \ No newline at end of file From f2cd3dd282b9f75e32a7cd8bf61e77914a99edb5 Mon Sep 17 00:00:00 2001 From: rido-min Date: Thu, 27 Feb 2025 11:26:04 -0800 Subject: [PATCH 2/8] start echo-bot from net repo --- samples/Samples.sln | 25 ++ .../basic/echo-bot/dotnet/AspNetExtensions.cs | 214 ++++++++++++++++++ .../basic/echo-bot/dotnet/BotController.cs | 25 ++ samples/basic/echo-bot/dotnet/EchoBot.csproj | 14 ++ samples/basic/echo-bot/dotnet/MyBot.cs | 36 +++ samples/basic/echo-bot/dotnet/Program.cs | 34 +++ .../Properties/launchSettings.TEMPLATE.json | 18 ++ .../dotnet/appsettings.Development.json | 8 + .../basic/echo-bot/dotnet/appsettings.json | 35 +++ samples/nuget.config | 8 + 10 files changed, 417 insertions(+) create mode 100644 samples/Samples.sln create mode 100644 samples/basic/echo-bot/dotnet/AspNetExtensions.cs create mode 100644 samples/basic/echo-bot/dotnet/BotController.cs create mode 100644 samples/basic/echo-bot/dotnet/EchoBot.csproj create mode 100644 samples/basic/echo-bot/dotnet/MyBot.cs create mode 100644 samples/basic/echo-bot/dotnet/Program.cs create mode 100644 samples/basic/echo-bot/dotnet/Properties/launchSettings.TEMPLATE.json create mode 100644 samples/basic/echo-bot/dotnet/appsettings.Development.json create mode 100644 samples/basic/echo-bot/dotnet/appsettings.json create mode 100644 samples/nuget.config diff --git a/samples/Samples.sln b/samples/Samples.sln new file mode 100644 index 0000000..b946818 --- /dev/null +++ b/samples/Samples.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35818.85 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoBot", "basic\echo-bot\dotnet\EchoBot.csproj", "{B1FC20C1-9900-FAC4-B8E6-223888633020}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B1FC20C1-9900-FAC4-B8E6-223888633020}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1FC20C1-9900-FAC4-B8E6-223888633020}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1FC20C1-9900-FAC4-B8E6-223888633020}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1FC20C1-9900-FAC4-B8E6-223888633020}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {46F8E711-A447-4AB5-8357-03A5B5D21B49} + EndGlobalSection +EndGlobal diff --git a/samples/basic/echo-bot/dotnet/AspNetExtensions.cs b/samples/basic/echo-bot/dotnet/AspNetExtensions.cs new file mode 100644 index 0000000..0af9cb6 --- /dev/null +++ b/samples/basic/echo-bot/dotnet/AspNetExtensions.cs @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Agents.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Protocols; +using System.Collections.Concurrent; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Validators; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Agents.Samples +{ + public static class AspNetExtensions + { + private static readonly ConcurrentDictionary> _openIdMetadataCache = new(); + + /// + /// Adds token validation typical for ABS/SMBA and Bot-to-bot. + /// default to Azure Public Cloud. + /// + /// + /// + /// Name of the config section to read. + /// Optional logger to use for authentication event logging. + /// + /// Configuration: + /// + /// "TokenValidation": { + /// "Audiences": [ + /// "{required:bot-appid}" + /// ], + /// "TenantId": "{recommended:tenant-id}", + /// "ValidIssuers": [ + /// "{default:Public-AzureBotService}" + /// ], + /// "IsGov": {optional:false}, + /// "AzureBotServiceOpenIdMetadataUrl": optional, + /// "OpenIdMetadataUrl": optional, + /// "AzureBotServiceTokenHandling": "{optional:true}" + /// "OpenIdMetadataRefresh": "optional-12:00:00" + /// } + /// + /// + /// `IsGov` can be omitted, in which case public Azure Bot Service and Azure Cloud metadata urls are used. + /// `ValidIssuers` can be omitted, in which case the Public Azure Bot Service issuers are used. + /// `TenantId` can be omitted if the Agent is not being called by another Agent. Otherwise it is used to add other known issuers. Only when `ValidIssuers` is omitted. + /// `AzureBotServiceOpenIdMetadataUrl` can be omitted. In which case default values in combination with `IsGov` is used. + /// `OpenIdMetadataUrl` can be omitted. In which case default values in combination with `IsGov` is used. + /// `AzureBotServiceTokenHandling` defaults to true and should always be true until Azure Bot Service sends Entra ID token. + /// + public static void AddBotAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation", ILogger logger = null) + { + IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); + List validTokenIssuers = tokenValidationSection.GetSection("ValidIssuers").Get>(); + List audiences = tokenValidationSection.GetSection("Audiences").Get>(); + + if (!tokenValidationSection.Exists()) + { + logger?.LogError($"Missing configuration section '{tokenValidationSectionName}'. This section is required to be present in appsettings.json"); + throw new InvalidOperationException($"Missing configuration section '{tokenValidationSectionName}'. This section is required to be present in appsettings.json"); + } + + // If ValidIssuers is empty, default for ABS Public Cloud + if (validTokenIssuers == null || validTokenIssuers.Count == 0) + { + validTokenIssuers = + [ + "https://api.botframework.com", + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", + "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", + "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", + ]; + + string tenantId = tokenValidationSection["TenantId"]; + if (!string.IsNullOrEmpty(tenantId)) + { + validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, tenantId)); + validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, tenantId)); + } + } + + if (audiences == null || audiences.Count == 0) + { + throw new ArgumentException($"{tokenValidationSectionName}:Audiences requires at least one value"); + } + + bool isGov = tokenValidationSection.GetValue("IsGov", false); + var azureBotServiceTokenHandling = tokenValidationSection.GetValue("AzureBotServiceTokenHandling", true); + + // If the `AzureBotServiceOpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate ABS tokens. + var azureBotServiceOpenIdMetadataUrl = tokenValidationSection["AzureBotServiceOpenIdMetadataUrl"]; + if (string.IsNullOrEmpty(azureBotServiceOpenIdMetadataUrl)) + { + azureBotServiceOpenIdMetadataUrl = isGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl; + } + + // If the `OpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate Entra ID tokens. + var openIdMetadataUrl = tokenValidationSection["OpenIdMetadataUrl"]; + if (string.IsNullOrEmpty(openIdMetadataUrl)) + { + openIdMetadataUrl = isGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl; + } + + var openIdRefreshInterval = tokenValidationSection.GetValue("OpenIdMetadataRefresh", BaseConfigurationManager.DefaultAutomaticRefreshInterval); + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + ValidIssuers = validTokenIssuers, + ValidAudiences = audiences, + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + }; + + // Using Microsoft.IdentityModel.Validators + options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); + + options.Events = new JwtBearerEvents + { + // Create a ConfigurationManager based on the requestor. This is to handle ABS non-Entra tokens. + OnMessageReceived = async context => + { + var authorizationHeader = context.Request.Headers.Authorization.ToString(); + + if (string.IsNullOrEmpty(authorizationHeader)) + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + await Task.CompletedTask.ConfigureAwait(false); + return; + } + + string[] parts = authorizationHeader?.Split(' '); + if (parts.Length != 2 || parts[0] != "Bearer") + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + await Task.CompletedTask.ConfigureAwait(false); + return; + } + + JwtSecurityToken token = new(parts[1]); + var issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value; + + if (azureBotServiceTokenHandling && AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer)) + { + // Use the Bot Framework authority for this configuration manager + context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(azureBotServiceOpenIdMetadataUrl, key => + { + return new ConfigurationManager(azureBotServiceOpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) + { + AutomaticRefreshInterval = openIdRefreshInterval + }; + }); + } + else + { + context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(openIdMetadataUrl, key => + { + return new ConfigurationManager(openIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) + { + AutomaticRefreshInterval = openIdRefreshInterval + }; + }); + } + + await Task.CompletedTask.ConfigureAwait(false); + }, + + OnTokenValidated = context => + { + logger?.LogDebug("TOKEN Validated"); + return Task.CompletedTask; + }, + OnForbidden = context => + { + logger?.LogWarning(context.Result.ToString()); + return Task.CompletedTask; + }, + OnAuthenticationFailed = context => + { + logger?.LogWarning(context.Exception.ToString()); + return Task.CompletedTask; + } + }; + }); + } + } +} diff --git a/samples/basic/echo-bot/dotnet/BotController.cs b/samples/basic/echo-bot/dotnet/BotController.cs new file mode 100644 index 0000000..70a1ebb --- /dev/null +++ b/samples/basic/echo-bot/dotnet/BotController.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Agents.Hosting.AspNetCore; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.BotBuilder; + +namespace EchoBot +{ + // ASP.Net Controller that receives incoming HTTP requests from the Azure Bot Service or other configured event activity protocol sources. + // When called, the request has already been authorized and credentials and tokens validated. + [Authorize] + [ApiController] + [Route("api/messages")] + public class BotController(IBotHttpAdapter adapter, IBot bot) : ControllerBase + { + [HttpPost] + public Task PostAsync(CancellationToken cancellationToken) + => adapter.ProcessAsync(Request, Response, bot, cancellationToken); + + } +} diff --git a/samples/basic/echo-bot/dotnet/EchoBot.csproj b/samples/basic/echo-bot/dotnet/EchoBot.csproj new file mode 100644 index 0000000..6dc2e61 --- /dev/null +++ b/samples/basic/echo-bot/dotnet/EchoBot.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/samples/basic/echo-bot/dotnet/MyBot.cs b/samples/basic/echo-bot/dotnet/MyBot.cs new file mode 100644 index 0000000..2441976 --- /dev/null +++ b/samples/basic/echo-bot/dotnet/MyBot.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.BotBuilder; +using Microsoft.Agents.Core.Interfaces; +using Microsoft.Agents.Core.Models; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace EchoBot +{ + // This is the core handler for the Bot Message loop. Each new request will be processed by this class. + public class MyBot : ActivityHandler + { + protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + // Create a new Activity from the message the user provided and modify the text to echo back. + IActivity message = MessageFactory.Text($"Echo: {turnContext.Activity.Text}"); + + // Send the response message back to the user. + await turnContext.SendActivityAsync(message, cancellationToken); + } + + protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) + { + // When someone (or something) connects to the bot, a MembersAdded activity is received. + // For this sample, we treat this as a welcome event, and send a message saying hello. + // For more details around the membership lifecycle, please see the lifecycle documentation. + IActivity message = MessageFactory.Text("Hello and Welcome!"); + + // Send the response message back to the user. + await turnContext.SendActivityAsync(message, cancellationToken); + } + } +} \ No newline at end of file diff --git a/samples/basic/echo-bot/dotnet/Program.cs b/samples/basic/echo-bot/dotnet/Program.cs new file mode 100644 index 0000000..d241d65 --- /dev/null +++ b/samples/basic/echo-bot/dotnet/Program.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using EchoBot; +using Microsoft.Agents.Hosting.AspNetCore; +using Microsoft.Agents.Samples; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddHttpClient(); + +// Add AspNet token validation +builder.Services.AddBotAspNetAuthentication(builder.Configuration); + +// Add basic bot functionality +builder.AddBot(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapGet("/", () => "Microsoft Copilot SDK Sample"); + app.UseDeveloperExceptionPage(); + app.MapControllers().AllowAnonymous(); +} +else +{ + app.MapControllers(); +} +app.Run(); \ No newline at end of file diff --git a/samples/basic/echo-bot/dotnet/Properties/launchSettings.TEMPLATE.json b/samples/basic/echo-bot/dotnet/Properties/launchSettings.TEMPLATE.json new file mode 100644 index 0000000..7490a9b --- /dev/null +++ b/samples/basic/echo-bot/dotnet/Properties/launchSettings.TEMPLATE.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:3978", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "Connections__BotServiceConnection__Settings__TenantId": "<>", + "Connections__BotServiceConnection__Settings__ClientId": "<>", + "Connections__BotServiceConnection__Settings__AuthorityEndpoint": "https://login.microsoftonline.com/<>", + "Connections__BotServiceConnection__Settings__ClientSecret": "<>" + } + } + } +} diff --git a/samples/basic/echo-bot/dotnet/appsettings.Development.json b/samples/basic/echo-bot/dotnet/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/basic/echo-bot/dotnet/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/basic/echo-bot/dotnet/appsettings.json b/samples/basic/echo-bot/dotnet/appsettings.json new file mode 100644 index 0000000..504969e --- /dev/null +++ b/samples/basic/echo-bot/dotnet/appsettings.json @@ -0,0 +1,35 @@ +{ + "TokenValidation": { + "Audiences": [ + "00000000-0000-0000-0000-000000000000" // this is the Client ID used for the Azure Bot + ] + }, + + "Connections": { + "BotServiceConnection": { + "Settings": { + "AuthType": "ClientSecret", // this is the AuthType for the connection, valid values can be found in Microsoft.Agents.Authentication.Msal.Model.AuthTypes. The default is ClientSecret. + "AuthorityEndpoint": "https://login.microsoftonline.com/{{TenantId}}", + "ClientId": "00000000-0000-0000-0000-000000000000", // this is the Client ID used for the connection. + "ClientSecret": "00000000-0000-0000-0000-000000000000", // this is the Client Secret used for the connection. + "Scopes": [ + "https://api.botframework.com/.default" + ] + } + } + }, + "ConnectionsMap": [ + { + "ServiceUrl": "*", + "Connection": "BotServiceConnection" + } + ], + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} \ No newline at end of file diff --git a/samples/nuget.config b/samples/nuget.config new file mode 100644 index 0000000..b31b213 --- /dev/null +++ b/samples/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + From 3519266537f2e17e487aa9b049799340e408ecdc Mon Sep 17 00:00:00 2001 From: rido-min Date: Thu, 27 Feb 2025 11:27:33 -0800 Subject: [PATCH 3/8] Add CI workflow for building .NET samples --- .github/workflows/ci-dotnet.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/ci-dotnet.yml diff --git a/.github/workflows/ci-dotnet.yml b/.github/workflows/ci-dotnet.yml new file mode 100644 index 0000000..b4ddc6d --- /dev/null +++ b/.github/workflows/ci-dotnet.yml @@ -0,0 +1,18 @@ +name: ci-dotnet + +on: + workflow_dispatch: + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build dotnet samples + run: dotnet build samples/Samples.sln \ No newline at end of file From ef2a04d6d62c5945435f38242958e258fff0f43c Mon Sep 17 00:00:00 2001 From: Rido Date: Thu, 27 Feb 2025 19:35:52 +0000 Subject: [PATCH 4/8] add upstream --- samples/nuget.config | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/nuget.config b/samples/nuget.config index b31b213..a431f92 100644 --- a/samples/nuget.config +++ b/samples/nuget.config @@ -3,6 +3,7 @@ + From f6bb2c01995a48cf536036ad09314b1cf2b621f0 Mon Sep 17 00:00:00 2001 From: Rido Date: Thu, 27 Feb 2025 19:37:07 +0000 Subject: [PATCH 5/8] fix warnings in extensions --- samples/basic/echo-bot/dotnet/AspNetExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/basic/echo-bot/dotnet/AspNetExtensions.cs b/samples/basic/echo-bot/dotnet/AspNetExtensions.cs index 0af9cb6..b9b5396 100644 --- a/samples/basic/echo-bot/dotnet/AspNetExtensions.cs +++ b/samples/basic/echo-bot/dotnet/AspNetExtensions.cs @@ -59,11 +59,11 @@ public static class AspNetExtensions /// `OpenIdMetadataUrl` can be omitted. In which case default values in combination with `IsGov` is used. /// `AzureBotServiceTokenHandling` defaults to true and should always be true until Azure Bot Service sends Entra ID token. /// - public static void AddBotAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation", ILogger logger = null) + public static void AddBotAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation", ILogger logger = null!) { IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); - List validTokenIssuers = tokenValidationSection.GetSection("ValidIssuers").Get>(); - List audiences = tokenValidationSection.GetSection("Audiences").Get>(); + List validTokenIssuers = tokenValidationSection.GetSection("ValidIssuers").Get>()!; + List audiences = tokenValidationSection.GetSection("Audiences").Get>()!; if (!tokenValidationSection.Exists()) { @@ -85,7 +85,7 @@ public static void AddBotAspNetAuthentication(this IServiceCollection services, "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", ]; - string tenantId = tokenValidationSection["TenantId"]; + string tenantId = tokenValidationSection["TenantId"]!; if (!string.IsNullOrEmpty(tenantId)) { validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, tenantId)); @@ -155,7 +155,7 @@ public static void AddBotAspNetAuthentication(this IServiceCollection services, return; } - string[] parts = authorizationHeader?.Split(' '); + string[] parts = authorizationHeader?.Split(' ')!; if (parts.Length != 2 || parts[0] != "Bearer") { // Default to AadTokenValidation handling From 59398d6425b230a171af20735d67089e22778581 Mon Sep 17 00:00:00 2001 From: rido-min Date: Thu, 27 Feb 2025 11:51:02 -0800 Subject: [PATCH 6/8] run code clean --- .../basic/echo-bot/dotnet/AspNetExtensions.cs | 30 +++++++------------ .../basic/echo-bot/dotnet/BotController.cs | 6 ++-- samples/basic/echo-bot/dotnet/MyBot.cs | 3 -- samples/basic/echo-bot/dotnet/Program.cs | 7 ++--- 4 files changed, 15 insertions(+), 31 deletions(-) diff --git a/samples/basic/echo-bot/dotnet/AspNetExtensions.cs b/samples/basic/echo-bot/dotnet/AspNetExtensions.cs index b9b5396..9d27d68 100644 --- a/samples/basic/echo-bot/dotnet/AspNetExtensions.cs +++ b/samples/basic/echo-bot/dotnet/AspNetExtensions.cs @@ -1,23 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Agents.Authentication; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.IdentityModel.Tokens; -using System; -using System.Collections.Generic; -using System.Globalization; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Protocols; -using System.Collections.Concurrent; -using System.Net.Http; -using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Validators; +using System.Collections.Concurrent; +using System.Globalization; using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using Microsoft.Extensions.Logging; namespace Microsoft.Agents.Samples { @@ -99,23 +91,23 @@ public static void AddBotAspNetAuthentication(this IServiceCollection services, } bool isGov = tokenValidationSection.GetValue("IsGov", false); - var azureBotServiceTokenHandling = tokenValidationSection.GetValue("AzureBotServiceTokenHandling", true); + bool azureBotServiceTokenHandling = tokenValidationSection.GetValue("AzureBotServiceTokenHandling", true); // If the `AzureBotServiceOpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate ABS tokens. - var azureBotServiceOpenIdMetadataUrl = tokenValidationSection["AzureBotServiceOpenIdMetadataUrl"]; + string? azureBotServiceOpenIdMetadataUrl = tokenValidationSection["AzureBotServiceOpenIdMetadataUrl"]; if (string.IsNullOrEmpty(azureBotServiceOpenIdMetadataUrl)) { azureBotServiceOpenIdMetadataUrl = isGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl; } // If the `OpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate Entra ID tokens. - var openIdMetadataUrl = tokenValidationSection["OpenIdMetadataUrl"]; + string? openIdMetadataUrl = tokenValidationSection["OpenIdMetadataUrl"]; if (string.IsNullOrEmpty(openIdMetadataUrl)) { openIdMetadataUrl = isGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl; } - var openIdRefreshInterval = tokenValidationSection.GetValue("OpenIdMetadataRefresh", BaseConfigurationManager.DefaultAutomaticRefreshInterval); + TimeSpan openIdRefreshInterval = tokenValidationSection.GetValue("OpenIdMetadataRefresh", BaseConfigurationManager.DefaultAutomaticRefreshInterval); services.AddAuthentication(options => { @@ -145,7 +137,7 @@ public static void AddBotAspNetAuthentication(this IServiceCollection services, // Create a ConfigurationManager based on the requestor. This is to handle ABS non-Entra tokens. OnMessageReceived = async context => { - var authorizationHeader = context.Request.Headers.Authorization.ToString(); + string authorizationHeader = context.Request.Headers.Authorization.ToString(); if (string.IsNullOrEmpty(authorizationHeader)) { @@ -165,7 +157,7 @@ public static void AddBotAspNetAuthentication(this IServiceCollection services, } JwtSecurityToken token = new(parts[1]); - var issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value; + string? issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value; if (azureBotServiceTokenHandling && AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer)) { diff --git a/samples/basic/echo-bot/dotnet/BotController.cs b/samples/basic/echo-bot/dotnet/BotController.cs index 70a1ebb..ae2de87 100644 --- a/samples/basic/echo-bot/dotnet/BotController.cs +++ b/samples/basic/echo-bot/dotnet/BotController.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.Agents.BotBuilder; +using Microsoft.Agents.Hosting.AspNetCore; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Agents.Hosting.AspNetCore; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.BotBuilder; namespace EchoBot { diff --git a/samples/basic/echo-bot/dotnet/MyBot.cs b/samples/basic/echo-bot/dotnet/MyBot.cs index 2441976..626b93f 100644 --- a/samples/basic/echo-bot/dotnet/MyBot.cs +++ b/samples/basic/echo-bot/dotnet/MyBot.cs @@ -4,9 +4,6 @@ using Microsoft.Agents.BotBuilder; using Microsoft.Agents.Core.Interfaces; using Microsoft.Agents.Core.Models; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; namespace EchoBot { diff --git a/samples/basic/echo-bot/dotnet/Program.cs b/samples/basic/echo-bot/dotnet/Program.cs index d241d65..9a0ec58 100644 --- a/samples/basic/echo-bot/dotnet/Program.cs +++ b/samples/basic/echo-bot/dotnet/Program.cs @@ -4,11 +4,8 @@ using EchoBot; using Microsoft.Agents.Hosting.AspNetCore; using Microsoft.Agents.Samples; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -var builder = WebApplication.CreateBuilder(args); +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddHttpClient(); @@ -19,7 +16,7 @@ // Add basic bot functionality builder.AddBot(); -var app = builder.Build(); +WebApplication app = builder.Build(); if (app.Environment.IsDevelopment()) { From ef91f3a3df1e64cfc2f368fa83deca6cced56485 Mon Sep 17 00:00:00 2001 From: Rido Date: Mon, 3 Mar 2025 16:52:15 +0000 Subject: [PATCH 7/8] Update README with debugging instructions and remove obsolete nuget.config files --- samples/README.md | 29 +++++++-- samples/_tools/configure-abs-tunnel.sh | 60 +++++++++++++++++++ .../{ => basic/echo-bot/dotnet}/nuget.config | 0 3 files changed, 83 insertions(+), 6 deletions(-) create mode 100755 samples/_tools/configure-abs-tunnel.sh rename samples/{ => basic/echo-bot/dotnet}/nuget.config (100%) diff --git a/samples/README.md b/samples/README.md index 96de2f3..b38f7f1 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,16 +1,33 @@ # M365 Agents SDK Samples -To enable debugging, open this folder using Visual Studio Code (VSCode). Follow these steps: +Here you can find samples showing how to use the Agents SDK in different languages. -1. Launch VSCode. -2. Select `File` > `Open Folder...`. -3. Navigate to the `/workspaces/Agents/samples` directory and open it. -4. Once the folder is open, you can start debugging by setting breakpoints and running the samples. ## Samples list |Category | Name | Description | node | dotnet | python | |---------|-------------|-------------|--------|--------|--------| -| Basic | Echo Bot | Simplest bot | [basic/echo-bot/nodejs](./basic/echo-bot/nodejs) | TBD | TBD | +| Basic | Echo Bot | Simplest bot | [basic/echo-bot/nodejs](./basic/echo-bot/nodejs) | [basic/echo-bot/dotnet](./basic/echo-bot/dotnet) | TBD | | Basic | Copilot Studio Client | Consume CopilotStudio Agent | [basic/copilotstudio-client/nodejs](./basic/copilotstudio-client/nodejs) | TBD | TBD | | Complex | Copilot Studio Skill | Call the echo bot from a Copilot Studio skill | [complex/copilotstudio-skill/nodejs](./complex/copilotstudio-skill/nodejs) | TBD | TBD | + + +## Debugging in localhost + +To debug your Agent in `localhost` you can: + +1. Use a client emulator, such as the [Teams App Test Tool](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/debug-your-teams-app-test-tool), or the [BotFramework Emulator](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-debug-emulator) +1. Use the WebChat UI, by registering one instance of Azure Bot Service, and configure the endpoint to use a tunnel to localhost (eg, by using devtunnels) + +### How to register an ABS instance with DevTunnels + +1. Install devtunnels and the Azure CLI + 1. This repo contains a CodeSpaces configuration with all the required tools already installed. +1. Login into devtunnels: `devtunnel user login`. +1. Login into Azure: `az login`. +1. Run the script `_tools/configure-abs-tunnel.sh`. + 1. This script configures devtunnels and ABS, using the machine name, and produces a `.env` file. +1. Copy the generated `.env` file to the sample folder with file name `.env`. + +> [!Tip] +> You can use the same ABS instance and tunnel to debug any of these samples, with the only caveat that you can only use one at a time. \ No newline at end of file diff --git a/samples/_tools/configure-abs-tunnel.sh b/samples/_tools/configure-abs-tunnel.sh new file mode 100755 index 0000000..0d6786e --- /dev/null +++ b/samples/_tools/configure-abs-tunnel.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +resource_group=$HOSTNAME-dev +tunnel_id=$HOSTNAME-tunnel + +if ! az account show &> /dev/null; then + echo "Please login to Azure CLI." + az login +fi + +if ! devtunnel list &> /dev/null; then + echo "Please login to devtunnel." + devtunnel user login +fi + +if ! devtunnel show $tunnel_id &> /dev/null; then + echo "Creating tunnel: $tunnel_id in port 3978" + devtunnel create $tunnel_id -a + devtunnel port create $tunnel_id -p 3978 +fi + +if ! az group show --name $resource_group &> /dev/null; then + echo "Creating resource group: $resource_group" + az group create --name $resource_group --location eastus +fi + +tunnel_details=$(devtunnel show $tunnel_id -j -v) +tunnel_url=$(echo $tunnel_details | grep -oP '(?<="portForwardingUris": \[ ")[^"]+(?=" \])') +echo $tunnel_url + +appId=$(az ad app create --display-name $tunnel_id --sign-in-audience "AzureADMyOrg" --query appId -o tsv) +echo "Created AppId: " $appId +secretJson=$(az ad app credential reset --id $appId | jq .) + +clientId=$(echo $secretJson | jq .appId | tr -d '"') +tenantId=$(echo $secretJson | jq .tenant | tr -d '"') +clientSecret=$(echo $secretJson | jq .password | tr -d '"') + +echo "clientId=$clientId" > "$tunnel_id.env" +echo "tenantId=$tenantId" >> "$tunnel_id.env" +echo "clientSecret=$clientSecret" >> "$tunnel_id.env" +echo "DEBUG=agents:*.info" >> "$tunnel_id.env" + +echo "Env File created: $tunnel_id.env" + +endpoint=$tunnel_url"api/messages" + +botJson=$(az bot create \ + --app-type SingleTenant \ + --appid $appId \ + --tenant-id $tenantId \ + --name $tunnel_id \ + --resource-group $resource_group \ + --endpoint $endpoint) + +echo $botJson +$teamsBotJson=$(az bot msteams create -n $tunnel_id -g $resource_group) +echo $teamsBotJson + +echo "Bot created: $tunnel_id" \ No newline at end of file diff --git a/samples/nuget.config b/samples/basic/echo-bot/dotnet/nuget.config similarity index 100% rename from samples/nuget.config rename to samples/basic/echo-bot/dotnet/nuget.config From 50082648519657e66a2c87a9f20d6e6bfc3cd850 Mon Sep 17 00:00:00 2001 From: Rido Date: Mon, 3 Mar 2025 17:52:38 +0000 Subject: [PATCH 8/8] Update README with instructions to debug with tunnels --- samples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/README.md b/samples/README.md index b38f7f1..e7ab96b 100644 --- a/samples/README.md +++ b/samples/README.md @@ -28,6 +28,7 @@ To debug your Agent in `localhost` you can: 1. Run the script `_tools/configure-abs-tunnel.sh`. 1. This script configures devtunnels and ABS, using the machine name, and produces a `.env` file. 1. Copy the generated `.env` file to the sample folder with file name `.env`. +1. Start the tunnel before debugging `devtunnel host ` > [!Tip] > You can use the same ABS instance and tunnel to debug any of these samples, with the only caveat that you can only use one at a time. \ No newline at end of file