Skip to content

Commit a6dcb99

Browse files
authored
Merge pull request #219 from microsoft/users/rido/teams-samples
Add TeamsExtension sample
2 parents e2b496f + f43c3bc commit a6dcb99

File tree

11 files changed

+634
-34
lines changed

11 files changed

+634
-34
lines changed

src/Microsoft.Agents.SDK.sln

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 17
4-
VisualStudioVersion = 17.2.32505.173
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.0.10615.163
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{4269F3C3-6B42-419B-B64A-3E6DC0F1574A}"
77
EndProject
@@ -129,6 +129,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Agents.Extensions
129129
EndProject
130130
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Agents.Extensions.Teams.AI.Tests", "tests\Microsoft.Agents.Extensions.Teams.AI.Tests\Microsoft.Agents.Extensions.Teams.AI.Tests.csproj", "{3D6B6E6F-483B-45F3-B53C-93A5A72EE0BF}"
131131
EndProject
132+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsAgent", "samples\Teams\TeamsAgent\TeamsAgent.csproj", "{D6410977-B795-C315-CC94-C2482E84BB4A}"
133+
EndProject
132134
Global
133135
GlobalSection(SolutionConfigurationPlatforms) = preSolution
134136
Debug|Any CPU = Debug|Any CPU
@@ -703,6 +705,18 @@ Global
703705
{3D6B6E6F-483B-45F3-B53C-93A5A72EE0BF}.Release|x64.Build.0 = Release|Any CPU
704706
{3D6B6E6F-483B-45F3-B53C-93A5A72EE0BF}.Release|x86.ActiveCfg = Release|Any CPU
705707
{3D6B6E6F-483B-45F3-B53C-93A5A72EE0BF}.Release|x86.Build.0 = Release|Any CPU
708+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
709+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
710+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Debug|x64.ActiveCfg = Debug|Any CPU
711+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Debug|x64.Build.0 = Debug|Any CPU
712+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Debug|x86.ActiveCfg = Debug|Any CPU
713+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Debug|x86.Build.0 = Debug|Any CPU
714+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Release|Any CPU.ActiveCfg = Release|Any CPU
715+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Release|Any CPU.Build.0 = Release|Any CPU
716+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Release|x64.ActiveCfg = Release|Any CPU
717+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Release|x64.Build.0 = Release|Any CPU
718+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Release|x86.ActiveCfg = Release|Any CPU
719+
{D6410977-B795-C315-CC94-C2482E84BB4A}.Release|x86.Build.0 = Release|Any CPU
706720
EndGlobalSection
707721
GlobalSection(SolutionProperties) = preSolution
708722
HideSolutionNode = FALSE
@@ -768,6 +782,7 @@ Global
768782
{D5202D4A-2F15-CE1B-F82C-2405C040EB14} = {674A812C-7287-4883-97F9-697D83750648}
769783
{A44FB40F-1383-4AAB-A7F0-261CCFF172D8} = {927E4F54-6FBC-4390-BF64-BF3C1874C1AB}
770784
{3D6B6E6F-483B-45F3-B53C-93A5A72EE0BF} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
785+
{D6410977-B795-C315-CC94-C2482E84BB4A} = {674A812C-7287-4883-97F9-697D83750648}
771786
EndGlobalSection
772787
GlobalSection(ExtensibilityGlobals) = postSolution
773788
SolutionGuid = {F1E8E538-309A-46F8-9CE7-AEC6589FAE60}

