Skip to content

Commit

Permalink
Adds dotnet SSO sample using CloudAdapter (#3673)
Browse files Browse the repository at this point in the history
* Added dotnet Skills with SSO and CloudAdapter sample

* Updated readme

* Renamed and updates skill manifest.
Renamed event name to SSO

* Updated code style and cleaned up some usings.

* Updated readme.

* Updated samples directory in main readme
  • Loading branch information
gabog authored Jan 14, 2022
1 parent 95f2fa8 commit 3d60289
Show file tree
Hide file tree
Showing 39 changed files with 3,670 additions and 9 deletions.
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,21 @@ To use the samples, clone this GitHub repository using Git.

Samples are designed to illustrate functionality you'll need to implement to build great bots!

- [Bot essentials](#bot-essentials)
- [Advanced bots](#advanced-bots)
- [Authentication samples](#authentication-samples)
- [QnA Maker samples](#qna-maker-samples)
- [Teams samples](#teams-samples)
- [Skills samples](#skills-samples)
- [Custom adapter samples](#custom-adapter-samples)
- [Experimental / preview samples](#experimental--preview-samples)
- [!Bot Framework Samples](#)
- [Click here to find out what's new with Bot Framework](#click-here-to-find-out-whats-new-with-bot-framework)
- [Overview](#overview)
- [Getting the samples](#getting-the-samples)
- [Sample lists](#sample-lists)
- [Bot essentials](#bot-essentials)
- [Advanced bots](#advanced-bots)
- [Authentication samples](#authentication-samples)
- [QnA Maker samples](#qna-maker-samples)
- [Teams samples](#teams-samples)
- [Custom adapter samples](#custom-adapter-samples)
- [Skills samples](#skills-samples)
- [Experimental / preview samples](#experimental--preview-samples)
- [Contributing](#contributing)
- [Reporting security issues](#reporting-security-issues)

### Bot essentials

Expand Down Expand Up @@ -108,7 +115,7 @@ Samples are designed to illustrate functionality you'll need to implement to bui
|:--:|:----------------------|:---------------------------------------------------------------------------------|:--------|:-------------|:--------|:--------
|80|Skills - simple bot to bot | This sample shows how to connect a skill to a skill consumer. | [.NET Core][cs#80] | [JavaScript][js#80] |[Python][py#80] |[Java][java#80]
|81|Skills - skill dialog | This sample shows how to connect a skill to a skill dialog consumer.| [.NET Core][cs#81] | [JavaScript][js#81] |[Python][py#81] |[Java][java#81]

|82|Skills - SSO with CloudAdapter | This sample shows how use SSO with skills and CloudAdapter.| [.NET Core][cs#82] | NA |NA |NA
### Experimental / preview samples

A [collection of **experimental** samples](./experimental) exist, intended to provide samples for features currently in preview or as a way to solicit feedback on a given design, approach, or technology being considered by the Bot Framework Team.
Expand Down Expand Up @@ -157,6 +164,7 @@ A [collection of **experimental** samples](./experimental) exist, intended to pr
[cs#63]:samples/csharp_dotnetcore/63.twilio-adapter
[cs#80]:samples/csharp_dotnetcore/80.skills-simple-bot-to-bot
[cs#81]:samples/csharp_dotnetcore/81.skills-skilldialog
[cs#82]:samples/csharp_dotnetcore/82.skills-sso-cloudadapter

[wa#13]:samples/csharp_webapi/13.core-bot

Expand Down
70 changes: 70 additions & 0 deletions samples/csharp_dotnetcore/82.skills-sso-cloudadapter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# SSO with Skills Sample

Bot Framework v4 skills SSO sample.

This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple root bot that sends message activities to a skill bot that echoes it back.

## Prerequisites

- [.NET Core SDK](https://dotnet.microsoft.com/download) version 3.1

```bash
# determine dotnet version
dotnet --version
```

## Key concepts in this sample

The solution includes a parent bot ([`rootBot`](RootBot/Bots/RootBot.cs)) and a skill bot ([`skillBot`](SkillBot/Bots/SkillBot.cs)) and shows how the skill bot can accept OAuth credentials from the root bot, without needing to send it's own OAuthPrompt.
This is the general authentication flow:
1. Root bot prompts user to authenticate with an OAuth prompt card.
2. Authentication succeeds and the user is granted a token.
3. User performs and action on the skill bot that requires authentication.
4. The skill bot sends an OAuth prompt card to the root bot.
5. The root bot intercepts the OAuth prompt card, aware that the user is already authenticated and that the user should authenticate with the skill via SSO.
6. Instead of showing the OAuth prompt card to the user, the root bot sends a token exchange request invoke activity along with the token to the skill.
7. The skill's OAuth prompt receives the token exchange request and uses the token from the root bot to continue authenticating.

## To try this sample

- Clone the repository

```bash
git clone https://github.com/microsoft/botbuilder-samples.git
```

- Create a bot registration in the azure portal for the **SkillBot** and update [appsettings.json](SkillBot/appsettings.json) with the `MicrosoftAppId` and `MicrosoftAppPassword` of the new bot registration.
- Update the `BotFrameworkSkills` section in the **RootBot** [appsettings.json](RootBot/appsettings.json) with the app ID for the skill you created in the previous step.
- Create a bot registration in the azure portal for the **RootBot** and update [appsettings.json](RootBot/appsettings.json) with the `MicrosoftAppId` and `MicrosoftAppPassword` of the new bot registration.
- Add the RootBot `MicrosoftAppId` to the `AllowedCallers` list in the **SkillBot** [appsettings.json](SkillBot/appsettings.json).
- Create and configure an OAuth connection for **RootBot**:
1. Create an Azure Active Directory V2 application for the root bot following the steps described in [Create the Azure AD identity for RootBot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication-sso?view=azure-bot-service-4.0&tabs=csharp%2Ceml#create-the-azure-ad-identity-for-rootbot)
1. Open the **RootBot** registration in the Azure portal, navigate to the Configuration tab and add a new OAuth Connection Settings using the settings of the app you created in the previous step as described in [Create an OAuth connection for a root bot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication-sso?view=azure-bot-service-4.0&tabs=csharp%2Ceml#create-an-oauth-connection-settings)
1. Update the **RootBot** [appsettings.json](SkillBot/appsettings.json) `ConnectionName` property with the name of the connection you created in the previous step
- Create and configure an OAuth connection for **SkillBot**:
1. Create an Azure Active Directory V2 application for the skill following the steps described in [Create the Azure AD identity for SkillBot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication-sso?view=azure-bot-service-4.0&tabs=csharp%2Ceml#create-the-azure-ad-identity-for-skillbot)
2. Open the **SkillBot** registration in the Azure portal, navigate to the Configuration tab and add a new OAuth Connection Settings using the settings of the app you created in the previous step as described in [Create an OAuth connection for a skill](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication-sso?view=azure-bot-service-4.0&tabs=csharp%2Ceml#create-an-oauth-connection-settings-1)
3. Update the **SkillBot** [appsettings.json](SkillBot/appsettings.json) `ConnectionName` property with the name of the connection you created in the previous step
- Open the `SkillsSSOCloudAdapter.sln` solution and configure it to [start debugging with multiple processes](https://docs.microsoft.com/en-us/visualstudio/debugger/debug-multiple-processes?view=vs-2019#start-debugging-with-multiple-processes)

**Note:** leave the `MicrosoftAppType` and `MicrosoftAppTenantId` empty to try this example, see the [Implement a skill](https://docs.microsoft.com/en-us/azure/bot-service/skill-implement-skill?view=azure-bot-service-4.0&tabs=cs) article for additional information on what authentication types are supported for skills.

## Testing the bot using the Bot Framework Emulator

The [Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.

- Install the Bot Framework Emulator version 4.14.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)

### Connect to the bot using Bot Framework Emulator

- Launch Bot Framework Emulator
- File -> Open Bot.
- Enter a Bot URL of `http://localhost:3978/api/messages`, the `MicrosoftAppId` and `MicrosoftAppPassword` for the `RootBot`.
- Click `Connect`.
- Follow the prompts to initiate the token exchange between the `SkillBot` and `RootBot`, resulting in a valid token displayed.

## Deploy the bots to Azure

To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// // Copyright (c) Microsoft Corporation. All rights reserved.
// // Licensed under the MIT License.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Builder.Skills;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
using Microsoft.BotBuilderSamples.RootBot.Dialogs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace Microsoft.BotBuilderSamples.RootBot
{
public class AdapterWithErrorHandler : CloudAdapter
{
private readonly BotFrameworkAuthentication _auth;
private readonly IConfiguration _configuration;
private readonly ConversationState _conversationState;
private readonly ILogger _logger;
private readonly SkillsConfiguration _skillsConfig;

public AdapterWithErrorHandler(BotFrameworkAuthentication auth, IConfiguration configuration, ILogger<IBotFrameworkHttpAdapter> logger, ConversationState conversationState, SkillsConfiguration skillsConfig = null)
: base(auth, logger)
{
_auth = auth ?? throw new ArgumentNullException(nameof(auth));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_conversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_skillsConfig = skillsConfig;

OnTurnError = HandleTurnError;
}

private async Task HandleTurnError(ITurnContext turnContext, Exception 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}");

await SendErrorMessageAsync(turnContext, exception);
await EndSkillConversationAsync(turnContext);
await ClearConversationStateAsync(turnContext);
}

private async Task SendErrorMessageAsync(ITurnContext turnContext, Exception exception)
{
try
{
// Send a message to the user.
var errorMessageText = "The bot encountered an error or bug.";
var errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.IgnoringInput);
await turnContext.SendActivityAsync(errorMessage);

errorMessageText = "To continue to run this bot, please fix the bot source code.";
errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.ExpectingInput);
await turnContext.SendActivityAsync(errorMessage);

// Send a trace activity, which will be displayed in the Bot Framework Emulator.
await turnContext.TraceActivityAsync("OnTurnError Trace", exception.ToString(), "https://www.botframework.com/schemas/error", "TurnError");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Exception caught in SendErrorMessageAsync : {ex}");
}
}

private async Task EndSkillConversationAsync(ITurnContext turnContext)
{
if (_skillsConfig == null)
{
return;
}

try
{
// Inform the active skill that the conversation is ended so that it has a chance to clean up.
// Note: the root bot manages the ActiveSkillPropertyName, which has a value while the root bot
// has an active conversation with a skill.
var activeSkill = await _conversationState.CreateProperty<BotFrameworkSkill>(MainDialog.ActiveSkillPropertyName).GetAsync(turnContext, () => null);
if (activeSkill != null)
{
var botId = _configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value;

var endOfConversation = Activity.CreateEndOfConversationActivity();
endOfConversation.Code = "RootSkillError";
endOfConversation.ApplyConversationReference(turnContext.Activity.GetConversationReference(), true);

await _conversationState.SaveChangesAsync(turnContext, true);

using var client = _auth.CreateBotFrameworkClient();

await client.PostActivityAsync(botId, activeSkill.AppId, activeSkill.SkillEndpoint, _skillsConfig.SkillHostEndpoint, endOfConversation.Conversation.Id, (Activity)endOfConversation, CancellationToken.None);
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Exception caught on attempting to send EndOfConversation : {ex}");
}
}

private async Task ClearConversationStateAsync(ITurnContext turnContext)
{
try
{
// Delete the conversationState for the current conversation to prevent the
// bot from getting stuck in a error-loop caused by being in a bad state.
// ConversationState should be thought of as similar to "cookie-state" for a Web page.
await _conversationState.DeleteAsync(turnContext);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Exception caught on attempting to Delete ConversationState : {ex}");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;

namespace Microsoft.BotBuilderSamples.RootBot.Bots
{
public class RootBot<T> : ActivityHandler
where T : Dialog
{
private readonly ConversationState _conversationState;
private readonly Dialog _mainDialog;

public RootBot(ConversationState conversationState, T dialog)
{
_conversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
_mainDialog = dialog ?? throw new ArgumentNullException(nameof(dialog));
}

public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
if (turnContext.Activity.Type != ActivityTypes.ConversationUpdate)
{
// Run the Dialog with the Activity.
await _mainDialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
}
else
{
// Let the base class handle the activity.
await base.OnTurnAsync(turnContext, cancellationToken);
}

// Save any state changes that might have occurred during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}

protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var member in membersAdded)
{
// Greet anyone that was not the target (recipient) of this message.
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Hello and welcome!"), cancellationToken);
await _mainDialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;

namespace Microsoft.BotBuilderSamples.RootBot.Controllers
{
// This ASP Controller is created to handle a request. Dependency injection will provide the Adapter and IBot
// implementation at runtime. Multiple different IBot implementations running at different endpoints can be
// achieved by specifying a more specific type for the bot constructor argument.
[Route("api/messages")]
[ApiController]
public class BotController : ControllerBase
{
private readonly IBotFrameworkHttpAdapter _adapter;
private readonly IBot _bot;

public BotController(IBotFrameworkHttpAdapter adapter, IBot bot)
{
_adapter = adapter;
_bot = bot;
}

[HttpPost]
[HttpGet]
public async Task PostAsync()
{
// Delegate the processing of the HTTP POST to the adapter.
// The adapter will invoke the bot.
await _adapter.ProcessAsync(Request, Response, _bot);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Builder.Skills;

namespace Microsoft.BotBuilderSamples.RootBot.Controllers
{
/// <summary>
/// A controller that handles skill replies to the bot.
/// This example uses the <see cref="CloudSkillHandler"/> that is registered as a <see cref="ChannelServiceHandlerBase"/> in startup.cs.
/// </summary>
[ApiController]
[Route("api/skills")]
public class SkillController : ChannelServiceController
{
public SkillController(ChannelServiceHandlerBase handler)
: base(handler)
{
}
}
}
Loading

0 comments on commit 3d60289

Please sign in to comment.