Skip to content

Commit

Permalink
Merge pull request #60 from microsoft/users/v-ajayjadhav/TeamsInfoSam…
Browse files Browse the repository at this point in the history
…ples

Adding teams info samples
  • Loading branch information
tracyboehrer authored Feb 3, 2025
2 parents 8e22337 + afcbd59 commit d97e9de
Show file tree
Hide file tree
Showing 87 changed files with 3,398 additions and 1 deletion.
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
23 changes: 22 additions & 1 deletion 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 @@ -336,6 +342,18 @@ Global
{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
{B9AD64EF-EA22-4CAC-B89B-03CEE46CFF4F}.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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -399,7 +417,10 @@ 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}
{B9AD64EF-EA22-4CAC-B89B-03CEE46CFF4F} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{B9AD64EF-EA22-4CAC-B89B-03CEE46CFF4F} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F1E8E538-309A-46F8-9CE7-AEC6589FAE60}
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)
{
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;
}
};
});
}
}
}
94 changes: 94 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,94 @@
// 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;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

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

0 comments on commit d97e9de

Please sign in to comment.