-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add EchoBot dotnet Sample #72
Changes from 6 commits
d1ea100
f2cd3dd
3519266
ef2a04d
f6bb2c0
59398d6
ef91f3a
5008264
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -395,3 +395,4 @@ dist/ | |
devTools/ | ||
node_modules/ | ||
|
||
launchSettings.json |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using Microsoft.Agents.Authentication; | ||
using Microsoft.AspNetCore.Authentication.JwtBearer; | ||
using Microsoft.IdentityModel.Protocols; | ||
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; | ||
|
||
namespace Microsoft.Agents.Samples | ||
{ | ||
public static class AspNetExtensions | ||
{ | ||
private static readonly ConcurrentDictionary<string, ConfigurationManager<OpenIdConnectConfiguration>> _openIdMetadataCache = new(); | ||
|
||
/// <summary> | ||
/// Adds token validation typical for ABS/SMBA and Bot-to-bot. | ||
/// default to Azure Public Cloud. | ||
/// </summary> | ||
/// <param name="services"></param> | ||
/// <param name="configuration"></param> | ||
/// <param name="tokenValidationSectionName">Name of the config section to read.</param> | ||
/// <param name="logger">Optional logger to use for authentication event logging.</param> | ||
/// <remarks> | ||
/// Configuration: | ||
/// <code> | ||
/// "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" | ||
/// } | ||
/// </code> | ||
/// | ||
/// `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. | ||
/// </remarks> | ||
public static void AddBotAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation", ILogger logger = null!) | ||
{ | ||
IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); | ||
List<string> validTokenIssuers = tokenValidationSection.GetSection("ValidIssuers").Get<List<string>>()!; | ||
List<string> audiences = tokenValidationSection.GetSection("Audiences").Get<List<string>>()!; | ||
|
||
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<bool>("IsGov", false); | ||
bool azureBotServiceTokenHandling = tokenValidationSection.GetValue<bool>("AzureBotServiceTokenHandling", true); | ||
|
||
// If the `AzureBotServiceOpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate ABS tokens. | ||
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. | ||
string? openIdMetadataUrl = tokenValidationSection["OpenIdMetadataUrl"]; | ||
if (string.IsNullOrEmpty(openIdMetadataUrl)) | ||
{ | ||
openIdMetadataUrl = isGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl; | ||
} | ||
|
||
TimeSpan openIdRefreshInterval = tokenValidationSection.GetValue<TimeSpan>("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 => | ||
{ | ||
string 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]); | ||
string? 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<OpenIdConnectConfiguration>(azureBotServiceOpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) | ||
{ | ||
AutomaticRefreshInterval = openIdRefreshInterval | ||
}; | ||
}); | ||
} | ||
else | ||
{ | ||
context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(openIdMetadataUrl, key => | ||
{ | ||
return new ConfigurationManager<OpenIdConnectConfiguration>(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; | ||
} | ||
}; | ||
}); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// 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; | ||
|
||
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); | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.Agents.Authentication.Msal" Version="0.2.95-alpha" /> | ||
<PackageReference Include="Microsoft.Agents.Hosting.AspNetCore" Version="0.2.95-alpha" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// 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; | ||
|
||
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<IMessageActivity> 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<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> 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); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using EchoBot; | ||
using Microsoft.Agents.Hosting.AspNetCore; | ||
using Microsoft.Agents.Samples; | ||
|
||
WebApplicationBuilder 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<MyBot>(); | ||
|
||
WebApplication app = builder.Build(); | ||
|
||
if (app.Environment.IsDevelopment()) | ||
{ | ||
app.MapGet("/", () => "Microsoft Copilot SDK Sample"); | ||
app.UseDeveloperExceptionPage(); | ||
app.MapControllers().AllowAnonymous(); | ||
} | ||
else | ||
{ | ||
app.MapControllers(); | ||
} | ||
app.Run(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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": "<<TENANT_ID>>", | ||
"Connections__BotServiceConnection__Settings__ClientId": "<<APP_ID>>", | ||
"Connections__BotServiceConnection__Settings__AuthorityEndpoint": "https://login.microsoftonline.com/<<TENANT_ID>>", | ||
"Connections__BotServiceConnection__Settings__ClientSecret": "<<CLIENT_SECRET>>" | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be here. We should add appsettings.Development and appsetting.Production to .gitignore There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. appSettings.Development can be used to share settings across a team, hence it's not gitignored. And more importantly it will be included in the packaged app for deployment. What is a bad security practice. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"Logging": { | ||
"LogLevel": { | ||
"Default": "Information", | ||
"Microsoft.AspNetCore": "Warning" | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure if having 2 CI is the right thing (we could also add some path filters), or just build ALL samples across languages in a single GHAction.