V1.0
This commit is contained in:
parent
25f2add75b
commit
725020797e
|
@ -0,0 +1,336 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
![1.0](https://img.shields.io/badge/version-1.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 [Placeholder]
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Setup bot with Direct Line](./bot/README.md)
|
||||||
|
|
||||||
|
- In a terminal, navigate to `[Placeholder]`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd [Placeholder]
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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 ‘Direct Line Secret’ 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 here](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-tutorial-authentication?view=azure-bot-service-3.0&tabs=aadv1) 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" 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_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 configration:
|
||||||
|
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 connet 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 [here](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 |
|
@ -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']
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
/.env
|
||||||
|
/node_modules/
|
|
@ -0,0 +1,131 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
- Clone the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone [Placeholder]
|
||||||
|
```
|
||||||
|
|
||||||
|
- In a terminal, navigate to `[Placeholder]`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd [Placeholder]
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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 from [here](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)
|
|
@ -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;
|
|
@ -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;
|
|
@ -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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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'))]"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
@ -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
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
![1.0](https://img.shields.io/badge/drop-1.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 the steps [here](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
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||||
|
"deployCdnPath": "temp/deploy"
|
||||||
|
}
|
|
@ -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 -->"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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/"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||||
|
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||||
|
}
|
|
@ -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
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
|
||||||
|
export interface IBotFrameworkSSOProps {
|
||||||
|
botEndpoint: string;
|
||||||
|
botName: string;
|
||||||
|
context: WebPartContext;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
8
samples/react-bot-framework-sso/webpart/src/webparts/botFrameworkChatSSO/loc/en-us.js
vendored
Normal file
8
samples/react-bot-framework-sso/webpart/src/webparts/botFrameworkChatSSO/loc/en-us.js
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
});
|
11
samples/react-bot-framework-sso/webpart/src/webparts/botFrameworkChatSSO/loc/mystrings.d.ts
vendored
Normal file
11
samples/react-bot-framework-sso/webpart/src/webparts/botFrameworkChatSSO/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
declare interface IBotFrameworkChatSSOWebPartStrings {
|
||||||
|
PropertyPaneDescription: string;
|
||||||
|
BasicGroupName: string;
|
||||||
|
BotEndpointLabel: string;
|
||||||
|
BotName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'BotFrameworkChatSSOWebPartStrings' {
|
||||||
|
const strings: IBotFrameworkChatSSOWebPartStrings;
|
||||||
|
export = strings;
|
||||||
|
}
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue