Merge pull request #1590 from DingmaomaoBJTU/react-bot-framework-sso

This commit is contained in:
Hugo Bernier 2020-11-07 15:38:45 -05:00 committed by GitHub
commit 734d317598
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 27770 additions and 0 deletions

View File

@ -0,0 +1,343 @@
# SharePoint webpart sample with SSO
## Summary
[Web parts](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/overview-client-side-web-parts) is a special kind of SharePoint controls that can be supported by the [Bot Framework](https://dev.botframework.com). This sample will show you how to embed a Bot Framework bot into a SharePoint web site with SSO.
There are two parts included in this sample:
1. A login bot sample
1. A web parts sample
The web parts embeds the login bot by using a webchat. As the user has already login in the SharePoint website, we could use [SSO](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication-sso?view=azure-bot-service-4.0&tabs=csharp%2Ceml) to authorize the bot. This sample shows how to do this including:
- Detect and intercept OAuth process.
- Use SharePoint library to get the exchange token and send it back to the bot.
This demo does not include any threat models and is designed for educational purposes only. When you design a production system, threat-modelling is an important task to make sure your system is secure and provide a way to quickly identify potential source of data breaches. IETF [RFC 6819](https://tools.ietf.org/html/rfc6819) and [OAuth 2.0 for Browser-Based Apps](https://tools.ietf.org/html/draft-ietf-oauth-browser-based-apps-01#section-9) is a good starting point for threat-modelling when using OAuth 2.0.
![demo](assets/sp-wp-sso.gif)
## Used SharePoint Framework Version
![SPFx 1.10.0](https://img.shields.io/badge/version-1.10.0-green.svg)
## Applies to
* [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Office 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
* [Microsoft Bot Framework](http://dev.botframework.com)
## Prerequisites
- [Node.js](https://nodejs.org) version 10.19 (Node.js v9.x, v11.x, and v12.x are not currently supported with SharePoint Framework development)
```bash
# determine node version
node --version
```
- [python](https://www.python.org/) version 2.7
```bash
# determine python version
python --version
```
## Solution
Solution|Author(s)
--------|---------
webpart | STCA BF Channel and ABS (stcabfchannel@microsoft.com) <br/> Stephan Bisser (@stephanbisser, bisser.io)
bot | STCA BF Channel and ABS (stcabfchannel@microsoft.com)
## Version history
Version|Date|Comments
-------|----|--------
1.0|Nov 10, 2020|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
### Enlist
- Clone the repository
```bash
git clone https://github.com/pnp/sp-dev-fx-webparts.git
```
### [Setup bot with Direct Line](./bot/README.md)
- In a terminal, navigate to `sp-dev-fx-webparts`
```bash
cd sp-dev-fx-webparts
```
- Navigate to the folder for this solution:
```base
cd samples
cd react-bot-framework-sso
```
- Install modules
```bash
npm install
```
- Register connections. You can get it done by [deploy your bot to Azure](https://aka.ms/azuredeployment). Save your bot service endpoint like: "https://YOUR_BOT.azurewebsites.net". Save your AAD Id as `YOUR_APP_ID`, AAD Name as `YOUR_APP_Name` and secret as `YOUR_APP_PSW` also.
- [Connect to direct line](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-channel-connect-directline?view=azure-bot-service-4.0), copy one of the Secret Key values as YOUR_DIRECT_LINE_SECRET and store this for later. This is your Direct Line Secret.
- Add `DirectLineSecret` to an `.env` config file under `./bot`
```bash
MicrosoftAppId=YOUR_APP_ID
MicrosoftAppPassword=YOUR_APP_PSW
DirectLineSecret=YOUR_DIRECT_LINE_SECRET
```
### Setup OAuth via Azure Active Directory for the Bot
[Check here](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication?view=azure-bot-service-4.0&tabs=csharp) for more information about getting an AADv2
application setup for use in Azure Bot Service.
- Go to your [Azure Active Directory](https://ms.portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Overview)
- Open your bot's application registration
- Save the tenant ID
- Select the "Overview" blade
- On the main pane, copy the content of "Directory (tenant) ID" as `YOUR_APP_TENANT` and store this for later
- Update Authentication
- Select the "Authentication" blade
- Click "Add a platform" to add web if Web is not added
- In "Redirect URIs" section, add a new entry `https://token.botframework.com/.auth/web/redirect`
- Update App Registration Manifest
- Select the "Manifest" blade
- Set `accessTokenAcceptedVersion` to `2`
- Add a scope
- Select the "Expose an API" blade
- Click the "Add a scope" button under "Scopes defined by this API"
- Click "Save and continue"
- Add a scope named `YOUR_AAD_SCOPE_NAME`
- Set "Who can consent?" to "Admins and users"
- Add an admin consent display name
- Add an admin consent description
- Click "Add scope"
- Save the Scope URL to configure authentication for the bot in the Bot Registration in the next section
- api://123a45b6-789c-01de-f23g-h4ij5k67a8bc/<YOUR_AAD_SCOPE_NAME>
- Select API permissions
- Click "API Permissions", select"Add a permission"
- Select "My APIs", `YOUR_APP_ID`, and enable `YOUR_AAD_SCOPE_NAME` scope `\`
Otherwise the non-admin user cannot use SSO.
### Setup Authentication via Azure Bot Services for the Bot
Check the [Add authentication to your bot via Azure Bot Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-tutorial-authentication?view=azure-bot-service-3.0&tabs=aadv1) article for more information about adding authentication to your bot.
- Add OAuth Connection for the bot to the Bot Channel Registration
- In the Bot Channel Registration open the settings blade
- Under "OAuth Connection Settings" click the "Add Setting" button
- Add new Connection Setting
- Set the connection "Name"
- Set the "Service Provider" to "Azure Active Directory 2"
- Add the Client ID as `YOUR_APP_ID`, Client secret as `YOUR_APP_PSW`, and Tenant ID as `YOUR_APP_TENANT`
- Set the "Token Exchange URL" to the scope URL that was created in the previous section
- api://123a45b6-789c-01de-f23g-h4ij5k67a8bc/<YOUR_AAD_SCOPE_NAME>
- Set the "Scopes" field to the scopes you want the bot to have permission to access (ie. `user.read`)
### Republish bot
- Update `.env` with `ConnectionName` to `.env` config file under `./bot`
```bash
ConnectionName=YOUR_CONNECTION_NAME
```
- Republish your bot with new config or restart it in local use:
```bash
npm start
```
### [Setup web parts](./webpart/README.md)
- Edit `BotSignInToast.tsx` to set your AAD scope uri(`scopeUri`) with `api://YOUR_APP_ID` directly like `api://123a45b6-789c-01de-f23g-h4ij5k67a8bc`:
```ts
return tokenProvider.getToken(scopeUri, true).then((token: string) => {
```
- Add the following config to `./config/package-solution.json`:
```diff
"webApiPermissionRequests": [
+ {
+ "resource": "<YOUR_APP_Name>",
+ "scope": "<YOUR_AAD_SCOPE_NAME>"
+ }
],
```
- [Publish and host webpart](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/hosting-webpart-from-office-365-cdn), prepare for approving permissions
- Refer [Connect to Azure AD-secured APIs](https://docs.microsoft.com/en-us/sharepoint/api-access) to publish and approve permissions from admin site
- Go to SharePoint admin center
- Find "API Access", approve "<YOUR_APP_Name>"
- (Opt. for hosted bot service) Config CORS \
[CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) must be set on bot app service to enable SharePoint client to get resource from bot service. Follow these steps to add your workbench to bot app service CORS configuration:
1. Go to your azure portal
2. Navigate to your bot app service, search for CORS settings
3. Add https://localhost:4321 and https://<YOUR_SITE>.sharepoint.com to CORS origins
- In the command line run
```bash
cd ../webpart
npm install
gulp serve
```
Now web parts is running locally.
- Open online test page with user account: https://<YOUR_SITE>.sharepoint.com/_layouts/15/Workbench.aspx
- Config bot endpoint \
Add the web parts, set bot endpoint to https://YOUR_BOT.azurewebsites.net, refresh this page, then you can successfully connect bot with SharePoint.
### Setup OAuth via Azure Active Directory for the SharePoint
The following operations will need an admin account.
- Go to your [Azure Active Directory](https://ms.portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Overview)
- Open "App registrations", find "SharePoint Online Client Extensibility Web Application Principal"
- Save the client ID
- Select the "Overview" blade
- On the main pane, copy the content of "Application ID" as YOUR_SHAREPOINT_ID and store this for later usage
- Update App Registration Manifest
- Select the "Manifest" blade
- Set `accessTokenAcceptedVersion` to `2`
### Add a client application to the OAuth for the Bot
- Open your bot's application registration
- Select the "Expose an API" blade
- Click the "Add a client application" under "Authorized client applications"
- Set the client id to the `YOUR_SHAREPOINT_ID`
- Check the box next to the scope we added in the previous step under "Authorized scopes"
- Click "Add application"
## Features
**Web Chat integration with SSO**
First a store is used to intercept activities with OAuth card. When the client receives an OAuth request, the client will ask the user if it should use SSO to login instead.
```ts
const store = useMemo(
() =>
createStore({}, ({ dispatch }) => next => action => {
if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY' && action.payload.activity.from.role === 'bot') {
const activity =
(action.payload.activity.attachments || []).find(
({ contentType }) => contentType === 'application/vnd.microsoft.card.oauth'
) || {};
const { content } = activity;
if (content) {
const { tokenExchangeResource } = content;
const { uri } = tokenExchangeResource;
if (uri) {
dispatch({
type: 'WEB_CHAT/SET_NOTIFICATION',
payload: {
data: { content },
id: 'signin',
level: 'info',
message: 'Please sign in to the app.'
}
});
return false;
}
}
}
return next(action);
}),
[]
);
```
Web Chat could use toast for custom prompts:
```ts
const toastMiddleware = () => next => ({ notification, ...otherArgs }) => {
const { id } = notification;
if (id === 'signin') {
return <BotSignInToast notification={notification} context={props.context}/>;
}
else if (id === 'traditionalbotauthentication') {
return <TraditionalBotAuthenticationToast notification={notification} />;
}
return next({ notification, ...otherArgs });
};
```
In the toast, [`aadTokenProvider`](https://docs.microsoft.com/en-us/javascript/api/sp-http/aadtokenprovider?view=sp-typescript-latest) is used to get the required token for exchange. If succeed, send an invoke activity back for authentication:
```ts
context.aadTokenProviderFactory.getTokenProvider().then((tokenProvider: AadTokenProvider) => {
return tokenProvider.getToken('api://123a45b6-789c-01de-f23g-h4ij5k67a8bc/scope', true).then((token: string) => {
const { connectionName, tokenExchangeResource } = content;
const { tokenId } = tokenExchangeResource;
if (token) {
postActivity({
channelData: { invokeId },
type: 'invoke',
name: 'signin/tokenExchange',
value: {
id: tokenId,
connectionName,
token,
},
});
}
});
});
```
If the SSO approach failed, fallback to traditional sign in link:
```ts
const handleClick = useCallback(() => {
dismissNotification(id);
performCardAction(signin);
}, [dismissNotification, id, performCardAction, signin]);
```
Note: The first time users try SSO, users may be presented with an OAuth card to log in. This is because users have not yet given consent to the bot's Azure AD app. To avoid this, users can grant admin consent for any graph permissions requested by the Azure AD app.
Note: due to a [SDK bug](https://github.com/microsoft/botbuilder-js/issues/3006), the consent card could not be shown properly yet. Granting admin consent may be necessary to workaround this.
## Further reading
- [SharePoint Web Parts Development Basics](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/overview-client-side-web-parts)
- [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 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)
- [Add authentication to your bot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-tutorial-authentication?view=azure-bot-service-3.0&tabs=aadv1)
- [Add single sign on to a bot](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication-sso?view=azure-bot-service-4.0&tabs=csharp%2Ceml)
- [Web Chat with SSO using MSAL](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/07.advanced-web-chat-apps/e.sso-on-behalf-of-authentication)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-bot-framework-sso" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 MiB

View File

@ -0,0 +1,17 @@
module.exports = {
extends: 'standard',
rules: {
semi: [2, 'always'],
indent: [2, 4],
'no-return-await': 0,
'space-before-function-paren': [
2,
{
named: 'never',
anonymous: 'never',
asyncArrow: 'always'
}
],
'template-curly-spacing': [2, 'always']
}
};

View File

@ -0,0 +1,2 @@
/.env
/node_modules/

View File

@ -0,0 +1,128 @@
# Authentication Bot Utilizing Microsoft Graph
Bot Framework v4 bot authentication using Microsoft Graph sample
This bot has been created using [Bot Framework](https://dev.botframework.com). It shows how to use the bot authentication capabilities of Azure Bot Service. In this sample we are assuming the OAuth 2 provider is Azure Active Directory v2 (AADv2) and are utilizing the Microsoft Graph API to retrieve data about the user. [Check here](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication?view=azure-bot-service-4.0&tabs=csharp) for information about getting an AADv2
application setup for use in Azure Bot Service. The [scopes](https://developer.microsoft.com/en-us/graph/docs/concepts/permissions_reference) used in this sample are the following:
- `User.Read`
## Prerequisites
- [Node.js](https://nodejs.org) version 10.14 or higher
```bash
# determine node version
node --version
```
- Update `.env` with required configuration settings
- MicrosoftAppId
- MicrosoftAppPassword
- ConnectionName
## To try this sample
- Install modules
```bash
npm install
```
- Start the bot
```bash
npm start
```
## Testing the bot using Bot Framework Emulator
[Microsoft 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](https://github.com/microsoft/botframework-emulator/releases)
- In Bot Framework Emulator Settings, enable `Use a sign-in verification code for OAuthCards` to receive the magic code
### Connect to bot using Bot Framework Emulator
- Launch Bot Framework Emulator
- File -> Open Bot
- Enter a Bot URL of `http://localhost:3978/api/messages
## Authentication
This sample uses the bot authentication capabilities of 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, and so on. These updates also
take steps towards an improved user experience by eliminating the magic code verification for some clients and channels.
It is important to note that the user's token does not need to be stored in the bot. When the bot needs to use or verify the user has a valid token at any point the OAuth prompt may be sent. If the token is not valid they will be prompted to login.
## Microsoft Graph API
This sample demonstrates using Azure Active Directory v2 as the OAuth2 provider and utilizes the Microsoft Graph API.
Microsoft Graph is a Microsoft developer platform that connects multiple services and devices. Initially released in 2015,
the Microsoft Graph builds on Office 365 APIs and allows developers to integrate their services with Microsoft products
including Windows, Office 365, and Azure.
## 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.
## GraphError 404: ResourceNotFound, Resource could not be discovered
This error may confusingly present itself if either of the following are true:
- You're using an email ending in `@microsoft.com`, and/or
- Your OAuth AAD tenant is `microsoft.onmicrosoft.com`.
## Testing Direct Line token generation
- [Connect to Direct Line](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-channel-connect-directline?view=azure-bot-service-4.0)
- Add Direct Line Secret to `.env`
```bash
DirectLineSecret=YOUR_DIRECT_LINE_SECRET
```
- Start the bot
```bash
npm start
```
- Open [PostMan](https://www.postman.com/) and setup a post request to http://localhost:3978/directline/token
with the following json request body:
```
{
"user": "USER_ID"
}
```
Then you can see the Direct Line token generated with YOUR_DIRECT_LINE_SECRET and USER_ID:
```
{
"conversationId": "XXXXX",
"token": "XXXXX",
"expires_in": 3600
}
```
## 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)
- [Microsoft Graph API](https://developer.microsoft.com/en-us/graph)
- [MS Graph Docs](https://developer.microsoft.com/en-us/graph/docs/concepts/overview) and [SDK](https://github.com/microsoftgraph/msgraph-sdk-dotnet)
- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
- [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)
- [.NET Core CLI tools](https://docs.microsoft.com/en-us/dotnet/core/tools/?tabs=netcore2x)
- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest)
- [Azure Portal](https://portal.azure.com)
- [Language Understanding using LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/)
- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0)
- [Restify](https://www.npmjs.com/package/restify)
- [dotenv](https://www.npmjs.com/package/dotenv)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-bot-framework-sso/bot" />

View File

@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { DialogBot } = require('./dialogBot');
const AdaptiveCard = require('../resources/adaptiveCard.json');
const { CardFactory } = require('botbuilder');
class AuthBot extends DialogBot {
constructor(conversationState, userState, dialog) {
super(conversationState, userState, dialog);
this.onMembersAdded(async (context, next) => {
const {
activity: { membersAdded, recipient }
} = context;
for (const { id } of membersAdded) {
if (id !== recipient.id) {
await context.sendActivity(
{ attachments: [this.createAdaptiveCard()] }
);
await context.sendActivity(
"Welcome! Type anything to get logged in. Type 'logout' to sign-out."
);
}
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
this.onTokenResponseEvent(async (context, next) => {
console.log('Running dialog with Token Response Event Activity.');
// Run the Dialog with the new Token Response Event Activity.
await this.dialog.run(context, this.dialogState);
// By calling next() you ensure that the next BotHandler is run.
await next();
});
}
createAdaptiveCard() {
return CardFactory.adaptiveCard(AdaptiveCard);
}
}
module.exports.AuthBot = AuthBot;

View File

@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { ActivityHandler } = require('botbuilder');
class DialogBot extends ActivityHandler {
/**
*
* @param {ConversationState} conversationState
* @param {UserState} userState
* @param {Dialog} dialog
*/
constructor(conversationState, userState, dialog) {
super();
if (!conversationState) throw new Error('[DialogBot]: Missing parameter. conversationState is required');
if (!userState) throw new Error('[DialogBot]: Missing parameter. userState is required');
if (!dialog) throw new Error('[DialogBot]: Missing parameter. dialog is required');
this.conversationState = conversationState;
this.userState = userState;
this.dialog = dialog;
this.dialogState = this.conversationState.createProperty('DialogState');
this.onMessage(async (context, next) => {
console.log('Running dialog with Message Activity.');
// Run the Dialog with the new message Activity.
await this.dialog.run(context, this.dialogState);
// By calling next() you ensure that the next BotHandler is run.
await next();
});
this.onDialog(async (context, next) => {
// Save any state changes. The load happened during the execution of the Dialog.
await this.conversationState.saveChanges(context, false);
await this.userState.saveChanges(context, false);
// By calling next() you ensure that the next BotHandler is run.
await next();
});
}
async onSignInInvoke(context) {
await this.dialog.run(context, this.dialogState);
}
}
module.exports.DialogBot = DialogBot;

View File

@ -0,0 +1,42 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"groupLocation": {
"value": ""
},
"groupName": {
"value": ""
},
"appId": {
"value": ""
},
"appSecret": {
"value": ""
},
"botId": {
"value": ""
},
"botSku": {
"value": ""
},
"newAppServicePlanName": {
"value": ""
},
"newAppServicePlanSku": {
"value": {
"name": "S1",
"tier": "Standard",
"size": "S1",
"family": "S",
"capacity": 1
}
},
"newAppServicePlanLocation": {
"value": ""
},
"newWebAppName": {
"value": ""
}
}
}

View File

@ -0,0 +1,39 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"appId": {
"value": ""
},
"appSecret": {
"value": ""
},
"botId": {
"value": ""
},
"botSku": {
"value": ""
},
"newAppServicePlanName": {
"value": ""
},
"newAppServicePlanSku": {
"value": {
"name": "S1",
"tier": "Standard",
"size": "S1",
"family": "S",
"capacity": 1
}
},
"appServicePlanLocation": {
"value": ""
},
"existingAppServicePlan": {
"value": ""
},
"newWebAppName": {
"value": ""
}
}
}

View File

@ -0,0 +1,183 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"groupLocation": {
"type": "string",
"metadata": {
"description": "Specifies the location of the Resource Group."
}
},
"groupName": {
"type": "string",
"metadata": {
"description": "Specifies the name of the Resource Group."
}
},
"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": {
"type": "string",
"metadata": {
"description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1."
}
},
"newAppServicePlanName": {
"type": "string",
"metadata": {
"description": "The name of the 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."
}
},
"newAppServicePlanLocation": {
"type": "string",
"metadata": {
"description": "The location of the App Service Plan. Defaults to \"westus\"."
}
},
"newWebAppName": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"."
}
}
},
"variables": {
"appServicePlanName": "[parameters('newAppServicePlanName')]",
"resourcesLocation": "[parameters('newAppServicePlanLocation')]",
"webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]",
"siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]",
"botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]"
},
"resources": [
{
"name": "[parameters('groupName')]",
"type": "Microsoft.Resources/resourceGroups",
"apiVersion": "2018-05-01",
"location": "[parameters('groupLocation')]",
"properties": {
}
},
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2018-05-01",
"name": "storageDeployment",
"resourceGroup": "[parameters('groupName')]",
"dependsOn": [
"[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]"
],
"properties": {
"mode": "Incremental",
"template": {
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {},
"variables": {},
"resources": [
{
"comments": "Create a new App Service Plan",
"type": "Microsoft.Web/serverfarms",
"name": "[variables('appServicePlanName')]",
"apiVersion": "2018-02-01",
"location": "[variables('resourcesLocation')]",
"sku": "[parameters('newAppServicePlanSku')]",
"properties": {
"name": "[variables('appServicePlanName')]"
}
},
{
"comments": "Create a Web App using the new App Service Plan",
"type": "Microsoft.Web/sites",
"apiVersion": "2015-08-01",
"location": "[variables('resourcesLocation')]",
"kind": "app",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]"
],
"name": "[variables('webAppName')]",
"properties": {
"name": "[variables('webAppName')]",
"serverFarmId": "[variables('appServicePlanName')]",
"siteConfig": {
"appSettings": [
{
"name": "WEBSITE_NODE_DEFAULT_VERSION",
"value": "10.14.1"
},
{
"name": "MicrosoftAppId",
"value": "[parameters('appId')]"
},
{
"name": "MicrosoftAppPassword",
"value": "[parameters('appSecret')]"
}
],
"cors": {
"allowedOrigins": [
"https://botservice.hosting.portal.azure.net",
"https://hosting.onecloud.azure-test.net/"
]
}
}
}
},
{
"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'))]"
]
}
],
"outputs": {}
}
}
}
]
}

View File

@ -0,0 +1,154 @@
{
"$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. Defaults to \"\"."
}
},
"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'))]",
"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 App Service Plan if no existing App Service Plan name was passed in.",
"type": "Microsoft.Web/serverfarms",
"condition": "[not(variables('useExistingAppServicePlan'))]",
"name": "[variables('servicePlanName')]",
"apiVersion": "2018-02-01",
"location": "[variables('resourcesLocation')]",
"sku": "[parameters('newAppServicePlanSku')]",
"properties": {
"name": "[variables('servicePlanName')]"
}
},
{
"comments": "Create a Web App using an App Service Plan",
"type": "Microsoft.Web/sites",
"apiVersion": "2015-08-01",
"location": "[variables('resourcesLocation')]",
"kind": "app",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]"
],
"name": "[variables('webAppName')]",
"properties": {
"name": "[variables('webAppName')]",
"serverFarmId": "[variables('servicePlanName')]",
"siteConfig": {
"appSettings": [
{
"name": "WEBSITE_NODE_DEFAULT_VERSION",
"value": "10.14.1"
},
{
"name": "MicrosoftAppId",
"value": "[parameters('appId')]"
},
{
"name": "MicrosoftAppPassword",
"value": "[parameters('appSecret')]"
}
],
"cors": {
"allowedOrigins": [
"https://botservice.hosting.portal.azure.net",
"https://hosting.onecloud.azure-test.net/"
]
}
}
}
},
{
"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'))]"
]
}
]
}

View File

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { ActivityTypes } = require('botbuilder');
const { ComponentDialog } = require('botbuilder-dialogs');
class LogoutDialog extends ComponentDialog {
async onBeginDialog(innerDc, options) {
const result = await this.interrupt(innerDc);
if (result) {
return result;
}
return await super.onBeginDialog(innerDc, options);
}
async onContinueDialog(innerDc) {
const result = await this.interrupt(innerDc);
if (result) {
return result;
}
return await super.onContinueDialog(innerDc);
}
async interrupt(innerDc) {
if (innerDc.context.activity.type === ActivityTypes.Message) {
const text = innerDc.context.activity.text ? innerDc.context.activity.text.toLowerCase() : '';
if (text === 'logout') {
// When the user sends the 'logout' command to the bot, we are going to send a 'oauth/signout'
// to the client application which will prompt the app to log the user out and send a 'oauth/signout'
// to the bot. When the bot receives the 'oauth/signout' event it will log the user out from the onEvent
// handler
await innerDc.context.sendActivity({ type: 'event', name: 'oauth/signout' });
await innerDc.context.adapter.signOutUser(innerDc.context, process.env.ConnectionName);
await innerDc.context.sendActivity('You have been signed out.');
}
}
}
}
module.exports.LogoutDialog = LogoutDialog;

View File

@ -0,0 +1,113 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const {
ChoicePrompt,
DialogSet,
DialogTurnStatus,
OAuthPrompt,
TextPrompt,
WaterfallDialog
} = require('botbuilder-dialogs');
const { LogoutDialog } = require('./logoutDialog');
const { OAuthHelpers } = require('../OAuthHelpers');
const MAIN_WATERFALL_DIALOG = 'mainWaterfallDialog';
const OAUTH_PROMPT = 'oAuthPrompt';
const CHOICE_PROMPT = 'choicePrompt';
const TEXT_PROMPT = 'textPrompt';
class MainDialog extends LogoutDialog {
constructor() {
super('MainDialog');
this.addDialog(new ChoicePrompt(CHOICE_PROMPT))
.addDialog(
new OAuthPrompt(OAUTH_PROMPT, {
connectionName: process.env.ConnectionName,
text: 'Please login',
title: 'Login',
timeout: 300000
})
)
.addDialog(new TextPrompt(TEXT_PROMPT))
.addDialog(
new WaterfallDialog(MAIN_WATERFALL_DIALOG, [
this.promptStep.bind(this),
this.loginStep.bind(this),
this.commandStep.bind(this),
this.processStep.bind(this)
])
);
this.initialDialogId = MAIN_WATERFALL_DIALOG;
}
/**
* The run method handles the incoming activity (in the form of a TurnContext) and passes it through the dialog system.
* If no dialog is active, it will start the default dialog.
* @param {*} turnContext
* @param {*} accessor
*/
async run(turnContext, accessor) {
const dialogSet = new DialogSet(accessor);
dialogSet.add(this);
const dialogContext = await dialogSet.createContext(turnContext);
const results = await dialogContext.continueDialog();
if (results.status === DialogTurnStatus.empty) {
await dialogContext.beginDialog(this.id);
}
}
async promptStep(step) {
return step.beginDialog(OAUTH_PROMPT);
}
async loginStep(step) {
// 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.
const tokenResponse = step.result;
if (tokenResponse) {
await OAuthHelpers.listMe(step.context, tokenResponse);
return await step.prompt(TEXT_PROMPT, {
prompt: "What would you like to do? You can type anything or 'logout' to retry."
});
}
await step.context.sendActivity('Login was not successful please try again.');
return await step.endDialog();
}
async commandStep(step) {
step.values.command = step.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.beginDialog(OAUTH_PROMPT);
}
async processStep(step) {
if (step.result) {
// We do not need to store the token in the bot. When we need the token we can
// send another prompt. If the token is valid the user will not need to log back in.
// The token will be available in the Result property of the task.
const tokenResponse = step.result;
// If we have the token use the user is authenticated so we may use it to make API calls.
if (tokenResponse && tokenResponse.token) {
await OAuthHelpers.listMe(step.context, tokenResponse);
}
} else {
await step.context.sendActivity("We couldn't log you in. Please try again later.");
}
return await step.endDialog();
}
}
module.exports.MainDialog = MainDialog;

View File

@ -0,0 +1,115 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const restify = require('restify');
const path = require('path');
const corsMiddleware = require('restify-cors-middleware');
const request = require('request');
const express = require('express');
// Import required bot services.
// See https://aka.ms/bot-services to learn more about the different parts of a bot.
const { BotFrameworkAdapter, ConversationState, MemoryStorage, UserState } = require('botbuilder');
const { AuthBot } = require('./bots/authBot');
const { MainDialog } = require('./dialogs/mainDialog');
const { Guid } = require('./util');
// Note: Ensure you have a .env file and include MicrosoftAppId and MicrosoftAppPassword.
// This MicrosoftApp should have OAuth enabled.
const ENV_FILE = path.join(__dirname, '.env');
require('dotenv').config({ path: ENV_FILE });
// Create the adapter. See https://aka.ms/about-bot-adapter to learn more adapters.
const adapter = new BotFrameworkAdapter({
appId: process.env.MicrosoftAppId,
appPassword: process.env.MicrosoftAppPassword
});
// Catch-all for errors.
adapter.onTurnError = async (context, error) => {
// This check writes out errors to console log .vs. app insights.
// NOTE: In production environment, you should consider logging this to Azure
// application insights.
console.error(`\n [onTurnError] unhandled error: ${ error }`);
// Send a trace activity, which will be displayed in Bot Framework Emulator
await context.sendTraceActivity(
'OnTurnError Trace',
`${ error }`,
'https://www.botframework.com/schemas/error',
'TurnError'
);
// Send a message to the user
await context.sendActivity('The bot encountered an error or bug.');
await context.sendActivity('To continue to run this bot, please fix the bot source code.');
// Clear out state
await conversationState.delete(context);
};
// Define a state store for your bot. See https://aka.ms/about-bot-state to learn more about using MemoryStorage.
// A bot requires a state store to persist the dialog and user state between messages.
// For local development, in-memory storage is used.
// CAUTION: The Memory Storage used here is for local bot debugging only. When the bot
// is restarted, anything stored in memory will be gone.
const memoryStorage = new MemoryStorage();
const conversationState = new ConversationState(memoryStorage);
const userState = new UserState(memoryStorage);
const dialog = new MainDialog();
const bot = new AuthBot(conversationState, userState, dialog);
// Add '*' origins for test only. You should update with your own origins in production code.
const cors = corsMiddleware({
origins: ['*'],
allowHeaders: ['*'],
exposeHeaders: ['*']
});
// Create HTTP server.
const server = restify.createServer();
server.pre(cors.preflight);
server.use(cors.actual);
server.use(express.json());
server.listen(process.env.port || process.env.PORT || 3978, function() {
console.log(`\n${ server.name } listening to ${ server.url }.`);
console.log('\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator');
console.log('\nTo talk to your bot, open the emulator select "Open Bot"');
});
// Listen for incoming requests.
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async context => {
await bot.run(context);
});
});
const GetUserId = (userName) => {
const userId = userName || Guid.newGuid();
return userId;
};
server.post('/directline/token', (req, res) => {
var secret = process.env.DirectLineSecret;
const authorization = `Bearer ${ secret }`;
const userId = 'dl_' + GetUserId((req.body || {}).user);
const options = {
method: 'POST',
uri: 'https://directline.botframework.com/v3/directline/tokens/generate',
body: JSON.stringify({ user: { id: userId } }),
headers: { Authorization: authorization, 'Content-Type': 'application/json' }
};
request.post(options, (error, response, body) => {
if (!error && response.statusCode < 300) {
if (body) { res.send(JSON.parse(body)); }
} else {
res.status(500).send('Call to retrieve token from DirectLine failed');
}
});
});

View File

@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { AttachmentLayoutTypes, CardFactory } = require('botbuilder');
const { SimpleGraphClient } = require('./simple-graph-client');
/**
* These methods call the Microsoft Graph API. The following OAuth scopes are used:
* 'OpenId' 'email' 'Mail.Send.Shared' 'Mail.Read' 'profile' 'User.Read' 'User.ReadBasic.All'
* for more information about scopes see:
* https://developer.microsoft.com/en-us/graph/docs/concepts/permissions_reference
*/
class OAuthHelpers {
/**
* Displays information about the user in the bot.
* @param {TurnContext} context A TurnContext instance containing all the data needed for processing this conversation turn.
* @param {TokenResponse} tokenResponse A response that includes a user token.
*/
static async listMe(context, tokenResponse) {
if (!context) {
throw new Error('OAuthHelpers.listMe(): `context` cannot be undefined.');
}
if (!tokenResponse) {
throw new Error('OAuthHelpers.listMe(): `tokenResponse` cannot be undefined.');
}
// Pull in the data from Microsoft Graph.
const client = new SimpleGraphClient(tokenResponse.token);
const { displayName } = await client.getMe();
await context.sendActivity(`Welcome ${ displayName }. Glad to meet you! You are now logged in.`);
}
}
exports.OAuthHelpers = OAuthHelpers;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
{
"name": "bot-authentication-msgraph",
"version": "1.0.0",
"description": "Bot Builder v4 using OAuthCard with AAD and accessing user details via MS Graph APIs sample",
"author": "Microsoft",
"license": "MIT",
"main": "index.js",
"scripts": {
"start": "node ./index.js",
"watch": "nodemon ./index.js",
"lint": "eslint .",
"lintfix": "eslint . --fix",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/BotBuilder-Samples.git"
},
"dependencies": {
"@microsoft/microsoft-graph-client": "~2.0.0",
"botbuilder": "^4.8.0",
"botbuilder-dialogs": "^4.8.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"request": "^2.88.2",
"restify": "~8.4.0",
"restify-cors-middleware": "^1.1.1"
},
"devDependencies": {
"eslint": "^6.6.0",
"eslint-config-standard": "^14.1.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-node": "^10.0.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"nodemon": "~1.19.4"
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { Client } = require('@microsoft/microsoft-graph-client');
/**
* This class is a wrapper for the Microsoft Graph API.
* See: https://developer.microsoft.com/en-us/graph for more information.
*/
class SimpleGraphClient {
constructor(token) {
if (!token || !token.trim()) {
throw new Error('SimpleGraphClient: Invalid token received.');
}
this._token = token;
// Get an Authenticated Microsoft Graph client using the token issued to the user.
this.graphClient = Client.init({
authProvider: done => {
done(null, this._token); // First parameter takes an error if you can't get an access token.
}
});
}
/**
* Collects information about the user in the bot.
*/
async getMe() {
return await this.graphClient
.api('/me')
.get()
.then(res => {
return res;
});
}
}
exports.SimpleGraphClient = SimpleGraphClient;

View File

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
class Guid {
static newGuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0;
var v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
}
module.exports.Guid = Guid;

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This configuration file is required if iisnode is used to run node processes behind
IIS or IIS Express. For more information, visit:
https://github.com/tjanczuk/iisnode/blob/master/src/samples/configuration/web.config
-->
<configuration>
<system.webServer>
<!-- Visit http://blogs.msdn.com/b/windowsazure/archive/2013/11/14/introduction-to-websockets-on-windows-azure-web-sites.aspx for more information on WebSocket support -->
<webSocket enabled="false" />
<handlers>
<!-- Indicates that the server.js file is a node.js site to be handled by the iisnode module -->
<add name="iisnode" path="index.js" verb="*" modules="iisnode"/>
</handlers>
<rewrite>
<rules>
<!-- Do not interfere with requests for node-inspector debugging -->
<rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
<match url="^index.js\/debug[\/]?" />
</rule>
<!-- First we consider whether the incoming URL matches a physical file in the /public folder -->
<rule name="StaticContent">
<action type="Rewrite" url="public{REQUEST_URI}"/>
</rule>
<!-- All other URLs are mapped to the node.js site entry point -->
<rule name="DynamicContent">
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/>
</conditions>
<action type="Rewrite" url="index.js"/>
</rule>
</rules>
</rewrite>
<!-- 'bin' directory has no special meaning in node.js and apps can be placed in it -->
<security>
<requestFiltering>
<hiddenSegments>
<remove segment="bin"/>
</hiddenSegments>
</requestFiltering>
</security>
<!-- Make sure error responses are left untouched -->
<httpErrors existingResponse="PassThrough" />
<!--
You can control how Node is hosted within IIS using the following options:
* watchedFiles: semi-colon separated list of files that will be watched for changes to restart the server
* node_env: will be propagated to node as NODE_ENV environment variable
* debuggingEnabled - controls whether the built-in debugger is enabled
See https://github.com/tjanczuk/iisnode/blob/master/src/samples/configuration/web.config for a full list of options
-->
<!--<iisnode watchedFiles="web.config;*.js"/>-->
</system.webServer>
</configuration>

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.10.0",
"libraryName": "spfx-bot-framework-chat-SSO",
"libraryId": "2768612e-cd75-4188-9066-74f51d0ce9b1",
"environment": "spo",
"packageManager": "npm",
"isCreatingSolution": true,
"isDomainIsolated": true,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,80 @@
# Microsoft Bot Framework Web Chat with SSO
## Summary
An web parts sample that uses the [botframework-webchat module](https://www.npmjs.com/package/botframework-webchat) to create a React component to render the Bot Framework v4 webchat component. This web parts sample is a single sign-on demo for on behalf of authentication using OAuth.
> When dealing with personal data, please respect user privacy. Follow platform guidelines and post your privacy statement online.
## Used SharePoint Framework Version
![SPFx 1.10.0](https://img.shields.io/badge/drop-1.10.0-green.svg)
## Applies to
* [SharePoint Framework Web Parts](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/overview-client-side-web-parts)
* [Office 365 developer tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
* [Microsoft Bot Framework](http://dev.botframework.com)
## Prerequisites
> You need to get familiar with [the web parts web chat sample](Placeholder) first as this sample is based on that sample.
> You need to have this [bot](../bot/) created and registered using the Microsoft Bot Framework and registered to use the Direct Line Channel, which will give you the token generation endpoint needed when adding this web parts to the page. For more information on creating a bot and registering the channel you can see the official web site at [dev.botframework.com](http://dev.botframework.com).
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- Edit `BotSignInToast.tsx` file to set your AAD scope uri(`scopeUri`) with `api://YOUR_APP_ID` directly like `api://123a45b6-789c-01de-f23g-h4ij5k67a8bc`:
```ts
return tokenProvider.getToken(scopeUri, true).then((token: string) => {
```
- Add the following config to ./config/package-solution.json:
```diff
"webApiPermissionRequests": [
+ {
+ "resource": "<YOUR_APP_ID>",
+ "scope": "<YOUR_AAD_SCOPE_NAME>"
+ }
],
```
- Refer [Connect to Azure AD-secured APIs](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aadhttpclient) to publish and approve permissions from admin site
- In the command line run
```bash
cd ../extension
npm install
gulp serve
```
- Open online test page with user account: https://<YOUR_SITE>.sharepoint.com/_layouts/15/Workbench.aspx
- Config bot endpoint \
Add the web part, set bot endpoint to https://YOUR_BOT.azurewebsites.net, refresh this page, then you can successfully connet bot with SharePoint.
## Deploy
If you want to deploy it follow [these steps](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/hosting-webpart-from-office-365-cdn)
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- Connecting and communicating with a bot built on the Microsoft Bot Framework using the Direct Line Channel
- Validating Property Pane Settings
- Office UI Fabric
- React
- Demo single sign-on for on behalf of authentication using OAuth
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-bot-framework-sso/webpart" />

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"bot-framework-chat-sso-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/BotFrameworkChatSSO/BotFrameworkChatSSOWebPart.js",
"manifest": "./src/webparts/BotFrameworkChatSSO/BotFrameworkChatSSOWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"BotFrameworkChatSSOWebPartStrings": "lib/webparts/BotFrameworkChatSSO/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "spfx-bot-framework-chat-SSO",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "spfx-bot-framework-chat-SSO-solution",
"id": "2768612e-cd75-4188-9066-74f51d0ce9b1",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [
{
"resource": "<YOUR_APP_ID>",
"scope": "<Your_AAD_Scope_Name>"
}
]
},
"paths": {
"zippedPackage": "solution/spfx-bot-framework-chat-SSO.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,7 @@
'use strict';
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(require('gulp'));

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
{
"name": "spfx-bot-framework-chat-SSO",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test",
"lint": "tslint --project .",
"lintfix": "tslint --project . --fix"
},
"dependencies": {
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"blueimp-md5": "^2.16.0",
"botframework-webchat": "^4.9.1",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "73dddf2a-0262-49ec-ad45-48755786a446",
"alias": "BotFrameworkChatSSOWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "BotFrameworkChatSSO" },
"description": { "default": "Sample Web Part to show how to add Web Chat with SSO" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "BotFrameworkChatSSO"
}
}]
}

View File

@ -0,0 +1,54 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
} from '@microsoft/sp-webpart-base';
import { BotFrameworkChatSSO } from './components/BotFrameworkChatSSO';
import { IBotFrameworkSSOProps } from './components/IBotFrameworkChatSSOProps';
import * as strings from 'BotFrameworkChatSSOWebPartStrings';
export interface IBotFrameworkChatSSOWebPartProps {
botEndpoint: string;
}
export default class BotFrameworkChatSSOWebPart extends BaseClientSideWebPart<IBotFrameworkChatSSOWebPartProps> {
public render(): void {
const element: React.ReactElement<IBotFrameworkSSOProps> = React.createElement(BotFrameworkChatSSO, {
botEndpoint: this.properties.botEndpoint,
botName: strings.BotName,
context: this.context,
});
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('botEndpoint', {
label: strings.BotEndpointLabel
}),
]
}
]
}
],
};
}
}

View File

@ -0,0 +1,110 @@
import * as React from 'react';
import { useState, useEffect, useMemo } from 'react';
import * as md5 from 'blueimp-md5';
import styles from './BotFrameworkChatv4.module.scss';
import { IBotFrameworkSSOProps as IBotFrameworkChatSSOProps } from './IBotFrameworkChatSSOProps';
import ReactWebChat, { createDirectLine, createStore } from 'botframework-webchat';
import { BotSignInToast } from './Notification/BotSignInToast';
import { TraditionalBotAuthenticationToast } from './Notification/TraditionalBotAuthenticationToast';
export const BotFrameworkChatSSO: React.FunctionComponent<IBotFrameworkChatSSOProps> = (props) => {
const user = props.context.pageContext.user;
const userDisplayName = (user.displayName && user.displayName.length > 0)? user.displayName.split(' ')[0] : "Me";
const [directLine, setDirectLine] = useState(createDirectLine({}));
const styleSetOptions = useMemo(
() => {
return {
userAvatarInitials: userDisplayName,
botAvatarInitials: props.botName,
};
},
[props]
);
const generateToken = async (botEndpoint: string, userId?: string): Promise<string> => {
const token = await window
.fetch(`${botEndpoint}/directline/token`, {
method: 'POST',
body: JSON.stringify({ user: userId ? userId : '' }),
headers: { 'Content-Type': 'application/json' },
})
.then(
async (response: any): Promise<string> => {
if (response.ok) {
const tokenResponse = await response.clone().json();
return tokenResponse.token;
}
return '';
}
);
return token;
};
useEffect(() => {
const userId = props.context.pageContext.user.loginName;
generateToken(props.botEndpoint, md5(userId)).then((token: string) => {
if (token) {
setDirectLine(createDirectLine({ token }));
}
});
}, []);
const toastMiddleware = () => (next) => ({ notification, ...otherArgs }) => {
const { id } = notification;
if (id === 'signin') {
return <BotSignInToast notification={notification} context={props.context} />;
} else if (id === 'traditionalbotauthentication') {
return <TraditionalBotAuthenticationToast notification={notification} />;
}
return next({ notification, ...otherArgs });
};
const store = useMemo(
() =>
createStore({}, ({ dispatch }) => (next) => (action) => {
if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY' && action.payload.activity.from.role === 'bot') {
const activity =
(action.payload.activity.attachments || []).find(
({ contentType }) => contentType === 'application/vnd.microsoft.card.oauth'
) || {};
const { content } = activity;
if (content) {
const { tokenExchangeResource } = content;
const { uri } = tokenExchangeResource;
if (uri) {
dispatch({
type: 'WEB_CHAT/SET_NOTIFICATION',
payload: {
data: { content },
id: 'signin',
level: 'info',
message: 'Please sign in to the app.',
},
});
return false;
}
}
}
return next(action);
}),
[]
);
return (
<div className={styles.botFrameworkChatv4} style={{ height: 700 }}>
<ReactWebChat
directLine={directLine}
styleOptions={styleSetOptions}
toastMiddleware={toastMiddleware}
store={store}
/>
</div>
);
};

View File

@ -0,0 +1,74 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.botFrameworkChatv4 {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,7 @@
import { WebPartContext } from '@microsoft/sp-webpart-base';
export interface IBotFrameworkSSOProps {
botEndpoint: string;
botName: string;
context: WebPartContext;
}

View File

@ -0,0 +1,113 @@
import { useCallback, useEffect, useRef } from 'react';
import { hooks } from 'botframework-webchat';
import { AadTokenProvider } from '@microsoft/sp-http';
import random from 'math-random';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import './index.css';
import * as React from 'react';
const { useActivities, useDismissNotification, usePostActivity, useSetNotification } = hooks;
export interface IBotSignInToastProps {
notification: any;
context: any;
}
export const BotSignInToast: React.FunctionComponent<IBotSignInToastProps> = ({ notification, context }) => {
const { current: invokeId } = useRef(random().toString(36).substr(2, 10));
const {
data: { content },
id,
} = notification;
const [activities] = useActivities();
const dismissNotification = useDismissNotification();
const postActivity = usePostActivity();
const setNotification = useSetNotification();
const handleDismiss = useCallback(() => {
dismissNotification(id);
setNotification({
id: 'traditionalbotauthentication',
data: { content },
level: 'info',
message: 'OK, please sign in to the bot directly.',
});
}, [dismissNotification, id]);
useEffect(() => {
const invokeActivity = activities.find((activity) => (activity.channelData || {}).invokeId === invokeId);
if (invokeActivity) {
const {
channelData: { state },
} = invokeActivity;
if (state === 'send failed') {
dismissNotification(id);
setNotification({
id: 'traditionalbotauthentication',
data: { content },
level: 'error',
message: 'There was an error authenticating the bot.',
});
} else if (state === 'sent') {
dismissNotification(id);
setNotification({
id: 'signinsuccessful',
level: 'success',
message: 'The bot was authenticated successfully.',
});
}
}
}, [activities]);
const handleAgreeClick = useCallback(() => {
try {
context.aadTokenProviderFactory.getTokenProvider().then((tokenProvider: AadTokenProvider) => {
// Get token: replace <Your_AAD_Scope_Uri> with your application ID URI, something like this: api://XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
return tokenProvider.getToken('<Your_AAD_Scope_Uri>', true).then((token: string) => {
const { connectionName, tokenExchangeResource } = content;
const { tokenId } = tokenExchangeResource;
if (token) {
postActivity({
channelData: { invokeId },
type: 'invoke',
name: 'signin/tokenExchange',
value: {
id: tokenId,
connectionName,
token,
},
});
}
});
});
} catch (error) {
dismissNotification(id);
setNotification({
id: 'traditionalbotauthentication',
data: { content },
level: 'error',
message: 'Authenticating the bot failed.',
});
}
}, [dismissNotification, postActivity, setNotification]);
return (
<div aria-label="Sign in" role="dialog" className="app__signInNotification">
<Icon iconName="Signin" className="ms-Icon-Old app__signInNotification__icon" />
{'Allow the bot to access your account? '}
{
<React.Fragment>
<button className="app__signInNotification__button" onClick={handleAgreeClick} type="button">
Yes
</button>{' '}
<button className="app__signInNotification__button" onClick={handleDismiss} type="button">
No
</button>
</React.Fragment>
}
</div>
);
};

View File

@ -0,0 +1,37 @@
import { useCallback } from 'react';
import { hooks } from 'botframework-webchat';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import './index.css';
import * as React from 'react';
const { useDismissNotification, usePerformCardAction } = hooks;
export interface ITraditionalBotAuthenticationToastProps {
notification: any;
}
export const TraditionalBotAuthenticationToast: React.FunctionComponent<ITraditionalBotAuthenticationToastProps> = ({
notification,
}) => {
const id = notification.id;
const signin = notification.data.content.buttons[0];
const dismissNotification = useDismissNotification();
const performCardAction = usePerformCardAction();
const handleClick = useCallback(() => {
dismissNotification(id);
performCardAction(signin);
}, [dismissNotification, id, performCardAction, signin]);
return (
<div aria-label="Sign in" role="dialog" className="app__signInNotification">
<Icon iconName="Signin" className="ms-Icon-Old app__signInNotification__icon" />
{'Please sign in to the bot directly.'}
<button className="app__signInNotification__button" onClick={handleClick} type="button">
Login
</button>
</div>
);
};

View File

@ -0,0 +1,42 @@
.ms-Icon-Old {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
display: inline-block;
font-style: normal;
font-weight: normal;
speak: none;
}
.app__signInNotification {
align-items: center;
color: #105e7d;
display: flex;
font-family: 'Calibri', 'Helvetica Neue', 'Arial', 'sans-serif';
font-size: 14px;
height: 100%;
}
.app__signInNotification__icon {
text-align: center;
width: 36px;
}
.app__signInNotification__button {
appearance: none;
background-color: rgba(255, 255, 255, 0.8);
border: solid 1px rgba(0, 0, 0, 0.3);
border-radius: 3px;
color: initial;
font-family: inherit;
font-size: inherit;
margin: 0 0 0 4px;
outline: 0;
}
.app__signInNotification__button:hover {
background-color: rgba(0, 0, 0, 0.12);
}
.app__signInNotification__button:focus {
border-color: rgba(0, 0, 0, 0.7);
}

View File

@ -0,0 +1,8 @@
define([], function() {
return {
"PropertyPaneDescription": "Set up bot configuration.",
"BasicGroupName": "Config",
"BotEndpointLabel": "Bot Endpoint (For example: https://YOUR_BOT.azurewebsites.net)",
"BotName": "Bot"
}
});

View File

@ -0,0 +1,11 @@
declare interface IBotFrameworkChatSSOWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
BotEndpointLabel: string;
BotName: string;
}
declare module 'BotFrameworkChatSSOWebPartStrings' {
const strings: IBotFrameworkChatSSOWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,40 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}