diff --git a/samples/18.bot-authentication/README.md b/samples/18.bot-authentication/README.md new file mode 100644 index 000000000..2902756f5 --- /dev/null +++ b/samples/18.bot-authentication/README.md @@ -0,0 +1,56 @@ +# Bot Authentication + +Bot Framework v4 bot authentication sample + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to use authentication in your bot using OAuth. + +The sample uses the bot authentication capabilities in [Azure Bot Service](https://docs.botframework.com), providing features to make it easier to develop a bot that authenticates users to various identity providers such as Azure AD (Azure Active Directory), GitHub, Uber, etc. + +NOTE: Microsoft Teams currently differs slightly in the way auth is integrated with the bot. Refer to sample 46.teams-auth. + +## Running the sample +- Clone the repository +```bash +git clone https://github.com/Microsoft/botbuilder-python.git +``` +- Activate your desired virtual environment +- Bring up a terminal, navigate to `botbuilder-python\samples\18.bot-authentication` folder +- In the terminal, type `pip install -r requirements.txt` +- Deploy your bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) +- [Add Authentication to your bot via Azure Bot Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication?view=azure-bot-service-4.0&tabs=csharp) +- Modify `APP_ID`, `APP_PASSWORD`, and `CONNECTION_NAME` in `config.py` + +After Authentication has been configured via Azure Bot Service, you can test the bot. + +- In the terminal, type `python app.py` + +## Testing the bot using Bot Framework Emulator + +[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.3.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` +- Enter the app id and password + +## Authentication + +This sample uses bot authentication capabilities in Azure Bot Service, providing features to make it easier to develop a bot that authenticates users to various identity providers such as Azure AD (Azure Active Directory), GitHub, Uber, etc. These updates also take steps towards an improved user experience by eliminating the magic code verification for some clients. + +## Deploy the bot 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. + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Azure Portal](https://portal.azure.com) +- [Add Authentication to Your Bot Via Azure Bot Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication?view=azure-bot-service-4.0&tabs=csharp) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) diff --git a/samples/18.bot-authentication/app.py b/samples/18.bot-authentication/app.py new file mode 100644 index 000000000..70d9e8334 --- /dev/null +++ b/samples/18.bot-authentication/app.py @@ -0,0 +1,102 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio +import sys +from datetime import datetime +from types import MethodType + +from flask import Flask, request, Response +from botbuilder.core import ( + BotFrameworkAdapter, + BotFrameworkAdapterSettings, + ConversationState, + MemoryStorage, + TurnContext, + UserState, +) +from botbuilder.schema import Activity, ActivityTypes + +from bots import AuthBot + +# Create the loop and Flask app +from dialogs import MainDialog + +LOOP = asyncio.get_event_loop() +app = Flask(__name__, instance_relative_config=True) +app.config.from_object("config.DefaultConfig") + +# Create adapter. +# See https://aka.ms/about-bot-adapter to learn more about how bots work. +SETTINGS = BotFrameworkAdapterSettings(app.config["APP_ID"], app.config["APP_PASSWORD"]) +ADAPTER = BotFrameworkAdapter(SETTINGS) + + +# Catch-all for errors. +async def on_error(self, context: TurnContext, error: Exception): + # This check writes out errors to console log .vs. app insights. + # NOTE: In production environment, you should consider logging this to Azure + # application insights. + print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) + + # Send a message to the user + await context.send_activity("The bot encountered an error or bug.") + await context.send_activity("To continue to run this bot, please fix the bot source code.") + + # Send a trace activity if we're talking to the Bot Framework Emulator + if context.activity.channel_id == 'emulator': + # Create a trace activity that contains the error object + trace_activity = Activity( + label="TurnError", + name="on_turn_error Trace", + timestamp=datetime.utcnow(), + type=ActivityTypes.trace, + value=f"{error}", + value_type="https://www.botframework.com/schemas/error" + ) + # Send a trace activity, which will be displayed in Bot Framework Emulator + await context.send_activity(trace_activity) + +ADAPTER.on_turn_error = MethodType(on_error, ADAPTER) + +# Create MemoryStorage and state +MEMORY = MemoryStorage() +USER_STATE = UserState(MEMORY) +CONVERSATION_STATE = ConversationState(MEMORY) + +# Create dialog +DIALOG = MainDialog(app.config["CONNECTION_NAME"]) + +# Create Bot +BOT = AuthBot(CONVERSATION_STATE, USER_STATE, DIALOG) + + +# Listen for incoming requests on /api/messages. +@app.route("/api/messages", methods=["POST"]) +def messages(): + # Main bot message handler. + if "application/json" in request.headers["Content-Type"]: + body = request.json + else: + return Response(status=415) + + activity = Activity().deserialize(body) + auth_header = ( + request.headers["Authorization"] if "Authorization" in request.headers else "" + ) + + try: + task = LOOP.create_task( + ADAPTER.process_activity(activity, auth_header, BOT.on_turn) + ) + LOOP.run_until_complete(task) + return Response(status=201) + except Exception as exception: + raise exception + + +if __name__ == "__main__": + try: + app.run(debug=False, port=app.config["PORT"]) # nosec debug + except Exception as exception: + raise exception diff --git a/samples/18.bot-authentication/bots/__init__.py b/samples/18.bot-authentication/bots/__init__.py new file mode 100644 index 000000000..d6506ffcb --- /dev/null +++ b/samples/18.bot-authentication/bots/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .dialog_bot import DialogBot +from .auth_bot import AuthBot + +__all__ = ["DialogBot", "AuthBot"] diff --git a/samples/18.bot-authentication/bots/auth_bot.py b/samples/18.bot-authentication/bots/auth_bot.py new file mode 100644 index 000000000..c1d1d936f --- /dev/null +++ b/samples/18.bot-authentication/bots/auth_bot.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import List +from botbuilder.core import ( + ConversationState, + UserState, + TurnContext, +) +from botbuilder.dialogs import Dialog +from botbuilder.schema import ChannelAccount + +from helpers.dialog_helper import DialogHelper +from .dialog_bot import DialogBot + + +class AuthBot(DialogBot): + def __init__( + self, + conversation_state: ConversationState, + user_state: UserState, + dialog: Dialog, + ): + super(AuthBot, self).__init__( + conversation_state, user_state, dialog + ) + + async def on_members_added_activity( + self, members_added: List[ChannelAccount], turn_context: TurnContext + ): + for member in members_added: + # Greet anyone that was not the target (recipient) of this message. + # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details. + if member.id != turn_context.activity.recipient.id: + await turn_context.send_activity("Welcome to AuthenticationBot. Type anything to get logged in. Type " + "'logout' to sign-out.") + + async def on_token_response_event( + self, turn_context: TurnContext + ): + # Run the Dialog with the new Token Response Event Activity. + await DialogHelper.run_dialog( + self.dialog, + turn_context, + self.conversation_state.create_property("DialogState"), + ) diff --git a/samples/18.bot-authentication/bots/dialog_bot.py b/samples/18.bot-authentication/bots/dialog_bot.py new file mode 100644 index 000000000..fc563d2ec --- /dev/null +++ b/samples/18.bot-authentication/bots/dialog_bot.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import asyncio + +from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext +from botbuilder.dialogs import Dialog +from helpers.dialog_helper import DialogHelper + + +class DialogBot(ActivityHandler): + def __init__( + self, + conversation_state: ConversationState, + user_state: UserState, + dialog: Dialog, + ): + if conversation_state is None: + raise Exception( + "[DialogBot]: Missing parameter. conversation_state is required" + ) + if user_state is None: + raise Exception("[DialogBot]: Missing parameter. user_state is required") + if dialog is None: + raise Exception("[DialogBot]: Missing parameter. dialog is required") + + self.conversation_state = conversation_state + self.user_state = user_state + self.dialog = dialog + + async def on_turn(self, turn_context: TurnContext): + await super().on_turn(turn_context) + + # Save any state changes that might have occurred during the turn. + await self.conversation_state.save_changes(turn_context, False) + await self.user_state.save_changes(turn_context, False) + + async def on_message_activity(self, turn_context: TurnContext): + await DialogHelper.run_dialog( + self.dialog, + turn_context, + self.conversation_state.create_property("DialogState"), + ) diff --git a/samples/18.bot-authentication/config.py b/samples/18.bot-authentication/config.py new file mode 100644 index 000000000..0acc113a3 --- /dev/null +++ b/samples/18.bot-authentication/config.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + +""" Bot Configuration """ + + +class DefaultConfig: + """ Bot Configuration """ + + PORT = 3978 + APP_ID = os.environ.get("MicrosoftAppId", "") + APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") + CONNECTION_NAME = os.environ.get("ConnectionName", "") diff --git a/samples/18.bot-authentication/deploymentTemplates/template-with-preexisting-rg.json b/samples/18.bot-authentication/deploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..bff8c096d --- /dev/null +++ b/samples/18.bot-authentication/deploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,242 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "publishingUsername": "[concat('$', parameters('newWebAppName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2016-09-01", + "name": "[variables('servicePlanName')]", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "kind": "linux", + "properties": { + "name": "[variables('servicePlanName')]", + "perSiteScaling": false, + "reserved": true, + "targetWorkerCount": 0, + "targetWorkerSizeId": 0 + } + }, + { + "comments": "Create a Web App using a Linux App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2016-08-01", + "name": "[variables('webAppName')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" + ], + "kind": "app,linux", + "properties": { + "enabled": true, + "hostNameSslStates": [ + { + "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Standard" + }, + { + "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]", + "sslState": "Disabled", + "hostType": "Repository" + } + ], + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]", + "reserved": true, + "scmSiteAlsoStopped": false, + "clientAffinityEnabled": false, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": false, + "siteConfig": { + "appSettings": [ + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + }, + { + "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", + "value": "true" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "type": "Microsoft.Web/sites/config", + "apiVersion": "2016-08-01", + "name": "[concat(variables('webAppName'), '/web')]", + "location": "[variables('resourcesLocation')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" + ], + "properties": { + "numberOfWorkers": 1, + "defaultDocuments": [ + "Default.htm", + "Default.html", + "Default.asp", + "index.htm", + "index.html", + "iisstart.htm", + "default.aspx", + "index.php", + "hostingstart.html" + ], + "netFrameworkVersion": "v4.0", + "phpVersion": "", + "pythonVersion": "", + "nodeVersion": "", + "linuxFxVersion": "PYTHON|3.7", + "requestTracingEnabled": false, + "remoteDebuggingEnabled": false, + "remoteDebuggingVersion": "VS2017", + "httpLoggingEnabled": true, + "logsDirectorySizeLimit": 35, + "detailedErrorLoggingEnabled": false, + "publishingUsername": "[variables('publishingUsername')]", + "scmType": "None", + "use32BitWorkerProcess": true, + "webSocketsEnabled": false, + "alwaysOn": false, + "appCommandLine": "", + "managedPipelineMode": "Integrated", + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": false, + "virtualDirectories": null + } + ], + "winAuthAdminState": 0, + "winAuthTenantState": 0, + "customAppPoolIdentityAdminState": false, + "customAppPoolIdentityTenantState": false, + "loadBalancing": "LeastRequests", + "routingRules": [], + "experiments": { + "rampUpRules": [] + }, + "autoHealEnabled": false, + "vnetName": "", + "minTlsVersion": "1.2", + "ftpsState": "AllAllowed", + "reservedInstanceCount": 0 + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/samples/18.bot-authentication/dialogs/__init__.py b/samples/18.bot-authentication/dialogs/__init__.py new file mode 100644 index 000000000..ab5189cd5 --- /dev/null +++ b/samples/18.bot-authentication/dialogs/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .logout_dialog import LogoutDialog +from .main_dialog import MainDialog + +__all__ = ["LogoutDialog", "MainDialog"] diff --git a/samples/18.bot-authentication/dialogs/logout_dialog.py b/samples/18.bot-authentication/dialogs/logout_dialog.py new file mode 100644 index 000000000..b8d420a40 --- /dev/null +++ b/samples/18.bot-authentication/dialogs/logout_dialog.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.dialogs import DialogTurnResult, ComponentDialog, DialogContext +from botbuilder.core import BotFrameworkAdapter +from botbuilder.schema import ActivityTypes + + +class LogoutDialog(ComponentDialog): + def __init__(self, dialog_id: str, connection_name: str): + super(LogoutDialog, self).__init__(dialog_id) + + self.connection_name = connection_name + + async def on_begin_dialog(self, inner_dc: DialogContext, options: object) -> DialogTurnResult: + return await inner_dc.begin_dialog(self.initial_dialog_id, options) + + async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult: + return await inner_dc.continue_dialog() + + async def _interrupt(self, inner_dc: DialogContext): + if inner_dc.context.activity.type == ActivityTypes.message: + text = inner_dc.context.activity.text.lower() + if text == "logout": + bot_adapter: BotFrameworkAdapter = inner_dc.context.adapter + await bot_adapter.sign_out_user(inner_dc.context, self.connection_name) + return await inner_dc.cancel_all_dialogs() diff --git a/samples/18.bot-authentication/dialogs/main_dialog.py b/samples/18.bot-authentication/dialogs/main_dialog.py new file mode 100644 index 000000000..3e7a80287 --- /dev/null +++ b/samples/18.bot-authentication/dialogs/main_dialog.py @@ -0,0 +1,83 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import MessageFactory +from botbuilder.dialogs import ( + WaterfallDialog, + WaterfallStepContext, + DialogTurnResult, + PromptOptions) +from botbuilder.dialogs.prompts import OAuthPrompt, OAuthPromptSettings, ConfirmPrompt + +from dialogs import LogoutDialog + + +class MainDialog(LogoutDialog): + def __init__( + self, connection_name: str + ): + super(MainDialog, self).__init__(MainDialog.__name__, connection_name) + + self.add_dialog( + OAuthPrompt( + OAuthPrompt.__name__, + OAuthPromptSettings( + connection_name=connection_name, + text="Please Sign In", + title="Sign In", + timeout=300000 + ) + ) + ) + + self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__)) + + self.add_dialog( + WaterfallDialog( + "WFDialog", [ + self.prompt_step, + self.login_step, + self.display_token_phase1, + self.display_token_phase2 + ] + ) + ) + + self.initial_dialog_id = "WFDialog" + + async def prompt_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + return await step_context.begin_dialog(OAuthPrompt.__name__) + + async def login_step(self, step_context: WaterfallStepContext) -> DialogTurnResult: + # Get the token from the previous step. Note that we could also have gotten the + # token directly from the prompt itself. There is an example of this in the next method. + if step_context.result: + await step_context.context.send_activity("You are now logged in.") + return await step_context.prompt(ConfirmPrompt.__name__, PromptOptions( + prompt=MessageFactory.text("Would you like to view your token?") + )) + + await step_context.context.send_activity("Login was not successful please try again.") + return await step_context.end_dialog() + + async def display_token_phase1(self, step_context: WaterfallStepContext) -> DialogTurnResult: + await step_context.context.send_activity("Thank you.") + + if step_context.result: + # Call the prompt again because we need the token. The reasons for this are: + # 1. If the user is already logged in we do not need to store the token locally in the bot and worry + # about refreshing it. We can always just call the prompt again to get the token. + # 2. We never know how long it will take a user to respond. By the time the + # user responds the token may have expired. The user would then be prompted to login again. + # + # There is no reason to store the token locally in the bot because we can always just call + # the OAuth prompt to get the token or get a new token if needed. + return await step_context.begin_dialog(OAuthPrompt.__name__) + + return await step_context.end_dialog() + + async def display_token_phase2(self, step_context: WaterfallStepContext) -> DialogTurnResult: + if step_context.result: + await step_context.context.send_activity(f"Here is your token {step_context.result['token']}") + + return await step_context.end_dialog() \ No newline at end of file diff --git a/samples/18.bot-authentication/helpers/__init__.py b/samples/18.bot-authentication/helpers/__init__.py new file mode 100644 index 000000000..a824eb8f4 --- /dev/null +++ b/samples/18.bot-authentication/helpers/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from . import dialog_helper + +__all__ = ["dialog_helper"] diff --git a/samples/18.bot-authentication/helpers/dialog_helper.py b/samples/18.bot-authentication/helpers/dialog_helper.py new file mode 100644 index 000000000..6b2646b0b --- /dev/null +++ b/samples/18.bot-authentication/helpers/dialog_helper.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from botbuilder.core import StatePropertyAccessor, TurnContext +from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus + + +class DialogHelper: + @staticmethod + async def run_dialog( + dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor + ): + dialog_set = DialogSet(accessor) + dialog_set.add(dialog) + + dialog_context = await dialog_set.create_context(turn_context) + results = await dialog_context.continue_dialog() + if results.status == DialogTurnStatus.Empty: + await dialog_context.begin_dialog(dialog.id) diff --git a/samples/18.bot-authentication/requirements.txt b/samples/18.bot-authentication/requirements.txt new file mode 100644 index 000000000..7e54b62ec --- /dev/null +++ b/samples/18.bot-authentication/requirements.txt @@ -0,0 +1,2 @@ +botbuilder-core>=4.4.0b1 +flask>=1.0.3