From 3f2747032c1e65965b3b6eeed31a780e0f0103b5 Mon Sep 17 00:00:00 2001 From: Tracy Boehrer Date: Fri, 17 Jan 2025 10:26:54 -0600 Subject: [PATCH 1/3] Ease OAuth by allowing injected middleware and minor cleanup --- .../Microsoft.Agents.BotBuilder/OAuthFlow.cs | 28 ++++--------- .../Hosting/AspNetCore/CloudAdapter.cs | 11 ++++- src/samples/AuthenticationBot/AuthBot.cs | 41 ++++++++----------- .../AuthenticationBot.csproj | 1 + src/samples/AuthenticationBot/Program.cs | 11 +++++ .../Bots/DialogBot.cs | 4 +- .../Bots/TeamsBot.cs | 4 +- .../Program.cs | 12 ++++-- .../TeamsSSOAdapter.cs | 16 ++------ 9 files changed, 62 insertions(+), 66 deletions(-) diff --git a/src/libraries/BotBuilder/Microsoft.Agents.BotBuilder/OAuthFlow.cs b/src/libraries/BotBuilder/Microsoft.Agents.BotBuilder/OAuthFlow.cs index e6e74855..9fdfa0b8 100644 --- a/src/libraries/BotBuilder/Microsoft.Agents.BotBuilder/OAuthFlow.cs +++ b/src/libraries/BotBuilder/Microsoft.Agents.BotBuilder/OAuthFlow.cs @@ -184,27 +184,17 @@ private async Task SendOAuthCardAsync(ITurnContext turnContext, IActivity prompt { var cardActionType = ActionTypes.Signin; var signInResource = await GetTokenClient(turnContext).GetSignInResourceAsync(ConnectionName, turnContext.Activity, null, cancellationToken).ConfigureAwait(false); - var value = signInResource.SignInLink; - - // use the SignInLink when - // in speech channel or - // bot is a skill or - // an extra OAuthAppCredentials is being passed in - /* - if (turnContext.Activity.IsFromStreamingConnection() - || (turnContext.TurnState.Get(ChannelAdapter.BotIdentityKey) is ClaimsIdentity botIdentity && ClaimsHelpers.IsBotClaim(botIdentity.Claims))) - { - if (turnContext.Activity.ChannelId == Channels.Emulator) - { - cardActionType = ActionTypes.OpenUrl; - } - } - */ + + string value; if ((ShowSignInLink != null && ShowSignInLink == false) || (ShowSignInLink == null && !ChannelRequiresSignInLink(turnContext.Activity.ChannelId))) { value = null; } + else + { + value = signInResource.SignInLink; + } prompt.Attachments.Add(new Attachment { @@ -213,8 +203,8 @@ private async Task SendOAuthCardAsync(ITurnContext turnContext, IActivity prompt { Text = Text, ConnectionName = ConnectionName, - Buttons = new[] - { + Buttons = + [ new CardAction { Title = Title, @@ -222,7 +212,7 @@ private async Task SendOAuthCardAsync(ITurnContext turnContext, IActivity prompt Type = cardActionType, Value = value }, - }, + ], TokenExchangeResource = signInResource.TokenExchangeResource, TokenPostResource = signInResource.TokenPostResource }, diff --git a/src/libraries/Hosting/AspNetCore/CloudAdapter.cs b/src/libraries/Hosting/AspNetCore/CloudAdapter.cs index efdde069..3e350078 100644 --- a/src/libraries/Hosting/AspNetCore/CloudAdapter.cs +++ b/src/libraries/Hosting/AspNetCore/CloudAdapter.cs @@ -42,11 +42,20 @@ public CloudAdapter( IChannelServiceClientFactory channelServiceClientFactory, IActivityTaskQueue activityTaskQueue, ILogger logger = null, - bool async = true) : base(channelServiceClientFactory, logger) + bool async = true, + params Core.Interfaces.IMiddleware[] middlewares) : base(channelServiceClientFactory, logger) { _activityTaskQueue = activityTaskQueue ?? throw new ArgumentNullException(nameof(activityTaskQueue)); _async = async; + if (middlewares != null) + { + foreach (var middleware in middlewares) + { + Use(middleware); + } + } + OnTurnError = async (turnContext, exception) => { // Log any leaked exception from the application. diff --git a/src/samples/AuthenticationBot/AuthBot.cs b/src/samples/AuthenticationBot/AuthBot.cs index 4d9e1649..b98a9181 100644 --- a/src/samples/AuthenticationBot/AuthBot.cs +++ b/src/samples/AuthenticationBot/AuthBot.cs @@ -8,7 +8,7 @@ using Microsoft.Agents.BotBuilder; using Microsoft.Agents.Core.Interfaces; using Microsoft.Agents.Core.Models; -using Microsoft.Agents.Storage; +using Microsoft.Agents.State; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -20,33 +20,20 @@ public class AuthBot : ActivityHandler private readonly ILogger _logger; private readonly IConfiguration _configuration; private readonly OAuthFlow _flow; - private readonly IStorage _storage; + private readonly ConversationState _conversationState; private FlowState _state; - public AuthBot(IConfiguration configuration, IStorage storage, ILogger logger) + public AuthBot(IConfiguration configuration, ConversationState conversationState, ILogger logger) { _logger = logger ?? NullLogger.Instance; _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _storage = storage ?? throw new ArgumentNullException(nameof(storage)); + _conversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState)); _flow = new OAuthFlow("Sign In", "Please sign in", _configuration["ConnectionName"], 30000, null); } - public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) - { - // Read OAuthFlow state for this conversation - var stateKey = GetStorageKey(turnContext); - var items = await _storage.ReadAsync([stateKey], cancellationToken); - _state = items.TryGetValue(stateKey, out object value) ? (FlowState)value : new FlowState(); - - await base.OnTurnAsync(turnContext, cancellationToken); - - // Store any changes to the OAuthFlow state after the turn is complete. - items[stateKey] = _state; - await _storage.WriteAsync(items, cancellationToken); - } - protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) { + // Display a welcome message foreach (var member in turnContext.Activity.MembersAdded) { if (member.Id != turnContext.Activity.Recipient.Id) @@ -116,6 +103,17 @@ protected override async Task OnSignInInvokeAsync(ITurnContext await OnContinueFlow(turnContext, cancellationToken); } + protected override async Task OnTurnBeginAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) + { + _state = await _conversationState.GetPropertyAsync(turnContext, "flowState", () => new FlowState(), cancellationToken); + } + + protected override async Task OnTurnEndAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) + { + // Save any state changes that might have occurred during the turn. + await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken); + } + private async Task OnContinueFlow(ITurnContext turnContext, CancellationToken cancellationToken) { TokenResponse tokenResponse = null; @@ -140,13 +138,6 @@ private async Task OnContinueFlow(ITurnContext turnContext, Cance _state.FlowStarted = false; return tokenResponse; } - - private static string GetStorageKey(ITurnContext turnContext) - { - var channelId = turnContext.Activity.ChannelId ?? throw new InvalidOperationException("invalid activity-missing channelId"); - var conversationId = turnContext.Activity.Conversation?.Id ?? throw new InvalidOperationException("invalid activity-missing Conversation.Id"); - return $"{channelId}/conversations/{conversationId}/flowState"; - } } class FlowState diff --git a/src/samples/AuthenticationBot/AuthenticationBot.csproj b/src/samples/AuthenticationBot/AuthenticationBot.csproj index f7743d92..3f8ec35f 100644 --- a/src/samples/AuthenticationBot/AuthenticationBot.csproj +++ b/src/samples/AuthenticationBot/AuthenticationBot.csproj @@ -7,6 +7,7 @@ + diff --git a/src/samples/AuthenticationBot/Program.cs b/src/samples/AuthenticationBot/Program.cs index 1c8169ce..4cc5e2dd 100644 --- a/src/samples/AuthenticationBot/Program.cs +++ b/src/samples/AuthenticationBot/Program.cs @@ -9,6 +9,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Agents.Hosting.AspNetCore; using AuthenticationBot; +using Microsoft.Agents.Core.Interfaces; +using Microsoft.Agents.BotBuilder.Teams; +using Microsoft.Agents.State; var builder = WebApplication.CreateBuilder(args); @@ -24,9 +27,17 @@ // Add basic bot functionality builder.AddBot(); +builder.Services.AddSingleton((sp) => +{ + return [new TeamsSSOTokenExchangeMiddleware(sp.GetService(), builder.Configuration["ConnectionName"])]; +}); + // Add IStorage for turn state persistence builder.Services.AddSingleton(); +// Create the Conversation state. +builder.Services.AddSingleton(); + var app = builder.Build(); if (app.Environment.IsDevelopment()) diff --git a/src/samples/Teams/bot-conversation-sso-quickstart/Bots/DialogBot.cs b/src/samples/Teams/bot-conversation-sso-quickstart/Bots/DialogBot.cs index 44755349..9077a837 100644 --- a/src/samples/Teams/bot-conversation-sso-quickstart/Bots/DialogBot.cs +++ b/src/samples/Teams/bot-conversation-sso-quickstart/Bots/DialogBot.cs @@ -18,12 +18,11 @@ namespace BotConversationSsoQuickstart.Bots // each with dependency on distinct IBot types, this way ASP Dependency Injection can glue everything together without ambiguity. // The ConversationState is used by the Dialog system. The UserState isn't, however, it might have been used in a Dialog implementation, // and the requirement is that all BotState objects are saved at the end of a turn. - public class DialogBot(ConversationState conversationState, UserState userState, T dialog, ILogger> logger) : TeamsActivityHandler where T : Dialog + public class DialogBot(ConversationState conversationState, T dialog, ILogger> logger) : TeamsActivityHandler where T : Dialog { protected readonly BotState _conversationState = conversationState; protected readonly Dialog _dialog = dialog; protected readonly ILogger _logger = logger; - protected readonly BotState _userState = userState; /// /// Handle when a message is addressed to the bot. @@ -47,7 +46,6 @@ protected override async Task OnTurnEndAsync(ITurnContext turnContext, Cancellat { // Save any state changes that might have occurred during the turn. await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken); - await _userState.SaveChangesAsync(turnContext, false, cancellationToken); } } } \ No newline at end of file diff --git a/src/samples/Teams/bot-conversation-sso-quickstart/Bots/TeamsBot.cs b/src/samples/Teams/bot-conversation-sso-quickstart/Bots/TeamsBot.cs index ea3e9289..9eacbfc0 100644 --- a/src/samples/Teams/bot-conversation-sso-quickstart/Bots/TeamsBot.cs +++ b/src/samples/Teams/bot-conversation-sso-quickstart/Bots/TeamsBot.cs @@ -14,8 +14,8 @@ namespace BotConversationSsoQuickstart.Bots { // This bot is derived (view DialogBot) from the TeamsActivityHandler class currently included as part of this sample. - public class TeamsBot(ConversationState conversationState, UserState userState, T dialog, ILogger> logger) - : DialogBot(conversationState, userState, dialog, logger) where T : Dialog + public class TeamsBot(ConversationState conversationState, T dialog, ILogger> logger) + : DialogBot(conversationState, dialog, logger) where T : Dialog { protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) { diff --git a/src/samples/Teams/bot-conversation-sso-quickstart/Program.cs b/src/samples/Teams/bot-conversation-sso-quickstart/Program.cs index 497382f2..4a34efe7 100644 --- a/src/samples/Teams/bot-conversation-sso-quickstart/Program.cs +++ b/src/samples/Teams/bot-conversation-sso-quickstart/Program.cs @@ -12,6 +12,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Agents.BotBuilder.Teams; +using Microsoft.Agents.Core.Interfaces; var builder = WebApplication.CreateBuilder(args); @@ -27,13 +29,15 @@ // Add basic bot functionality builder.AddBot, TeamsSSOAdapter>(); +builder.Services.AddSingleton((sp) => +{ + return [new TeamsSSOTokenExchangeMiddleware(sp.GetService(), builder.Configuration["ConnectionName"])]; +}); + // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.) builder.Services.AddSingleton(); -// Create the User state. (Used in this bot's Dialog implementation.) -builder.Services.AddSingleton(); - -// Create the Conversation state. (Used by the Dialog system itself.) +// Create the Conversation state. builder.Services.AddSingleton(); // The Dialog that will be run by the bot. diff --git a/src/samples/Teams/bot-conversation-sso-quickstart/TeamsSSOAdapter.cs b/src/samples/Teams/bot-conversation-sso-quickstart/TeamsSSOAdapter.cs index a2461e2f..7aab678f 100644 --- a/src/samples/Teams/bot-conversation-sso-quickstart/TeamsSSOAdapter.cs +++ b/src/samples/Teams/bot-conversation-sso-quickstart/TeamsSSOAdapter.cs @@ -6,11 +6,9 @@ using Microsoft.Agents.State; using Microsoft.Agents.Hosting.AspNetCore; using Microsoft.Agents.Hosting.AspNetCore.BackgroundQueue; -using Microsoft.Agents.BotBuilder.Teams; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Agents.BotBuilder; -using Microsoft.Agents.Storage; +using Microsoft.Agents.Core.Interfaces; namespace BotConversationSsoQuickstart { @@ -19,20 +17,14 @@ public class TeamsSSOAdapter : CloudAdapter public TeamsSSOAdapter( IChannelServiceClientFactory channelServiceClientFactory, IActivityTaskQueue activityTaskQueue, - IConfiguration configuration, ILogger logger, - IStorage storage, - ConversationState conversationState) - : base(channelServiceClientFactory, activityTaskQueue, logger) + ConversationState conversationState, + params IMiddleware[] middlewares) + : base(channelServiceClientFactory, activityTaskQueue, logger, middlewares: middlewares) { - base.Use(new TeamsSSOTokenExchangeMiddleware(storage, configuration["ConnectionName"])); - OnTurnError = async (turnContext, exception) => { // Log any leaked exception from the application. - // NOTE: In production environment, you should consider logging this to - // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how - // to add telemetry capture to your bot. logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); // Uncomment below commented line for local debugging.. From 22ef9c21816ba63cfcc5cc1f5d9d5657ccb1e71c Mon Sep 17 00:00:00 2001 From: Tracy Boehrer Date: Fri, 17 Jan 2025 11:09:24 -0600 Subject: [PATCH 2/3] EvalClient project change --- src/samples/EvalClient/EvalClient.csproj | 45 ++++++++++++------------ 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/samples/EvalClient/EvalClient.csproj b/src/samples/EvalClient/EvalClient.csproj index e44c1669..89df21e0 100644 --- a/src/samples/EvalClient/EvalClient.csproj +++ b/src/samples/EvalClient/EvalClient.csproj @@ -1,12 +1,11 @@ - - - - Exe - net8.0 - enable - enable - - + + + + net8.0 + enable + enable + + @@ -16,20 +15,20 @@ - - + + - - - - - PreserveNewest - - - PreserveNewest - - - - + + + + + PreserveNewest + + + PreserveNewest + + + + From dbf40ca63f9e1fe8c9253abf260382cb29d21f1b Mon Sep 17 00:00:00 2001 From: Tracy Boehrer Date: Fri, 17 Jan 2025 11:25:59 -0600 Subject: [PATCH 3/3] EvalClient: Fixed some nullable warnings --- src/samples/EvalClient/EvaluationService.cs | 3 ++- src/samples/EvalClient/Program.cs | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/samples/EvalClient/EvaluationService.cs b/src/samples/EvalClient/EvaluationService.cs index 85e85b21..8cfdfccf 100644 --- a/src/samples/EvalClient/EvaluationService.cs +++ b/src/samples/EvalClient/EvaluationService.cs @@ -4,10 +4,11 @@ using System.Globalization; using CsvHelper.Configuration; using Microsoft.Agents.CopilotStudio.Client; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.AI; using Microsoft.Agents.Core.Models; +#nullable disable + namespace EvalClient; /// diff --git a/src/samples/EvalClient/Program.cs b/src/samples/EvalClient/Program.cs index 8b5b677a..2d9fbb20 100644 --- a/src/samples/EvalClient/Program.cs +++ b/src/samples/EvalClient/Program.cs @@ -3,13 +3,12 @@ // See https://aka.ms/new-console-template for more information using System.ClientModel; using Microsoft.Agents.CopilotStudio.Client; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.AI; using Azure.AI.OpenAI; using EvalClient; +#nullable disable + // Setup the Direct To Engine client example. HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);