src/libraries/Extensions/Microsoft.Agents.Extensions.Teams/App/TeamsApplicationOptions.cs

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/samples/FullAuthentication/AspNetExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static void AddAgentAspNetAuthentication(this IServiceCollection services
6767

6868
if (!tokenValidationSection.Exists())
6969
{
70-
logger?.LogError("Missing configuration section '{tokenValidationSectionName}'. This section is required to be present in appsettings.json",tokenValidationSectionName);
70+
logger?.LogError("Missing configuration section '{tokenValidationSectionName}'. This section is required to be present in appsettings.json", tokenValidationSectionName);
7171
throw new InvalidOperationException($"Missing configuration section '{tokenValidationSectionName}'. This section is required to be present in appsettings.json");
7272
}
7373

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Agents.Authentication;
5+
using Microsoft.AspNetCore.Authentication.JwtBearer;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.IdentityModel.Protocols;
10+
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
11+
using Microsoft.IdentityModel.Tokens;
12+
using Microsoft.IdentityModel.Validators;
13+
using System;
14+
using System.Collections.Concurrent;
15+
using System.Collections.Generic;
16+
using System.Globalization;
17+
using System.IdentityModel.Tokens.Jwt;
18+
using System.Linq;
19+
using System.Net.Http;
20+
using System.Threading.Tasks;
21+
22+
namespace TeamsAgent;
23+
24+
public static class AspNetExtensions
25+
{
26+
private static readonly ConcurrentDictionary<string, ConfigurationManager<OpenIdConnectConfiguration>> _openIdMetadataCache = new();
27+
28+
/// <summary>
29+
/// Adds token validation typical for ABS/SMBA and agent-to-agent.
30+
/// default to Azure Public Cloud.
31+
/// </summary>
32+
/// <param name="services"></param>
33+
/// <param name="configuration"></param>
34+
/// <param name="tokenValidationSectionName">Name of the config section to read.</param>
35+
/// <param name="logger">Optional logger to use for authentication event logging.</param>
36+
/// <remarks>
37+
/// Configuration:
38+
/// <code>
39+
/// "TokenValidation": {
40+
/// "Audiences": [
41+
/// "{required:agent-appid}"
42+
/// ],
43+
/// "TenantId": "{recommended:tenant-id}",
44+
/// "ValidIssuers": [
45+
/// "{default:Public-AzureBotService}"
46+
/// ],
47+
/// "IsGov": {optional:false},
48+
/// "AzureBotServiceOpenIdMetadataUrl": optional,
49+
/// "OpenIdMetadataUrl": optional,
50+
/// "AzureBotServiceTokenHandling": "{optional:true}"
51+
/// "OpenIdMetadataRefresh": "optional-12:00:00"
52+
/// }
53+
/// </code>
54+
///
55+
/// `IsGov` can be omitted, in which case public Azure Bot Service and Azure Cloud metadata urls are used.
56+
/// `ValidIssuers` can be omitted, in which case the Public Azure Bot Service issuers are used.
57+
/// `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.
58+
/// `AzureBotServiceOpenIdMetadataUrl` can be omitted. In which case default values in combination with `IsGov` is used.
59+
/// `OpenIdMetadataUrl` can be omitted. In which case default values in combination with `IsGov` is used.
60+
/// `AzureBotServiceTokenHandling` defaults to true and should always be true until Azure Bot Service sends Entra ID token.
61+
/// </remarks>
62+
public static void AddAgentAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation", ILogger logger = null!)
63+
{
64+
IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName);
65+
List<string> validTokenIssuers = tokenValidationSection.GetSection("ValidIssuers").Get<List<string>>()!;
66+
List<string> audiences = tokenValidationSection.GetSection("Audiences").Get<List<string>>()!;
67+
68+
if (!tokenValidationSection.Exists())
69+
{
70+
logger?.LogError("Missing configuration section '{tokenValidationSectionName}'. This section is required to be present in appsettings.json", tokenValidationSectionName);
71+
throw new InvalidOperationException($"Missing configuration section '{tokenValidationSectionName}'. This section is required to be present in appsettings.json");
72+
}
73+
74+
// If ValidIssuers is empty, default for ABS Public Cloud
75+
if (validTokenIssuers == null || validTokenIssuers.Count == 0)
76+
{
77+
validTokenIssuers =
78+
[
79+
"https://api.botframework.com",
80+
"https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/",
81+
"https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0",
82+
"https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",
83+
"https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",
84+
"https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/",
85+
"https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0",
86+
];
87+
88+
string? tenantId = tokenValidationSection["TenantId"];
89+
if (!string.IsNullOrEmpty(tenantId))
90+
{
91+
validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, tenantId));
92+
validTokenIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, tenantId));
93+
}
94+
}
95+
96+
if (audiences == null || audiences.Count == 0)
97+
{
98+
throw new ArgumentException($"{tokenValidationSectionName}:Audiences requires at least one value");
99+
}
100+
101+
bool isGov = tokenValidationSection.GetValue("IsGov", false);
102+
bool azureBotServiceTokenHandling = tokenValidationSection.GetValue("AzureBotServiceTokenHandling", true);
103+
104+
// If the `AzureBotServiceOpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate ABS tokens.
105+
string? azureBotServiceOpenIdMetadataUrl = tokenValidationSection["AzureBotServiceOpenIdMetadataUrl"];
106+
if (string.IsNullOrEmpty(azureBotServiceOpenIdMetadataUrl))
107+
{
108+
azureBotServiceOpenIdMetadataUrl = isGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl;
109+
}
110+
111+
// If the `OpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate Entra ID tokens.
112+
string? openIdMetadataUrl = tokenValidationSection["OpenIdMetadataUrl"];
113+
if (string.IsNullOrEmpty(openIdMetadataUrl))
114+
{
115+
openIdMetadataUrl = isGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl;
116+
}
117+
118+
TimeSpan openIdRefreshInterval = tokenValidationSection.GetValue("OpenIdMetadataRefresh", BaseConfigurationManager.DefaultAutomaticRefreshInterval);
119+
120+
_ = services.AddAuthentication(options =>
121+
{
122+
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
123+
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
124+
})
125+
.AddJwtBearer(options =>
126+
{
127+
options.SaveToken = true;
128+
options.TokenValidationParameters = new TokenValidationParameters
129+
{
130+
ValidateIssuer = true,
131+
ValidateAudience = true,
132+
ValidateLifetime = true,
133+
ClockSkew = TimeSpan.FromMinutes(5),
134+
ValidIssuers = validTokenIssuers,
135+
ValidAudiences = audiences,
136+
ValidateIssuerSigningKey = true,
137+
RequireSignedTokens = true,
138+
};
139+
140+
// Using Microsoft.IdentityModel.Validators
141+
options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation();
142+
143+
options.Events = new JwtBearerEvents
144+
{
145+
// Create a ConfigurationManager based on the requestor. This is to handle ABS non-Entra tokens.
146+
OnMessageReceived = async context =>
147+
{
148+
string authorizationHeader = context.Request.Headers.Authorization.ToString();
149+
150+
if (string.IsNullOrEmpty(authorizationHeader))
151+
{
152+
// Default to AadTokenValidation handling
153+
context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager;
154+
await Task.CompletedTask.ConfigureAwait(false);
155+
return;
156+
}
157+
158+
string[]? parts = authorizationHeader?.Split(' ');
159+
if (parts?.Length != 2 || parts[0] != "Bearer")
160+
{
161+
// Default to AadTokenValidation handling
162+
context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager;
163+
await Task.CompletedTask.ConfigureAwait(false);
164+
return;
165+
}
166+
167+
JwtSecurityToken? token = new(parts[1]);
168+
string issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value!;
169+
170+
if (azureBotServiceTokenHandling && AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer))
171+
{
172+
// Use the Azure Bot authority for this configuration manager
173+
context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(azureBotServiceOpenIdMetadataUrl, key =>
174+
{
175+
return new ConfigurationManager<OpenIdConnectConfiguration>(azureBotServiceOpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient())
176+
{
177+
AutomaticRefreshInterval = openIdRefreshInterval
178+
};
179+
});
180+
}
181+
else
182+
{
183+
context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(openIdMetadataUrl, key =>
184+
{
185+
return new ConfigurationManager<OpenIdConnectConfiguration>(openIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient())
186+
{
187+
AutomaticRefreshInterval = openIdRefreshInterval
188+
};
189+
});
190+
}
191+
192+
await Task.CompletedTask.ConfigureAwait(false);
193+
},
194+
195+
OnTokenValidated = context =>
196+
{
197+
logger?.LogDebug("TOKEN Validated");
198+
return Task.CompletedTask;
199+
},
200+
OnForbidden = context =>
201+
{
202+
logger?.LogWarning("Forbidden: {m}", context.Result.ToString());
203+
return Task.CompletedTask;
204+
},
205+
OnAuthenticationFailed = context =>
206+
{
207+
logger?.LogWarning("Auth Failed {m}", context.Exception.ToString());
208+
return Task.CompletedTask;
209+
}
210+
};
211+
});
212+
}
213+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Microsoft.Agents.Authentication;
2+
using Microsoft.Agents.Builder;
3+
using Microsoft.Agents.Builder.App;
4+
using Microsoft.Agents.Builder.State;
5+
using Microsoft.Agents.Extensions.Teams.App;
6+
using Microsoft.Agents.Hosting.AspNetCore;
7+
using Microsoft.Agents.Storage;
8+
using TeamsAgent;
9+
10+
11+
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
12+
builder.Services.AddHttpClient();
13+
builder.Services.AddAgentAspNetAuthentication(builder.Configuration);
14+
builder.Services.AddSingleton<IStorage, MemoryStorage>();
15+
builder.Services.AddTransient(sp =>
16+
{
17+
return new AgentApplicationOptions(sp.GetService<IStorage>()!)
18+
{
19+
StartTypingTimer = false,
20+
TurnStateFactory = () => new TurnState(sp.GetService<IStorage>()!),
21+
// new AttachmentDownloader(sp.GetService<IHttpClientFactory>()!),
22+
FileDownloaders = [new TeamsAttachmentDownloader(new TeamsAttachmentDownloaderOptions()
23+
{
24+
TokenProviderName = "ServiceConnection"
25+
},
26+
sp.GetService<IConnections>()!,
27+
sp.GetService<IHttpClientFactory>()!)]
28+
};
29+
});
30+
builder.AddAgent<TeamsAgent.TeamsAgent>();
31+
WebApplication app = builder.Build();
32+
33+
app.MapPost("/api/messages",
34+
(HttpRequest request, HttpResponse response, IAgentHttpAdapter adapter, IAgent agent, CancellationToken cancellationToken) =>
35+
adapter.ProcessAsync(request, response, agent, cancellationToken));
36+
app.Run();

0 commit comments

Comments
 (0)