-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds dotnet SSO sample using CloudAdapter (#3673)
* 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
Showing
39 changed files
with
3,670 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
samples/csharp_dotnetcore/82.skills-sso-cloudadapter/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
123 changes: 123 additions & 0 deletions
123
samples/csharp_dotnetcore/82.skills-sso-cloudadapter/RootBot/AdapterWithErrorHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}"); | ||
} | ||
} | ||
} | ||
} |
56 changes: 56 additions & 0 deletions
56
samples/csharp_dotnetcore/82.skills-sso-cloudadapter/RootBot/Bots/RootBot.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
samples/csharp_dotnetcore/82.skills-sso-cloudadapter/RootBot/Controllers/BotController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
samples/csharp_dotnetcore/82.skills-sso-cloudadapter/RootBot/Controllers/SkillController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
{ | ||
} | ||
} | ||
} |
Oops, something went wrong.