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

Sample Code Optimization - Bot SSO Adaptive Card C# Sample. #1522

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
// <copyright file="AdapterWithErrorHandler.cs" company="Microsoft">
// Copyright (c) Microsoft. All rights reserved.
// </copyright>

using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.Bot.Connector.Authentication;
Expand All @@ -12,23 +8,22 @@ namespace Microsoft.BotBuilderSamples
{
public class AdapterWithErrorHandler : CloudAdapter
{
public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger<IBotFrameworkHttpAdapter> logger, ConversationState conversationState = default)
// Constructor that initializes the bot framework authentication and logger.
public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger<IBotFrameworkHttpAdapter> logger)
: base(auth, logger)
{
// Define the error handling behavior during the bot's turn.
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}");
// Log the exception details for debugging and tracking errors.
logger.LogError(exception, $"[OnTurnError] unhandled error: {exception.Message}");

// Uncomment below commented line for local debugging.
// For development purposes, uncomment to provide a custom error message to users locally.
// await turnContext.SendActivityAsync($"Sorry, it looks like something went wrong. Exception Caught: {exception.Message}");

// Send a trace activity, which will be displayed in the Bot Framework Emulator
// Send a trace activity to the Bot Framework Emulator for deeper debugging.
await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError");
};
}
}
}
}
171 changes: 62 additions & 109 deletions samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@

namespace Microsoft.BotBuilderSamples
{
// This IBot implementation can run any type of Dialog. The use of type parameterization is to allows multiple different bots
// to be run at different endpoints within the same project. This can be achieved by defining distinct Controller types
// 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.
// This IBot implementation can run any type of Dialog.
// The use of type parameterization allows multiple bots to run at different endpoints within the same project.
// ConversationState is used by the Dialog system, while UserState may or may not be used in a Dialog implementation.
// All BotState objects should be saved at the end of a turn.
public class DialogBot<T> : TeamsActivityHandler where T : Dialog
{
protected readonly BotState _conversationState;
protected readonly Dialog _dialog;
protected readonly ILogger _logger;
protected readonly BotState _userState;
protected string _connectionName { get; }
protected readonly BotState _conversationState; // Represents the conversation state
protected readonly Dialog _dialog; // The dialog logic to run
protected readonly ILogger _logger; // Logger for debugging and tracing
protected readonly BotState _userState; // Represents the user state
protected string _connectionName { get; } // Connection name for OAuth

// Constructor to initialize the bot with necessary dependencies
public DialogBot(ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger, string connectionName)
{
_conversationState = conversationState;
Expand All @@ -43,69 +43,56 @@ public DialogBot(ConversationState conversationState, UserState userState, T dia
_connectionName = connectionName;
}

/// <summary>
/// Get sign in link
/// </summary>
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
// Get the sign-in link for OAuth
private async Task<string> GetSignInLinkAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
var userTokenClient = turnContext.TurnState.Get<UserTokenClient>();
var resource = await userTokenClient.GetSignInResourceAsync(_connectionName, turnContext.Activity as Activity, null, cancellationToken).ConfigureAwait(false);
return resource.SignInLink;
}

/// <summary>
/// Add logic to apply after the type-specific logic after the call to the base class method.
/// </summary>
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
// OnTurnAsync: Handles parallel saving of conversation and user state changes
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);

// Save any state changes that might have occurred during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
// Save any state changes in parallel to improve performance
await Task.WhenAll(
_conversationState.SaveChangesAsync(turnContext, false, cancellationToken),
_userState.SaveChangesAsync(turnContext, false, cancellationToken)
);
}

/// <summary>
/// Override this in a derived class to provide logic specific to Message activities, such as the conversational logic.
/// </summary>
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
// Simplified message activity handling to trigger appropriate adaptive card based on the message command
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
var signInLink = await GetSignInLinkAsync(turnContext, cancellationToken).ConfigureAwait(false);
if (turnContext.Activity.Text.Contains("login"))
await HandleCommandAsync(turnContext.Activity.Text, turnContext, signInLink, cancellationToken);
}

