Skip to content
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

Adding teams info samples #60

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<ItemGroup>
<PackageVersion Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageVersion Include="CsvHelper" Version="33.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.SpaServices" Version="3.1.32" />
<PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.36" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.1.0-preview.1.25064.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.1.2" />
Expand Down
21 changes: 21 additions & 0 deletions src/Microsoft.Agents.SDK.sln
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InMeetingNotificationsBot",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagMentionBot", "samples\Teams\bot-tag-mention\TagMentionBot.csproj", "{BC5EFA6C-7EB5-4803-B7C5-093892E9DBB8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotRequestApproval", "samples\Teams\bot-request-approval\BotRequestApproval.csproj", "{BF587311-1240-889C-E6AE-ED61A6ED2B37}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TypeaheadSearch", "samples\Teams\bot-type-ahead-search-adaptive-cards\TypeaheadSearch.csproj", "{697C1093-D392-8ABC-2BC8-F955022B1853}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeetingContextApp", "samples\Teams\Meeting-Context-App\MeetingContextApp.csproj", "{153EA430-9914-18E7-409F-7292CB1914AB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Agents.Authentication.Msal.Tests", "tests\Microsoft.Agents.Authentication.Msal.Tests\Microsoft.Agents.Authentication.Msal.Tests.csproj", "{B9AD64EF-EA22-4CAC-B89B-03CEE46CFF4F}"
EndProject
Global
Expand Down Expand Up @@ -332,6 +338,18 @@ Global
{BC5EFA6C-7EB5-4803-B7C5-093892E9DBB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC5EFA6C-7EB5-4803-B7C5-093892E9DBB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC5EFA6C-7EB5-4803-B7C5-093892E9DBB8}.Release|Any CPU.Build.0 = Release|Any CPU
{BF587311-1240-889C-E6AE-ED61A6ED2B37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF587311-1240-889C-E6AE-ED61A6ED2B37}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF587311-1240-889C-E6AE-ED61A6ED2B37}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF587311-1240-889C-E6AE-ED61A6ED2B37}.Release|Any CPU.Build.0 = Release|Any CPU
{697C1093-D392-8ABC-2BC8-F955022B1853}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{697C1093-D392-8ABC-2BC8-F955022B1853}.Debug|Any CPU.Build.0 = Debug|Any CPU
{697C1093-D392-8ABC-2BC8-F955022B1853}.Release|Any CPU.ActiveCfg = Release|Any CPU
{697C1093-D392-8ABC-2BC8-F955022B1853}.Release|Any CPU.Build.0 = Release|Any CPU
{153EA430-9914-18E7-409F-7292CB1914AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{153EA430-9914-18E7-409F-7292CB1914AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{153EA430-9914-18E7-409F-7292CB1914AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{153EA430-9914-18E7-409F-7292CB1914AB}.Release|Any CPU.Build.0 = Release|Any CPU
{B9AD64EF-EA22-4CAC-B89B-03CEE46CFF4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9AD64EF-EA22-4CAC-B89B-03CEE46CFF4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9AD64EF-EA22-4CAC-B89B-03CEE46CFF4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -399,6 +417,9 @@ Global
{7D1A1CE5-6D9B-4D31-AC77-C3B1787F575D} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{06E490F7-F0BB-E3C4-54FE-5210627292A1} = {183D0E91-B84E-46D7-B653-6D85B4CCF804}
{BC5EFA6C-7EB5-4803-B7C5-093892E9DBB8} = {183D0E91-B84E-46D7-B653-6D85B4CCF804}
{BF587311-1240-889C-E6AE-ED61A6ED2B37} = {183D0E91-B84E-46D7-B653-6D85B4CCF804}
{697C1093-D392-8ABC-2BC8-F955022B1853} = {183D0E91-B84E-46D7-B653-6D85B4CCF804}
{153EA430-9914-18E7-409F-7292CB1914AB} = {183D0E91-B84E-46D7-B653-6D85B4CCF804}
{B9AD64EF-EA22-4CAC-B89B-03CEE46CFF4F} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
Expand Down
207 changes: 207 additions & 0 deletions src/samples/Teams/Meeting-Context-App/AspNetExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// 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<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="authenticationSection">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 authenticationSection = "TokenValidation", ILogger logger = null)
AjayJ-MSFT marked this conversation as resolved.
Show resolved Hide resolved
{
IConfigurationSection tokenValidationSection = configuration.GetSection("TokenValidation");

List<string> validTokenIssuers = tokenValidationSection.GetSection("ValidIssuers").Get<List<string>>();

// 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",
];

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));
}
}