// Helper function to handle commands and send the appropriate adaptive card
private async Task HandleCommandAsync(string command, ITurnContext<IMessageActivity> turnContext, string signInLink, CancellationToken cancellationToken)
{
var commandToFileMap = new Dictionary<string, string>
{
string[] path = { ".", "Resources", "options.json" };
var member = await TeamsInfo.GetMemberAsync(turnContext, turnContext.Activity.From.Id, cancellationToken);
var initialAdaptiveCard = GetAdaptiveCardFromFileName(path, signInLink, turnContext.Activity.From.Name, member.Id);
await turnContext.SendActivityAsync(MessageFactory.Attachment(initialAdaptiveCard), cancellationToken);
}
else if (turnContext.Activity.Text.Contains("PerformSSO"))
{ "login", "options.json" },
{ "PerformSSO", "AdaptiveCardWithSSOInRefresh.json" }
};

if (commandToFileMap.ContainsKey(command))
{
string[] path = { ".", "Resources", "AdaptiveCardWithSSOInRefresh.json" };
string[] path = { ".", "Resources", commandToFileMap[command] };
var member = await TeamsInfo.GetMemberAsync(turnContext, turnContext.Activity.From.Id, cancellationToken);
var initialAdaptiveCard = GetAdaptiveCardFromFileName(path, signInLink, turnContext.Activity.From.Name, member.Id);
await turnContext.SendActivityAsync(MessageFactory.Attachment(initialAdaptiveCard), cancellationToken);
var adaptiveCard = GetAdaptiveCardFromFileName(path, signInLink, turnContext.Activity.From.Name, member.Id);
await turnContext.SendActivityAsync(MessageFactory.Attachment(adaptiveCard), cancellationToken);
}
else
{
await turnContext.SendActivityAsync(MessageFactory.Text("Please send 'login' for options"), cancellationToken);
}
}

/// <summary>
/// The OAuth Prompt needs to see the Invoke Activity in order to complete the login process.
/// </summary>
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
// Override to handle invoke activities, such as OAuth and adaptive card actions
protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
if (turnContext.Activity.Name == "adaptiveCard/action")
Expand All @@ -118,104 +105,77 @@ protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext
if (value["action"] == null)
return null;

JObject actiondata = JsonConvert.DeserializeObject<JObject>(value["action"].ToString());
JObject actionData = JsonConvert.DeserializeObject<JObject>(value["action"].ToString());

if (actiondata["verb"] == null)
if (actionData["verb"] == null)
return null;
string verb = actiondata["verb"].ToString();

string verb = actionData["verb"].ToString();

JObject authentication = null;
string state = null;

// When adaptiveCard/action invoke activity from teams contains token in response to sso flow from earlier invoke.
// Check for authentication token or state
if (value["authentication"] != null)
{
authentication = JsonConvert.DeserializeObject<JObject>(value["authentication"].ToString());
}

// When adaptiveCard/action invoke activity from teams contains 6 digit state in response to nominal sign in flow from bot.
string state = null;
if (value["state"] != null)
{
state = value["state"].ToString();
}

// authToken and state are absent, handle verb
// Token and state are absent, initiate SSO
if (authentication == null && state == null)
{
switch (verb)
{ // when token is absent in the invoke. We can initiate SSO in response to the invoke
case "initiateSSO":
return await initiateSSOAsync(turnContext, cancellationToken);
if (verb == "initiateSSO")
{
return await InitiateSSOAsync(turnContext, cancellationToken);
}
}
else
{
return createAdaptiveCardInvokeResponseAsync(authentication, state);
return CreateAdaptiveCardInvokeResponseAsync(authentication, state);
}
}

return null;
}