List<string> audiences = tokenValidationSection.GetSection("Audiences").Get<List<string>>();
if (audiences == null || audiences.Count == 0)
{
throw new ArgumentException($"{authenticationSection}:Audiences requires at least one value");
}

bool isGov = tokenValidationSection.GetValue<bool>("IsGov", false);
var 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.
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<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 =>
{
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<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;
}
};
});
}
}
}
90 changes: 90 additions & 0 deletions src/samples/Teams/Meeting-Context-App/Bots/MeetingContextBot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Agents.BotBuilder.Teams;
using Microsoft.Agents.Core.Interfaces;
using Microsoft.Agents.Core.Models;
using Microsoft.Agents.Core.Teams.Models;

namespace MeetingContextApp.Bots
{
public class MeetingContextBot : TeamsActivityHandler
{
public const string CommandString = "Please use one of these two commands: **Meeting Context** or **Participant Context**";

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
if (turnContext.Activity.Text != null)
{
var text = turnContext.Activity.RemoveRecipientMention();
if (text.ToLower().Contains("participant context"))
{
var channelDataObject = turnContext.Activity.GetChannelData<TeamsChannelData>();

var tenantId = channelDataObject.Tenant.Id;
var meetingId = channelDataObject.Meeting.Id;
var participantId = turnContext.Activity.From.AadObjectId;

// GetMeetingParticipant
TeamsMeetingParticipant participantDetails = await TeamsInfo.GetMeetingParticipantAsync(turnContext, meetingId, participantId, tenantId, cancellationToken: cancellationToken);

var formattedString = GetFormattedSerializeObject(participantDetails);

await turnContext.SendActivityAsync(MessageFactory.Text(formattedString), cancellationToken);
}
else if (text.ToLower().Contains("meeting context"))
{
MeetingInfo meetingInfo = await TeamsInfo.GetMeetingInfoAsync(turnContext, cancellationToken: cancellationToken);

var formattedString = GetFormattedSerializeObject(meetingInfo);

await turnContext.SendActivityAsync(MessageFactory.Text(formattedString), cancellationToken);
}
else
{
await turnContext.SendActivityAsync(MessageFactory.Text(CommandString), cancellationToken);
}
}
}

protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Hello and Welcome!"), cancellationToken);
await turnContext.SendActivityAsync(MessageFactory.Text(CommandString), cancellationToken);
}

/// <summary>
/// Gets the serialize formatted object string.
/// </summary>
/// <param name="obj">Incoming object needs to be formatted.</param>
/// <returns>Formatted string.</returns>
private static string GetFormattedSerializeObject (object obj)
{
var formattedString = "";
foreach (var meetingDetails in obj.GetType().GetProperties())
{
var detail = meetingDetails.GetValue(obj, null);
var block = $"<b>{meetingDetails.Name}:</b> <br>";
var storeTemporaryFormattedString = "";

if (detail != null)
{
if (detail.GetType().Name != "String")
{
foreach (var value in detail.GetType().GetProperties())
{
storeTemporaryFormattedString += $" <b> &nbsp;&nbsp;{value.Name}:</b> {value.GetValue(detail, null)}<br/>";
}

Console.WriteLine(storeTemporaryFormattedString);

formattedString += block + storeTemporaryFormattedString;
storeTemporaryFormattedString = String.Empty;
}
}
}

return formattedString;
}
}
}
23 changes: 23 additions & 0 deletions src/samples/Teams/Meeting-Context-App/ClientApp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
Loading
Loading