/// <summary>
/// Authentication success.
/// AuthToken or state is present. Verify token/state in invoke payload and return AC response
/// </summary>
/// <param name="authentication">authToken are absent, handle verb</param>
/// <param name="state">state are absent, handle verb</param>
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <param name="isBasicRefresh">Refresh type</param>
/// <param name="fileName">AdaptiveCardResponse.json</param>
/// <returns>A task that represents the work queued to execute.</returns>
private InvokeResponse createAdaptiveCardInvokeResponseAsync(JObject authentication, string state, bool isBasicRefresh = false, string fileName = "AdaptiveCardResponse.json")
// Method to create adaptive card invoke response with dynamic data
private InvokeResponse CreateAdaptiveCardInvokeResponseAsync(JObject authentication, string state, bool isBasicRefresh = false, string fileName = "AdaptiveCardResponse.json")
{
// Verify token is present or not.

bool isTokenPresent = authentication != null ? true : false;
bool isStatePresent = state != null && state != "" ? true : false;

string[] filepath = { ".", "Resources", fileName };
string authResultData = (authentication != null) ? "SSO success" : (state != null && state != "") ? "OAuth success" : "SSO/OAuth failed";

var adaptiveCardJson = File.ReadAllText(Path.Combine(filepath));
AdaptiveCardTemplate template = new AdaptiveCardTemplate(adaptiveCardJson);
var authResultData = isTokenPresent ? "SSO success" : isStatePresent ? "OAuth success" : "SSO/OAuth failed";

if (isBasicRefresh)
{
authResultData = "Refresh done";
}

var payloadData = new
{
authResult = authResultData,
};

var cardJsonstring = template.Expand(payloadData);
string[] filePath = { ".", "Resources", fileName };
var adaptiveCardJson = File.ReadAllText(Path.Combine(filePath));
AdaptiveCardTemplate template = new AdaptiveCardTemplate(adaptiveCardJson);
var payloadData = new { authResult = authResultData };
var cardJsonString = template.Expand(payloadData);

var adaptiveCardResponse = new AdaptiveCardInvokeResponse()
{
StatusCode = 200,
Type = AdaptiveCard.ContentType,
Value = JsonConvert.DeserializeObject(cardJsonstring)
Value = JsonConvert.DeserializeObject(cardJsonString)
};

return CreateInvokeResponse(adaptiveCardResponse);
}

/// <summary>
/// when token is absent in the invoke. We can initiate SSO in response to the invoke
/// </summary>
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
private async Task<InvokeResponse> initiateSSOAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
// Method to initiate SSO flow by sending OAuth card
private async Task<InvokeResponse> InitiateSSOAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
var signInLink = await GetSignInLinkAsync(turnContext, cancellationToken).ConfigureAwait(false);
var oAuthCard = new OAuthCard
{
Text = "Signin Text",
Text = "Please sign in",
ConnectionName = _connectionName,
TokenExchangeResource = new TokenExchangeResource
{
Expand All @@ -227,7 +187,7 @@ private async Task<InvokeResponse> initiateSSOAsync(ITurnContext<IInvokeActivity
{
Type = ActionTypes.Signin,
Value = signInLink,
Title = "Please sign in",
Title = "Sign In",
},
}
};
Expand All @@ -242,14 +202,7 @@ private async Task<InvokeResponse> initiateSSOAsync(ITurnContext<IInvokeActivity
return CreateInvokeResponse(loginReqResponse);
}

/// <summary>
/// Get Adaptive Card
/// </summary>
/// <param name="filepath">json path</param>
/// <param name="signInLink">Get sign in link</param>
/// <param name="name">createdBy</param>
/// <param name="userMRI">createdById</param>
/// <returns></returns>
// Method to retrieve adaptive card from a file and expand with dynamic data
private Attachment GetAdaptiveCardFromFileName(string[] filepath, string signInLink, string name = null, string userMRI = null)
{
var adaptiveCardJson = File.ReadAllText(Path.Combine(filepath));
Expand All @@ -259,9 +212,9 @@ private Attachment GetAdaptiveCardFromFileName(string[] filepath, string signInL
createdById = userMRI,
createdBy = name
};
var cardJsonstring = template.Expand(payloadData);
var card = JsonConvert.DeserializeObject<JObject>(cardJsonstring);

var cardJsonString = template.Expand(payloadData);
var card = JsonConvert.DeserializeObject<JObject>(cardJsonString);
var adaptiveCardAttachment = new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Expand Down
Loading