├── .deployment ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── config ├── default.json └── sample.json ├── deploy.cmd ├── deploy.sh ├── doc └── images │ ├── AAD-SSO-Tab-1-NeedsConsent.png │ ├── AAD-SSO-Tab-2-ConsentPopup.png │ ├── AAD-SSO-Tab-3-ConsentDeclined.png │ ├── AAD-SSO-Tab-4-ConsentGranted.png │ ├── AAD-SSO-Tab-5-ConsentPreviouslyGranted.png │ ├── AAD-SSO-Tab-6-Mobile.png │ └── AAD-SSO-Tab-7-DeletingConsentForTesting.png ├── gulpfile.js ├── package-lock.json ├── package.json └── src ├── app.js ├── manifest.json ├── static ├── images │ ├── contoso20x20.png │ └── contoso96x96.png ├── scripts │ ├── config.js │ ├── initTeamsTab.js │ └── ssoDemo.js └── styles │ ├── custom.css │ └── msteams-16.css ├── tabs.js └── views ├── auth-end.pug ├── auth-start.pug ├── configure.pug ├── hello.pug ├── layout.pug └── ssoDemo.pug /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | command = bash deploy.sh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | *.zip 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # Distributables 62 | dist/ 63 | manifest/ 64 | build/ 65 | 66 | # VS Code config files 67 | .vscode/** -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | products: 4 | - office-365 5 | languages: 6 | - javascript 7 | title: Microsoft Teams NodeJS Helloworld - Tabs Azure AD SSO Sample 8 | description: Microsoft Teams hello world sample app for tabs Azure AD SSO in Node.js 9 | extensions: 10 | contentType: samples 11 | createdDate: 11/3/2017 12:53:17 PM 12 | --- 13 | 14 | # Microsoft Teams - Tabs Azure AD Single Sign-On Sample 15 | 16 | This sample shows how to implement Azure AD single sign-on support for tabs. It will: 17 | 18 | 1. Obtain an access token for the logged-in user using SSO 19 | 20 | 2. Call a web service - also part of this project - to exchange this access token for one with User.Read permission 21 | 22 | 3. Call Graph and retrieve the user's profile 23 | 24 | ![Screen shot of solution](./doc/images/AAD-SSO-Tab-4-ConsentGranted.png) 25 | 26 | ## Prerequisites 27 | 28 | You will need: 29 | 30 | 1. A global administrator account for an Office 365 tenant. Testing in a production tenant is not recommended! You can get a free tenant for development use by signing up for the [Office 365 Developer Program](https://developer.microsoft.com/en-us/microsoft-365/dev-program). 31 | 32 | 1. To test locally, [NodeJS](https://nodejs.org/en/download/) must be installed on your development machine. 33 | 34 | 1. To test locally, you'll need [Ngrok](https://ngrok.com/) installed on your development machine. 35 | Make sure you've downloaded and installed Ngrok on your local machine. ngrok will tunnel requests from the Internet to your local computer and terminate the SSL connection from Teams. 36 | 37 | > NOTE: The free ngrok plan will generate a new URL every time you run it, which requires you to update your Azure AD registration, the Teams app manifest, and the project configuration. A paid account with a permanent ngrok URL is recommended. 38 | 39 | ## Step 1: Register an Azure AD Application 40 | 41 | Your tab needs to run as a registered Azure AD application in order to obtain an access token from Azure AD. In this step you'll register the app in your tenant and give Teams permission to obtain access tokens on its behalf. 42 | 43 | 1. Create an [AAD application](https://docs.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/authentication/auth-aad-sso#1-create-your-aad-application-in-azure) in Azure. You can do this by visiting the "Azure AD app registration" portal in Azure. 44 | 45 | * Set your application URI to the same URI you've created in Ngrok. 46 | * Ex: `api://contoso.ngrok.io/{appId}` 47 | using the application ID that was assigned to your app 48 | * Setup your redirect URIs. This will allow Azure AD to return authentication results to the correct URI. 49 | * Visit `Manage > Authentication`. 50 | * Create a redirect URI in the format of: `https://contoso.ngrok.io/auth/auth-end`. 51 | * Enable Implicit Grant by selecting `Access Tokens` and `ID Tokens`. 52 | * Setup a client secret. You will need this when you exchange the token for more API permissions from your backend. 53 | * Visit `Manage > Certificates & secrets` 54 | * Create a new client secret. 55 | * Setup your API permissions. This is what your application is allowed to request permission to access. 56 | * Visit `Manage > API Permissions` 57 | * Make sure you have the following Graph permissions enabled: `email`, `offline_access`, `openid`, `profile`, and `User.Read`. 58 | * Our SSO flow will give you access to the first 4 permissions, and we will have to exchange the token server-side to get an elevated token for the `profile` permission (for example, if we want access to the user's profile photo). 59 | 60 | * Expose an API that will give the Teams desktop, web and mobile clients access to the permissions above 61 | * Visit `Manage > Expose an API` 62 | * Add a scope and give it a scope name of `access_as_user`. Your API url should look like this: `api://contoso.ngrok.io/{appID}/access_as_user`. In the "who can consent" step, enable it for "Admins and users". Make sure the state is set to "enabled". 63 | * Next, add two client applications. This is for the Teams desktop/mobile clients and the web client. 64 | * 5e3ce6c0-2b1f-4285-8d4b-75ee78787346 65 | * 1fec8e78-bce4-4aaf-ab1b-5451cc387264 66 | 67 | ## Update the app manifest and config.js file 68 | 69 | 1. Update the `manifest.json` file as follows: 70 | 71 | * Generate a new unique ID for the application and replace the id field with this GUID. On Windows, you can generate a new GUID in PowerShell with this command: 72 | ~~~ powershell 73 | [guid]::NewGuid() 74 | ~~~ 75 | * Ensure the package name is unique within the tenant where you will run the app 76 | * Replace `{ngrokSubdomain}` with the subdomain you've assigned to your Ngrok account in step #1 above. 77 | * Update your `webApplicationInfo` section with your Azure AD application ID that you were assigned in step #2 above. 78 | 79 | 80 | 81 | 2. Update your `config/default.json` file 82 | * Replace the `tab.id` property with you Azure AD application ID 83 | * Replace the `tab.password` property with the "client secret" you were assigned in step #2 84 | * If you want to use a port other than 3333, fill that in here (and in your ngrok command) 85 | 86 | ## Running the app locally 87 | 88 | 1. Run Ngrok to expose your local web server via a public URL. Make sure to point it to your Ngrok URI. For example, if you're using port 3333 locally, run: 89 | * Win: `./ngrok http 3333 -host-header=localhost:3333 -subdomain="contoso"` 90 | * Mac: `/ngrok http 3333 -host-header=localhost:3333 -subdomain="contoso"` 91 | 92 | Leave this running while you're running the application locally, and open another command prompt for the steps which follow. 93 | 94 | 2. Install the neccessary NPM packages and start the app 95 | * `npm install` 96 | * `npm start` 97 | 98 | Thhe app should start running on port 3333 or the port you configured 99 | 100 | ## Packaging and installing your app to Teams 101 | 102 | 1. Package your manifest 103 | * `gulp generate-manifest` 104 | * This will create a zip file in the manifest folder 105 | 2. Install in Teams 106 | * Open Teams and visit the app store. Depending on the version of Teams, you may see an "App Store" button in the bottom left of Teams or you can find the app store by visiting `Apps > More Apps` in the left-hand app rail. 107 | * Install the app by clicking on the `Upload a custom app` link in the bottom left-hand side of the app store. 108 | * Upload the manifest zip file created in step #1 109 | 110 | ## Trying out the app 111 | 112 | 1. Once you've installed the app, it should automatically open for you. Visit the `Auth Tab` to begin testing out the authentication flow. 113 | 2. Follow the onscreen prompts. The authentication flow will print the output to your screen. 114 | 115 | * The first time you run the app it should get an access token from Microsoft Teams, but it won't be able to get one from the server unless the user or an administrator consents. If this is necessary, you will see a consent button. 116 | 117 | ![Screen with consent button](./doc/images/AAD-SSO-Tab-1-NeedsConsent.png) 118 | 119 | * Click the consent button and a pop-up window will display the consent dialog from Azure AD. 120 | 121 | ![Azure AD pop-up window](./doc/images/AAD-SSO-Tab-2-ConsentPopup.png) 122 | 123 | * Once you've granted all the permissions, the page will use the access token it received to make a Graph API call. 124 | 125 | ![Graph call following consent](./doc/images/AAD-SSO-Tab-4-ConsentGranted.png) 126 | 127 | * Once you've granted all the permissions, you can revisit this tab and you will notice that you will automatically be logged in. 128 | 129 | ![Subsequent visit to tab](./doc/images/AAD-SSO-Tab-5-ConsentPreviouslyGranted.png) 130 | 131 | * The SSO even works on mobile devices. 132 | 133 | ![SSO from a mobile device](./doc/images/AAD-SSO-Tab-6-Mobile.png) 134 | 135 | ## Testing the consent process 136 | 137 | If you need to remove all consents for the application for test purposes, simply delete its service principal in the Azure AD portal. It may take a few minutes for cached values to time out. The service principal is created automatically the first time someone consents. 138 | 139 | ![Service principal](./doc/images/AAD-SSO-Tab-7-DeletingConsentForTesting.png) 140 | 141 | # App structure 142 | 143 | ## Routes 144 | 145 | Compared to the Hello World sample, this app has four additional routes: 146 | 1. `/ssoDemo` renders the tab UI. 147 | * This is the tab called `Auth Tab` in personal app inside Teams. The purpose of this page is primarily to execute the `auth.js` file that handles initiates the authentication flow. 148 | * This tab can also be added to Teams channels 149 | 2. `/auth/token` does not render anything but instead is the server-side route for initiating the [on-behalf-of flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-on-behalf-of-flow). 150 | * It takes the token it receives from the `/ssoDemo` page and attemps to exchange it for a new token that has elevated permissions to access the `profile` Graph API (which is usually used to retrieve the users profile photo). 151 | * If it fails (because the user hasn't granted permission to access the `profile` API), it returns an error to the `/ssoDemo` page. This error is used to display the "Consent" button which uses the Teams SDK to open the `/auth/start` page in a pop-up window. 152 | 3. `/auth/start` and `/auth/end` routes are used if the user needs to grant further permissions. This experience happens in a seperate window. 153 | * The `/auth/start` page merely creates a valid AAD authorization endpoint and redirects to that AAD consent page. 154 | * Once the user has consented to the permissions, AAD redirects the user back to `/auth/end`. This page is responsible for returning the results back to the `/auth` page by calling the `notifySuccess` API. 155 | * This workflow is only neccessary if you want authorization to use additional Graph APIs. Most apps will find this flow unnesseccary if all they want to do is authenticate the user. 156 | * This workflow is the same as our standard [web-based authentication flow](https://docs.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/authentication/auth-tab-aad#navigate-to-the-authorization-page-from-your-popup-page) that we've always had in Teams before we had single sign-on support. It just so happens that it's a great way to request additional permissions from the user, so it's left in this sample as an illustration of what that flow looks like. 157 | 158 | ## ssoDemo.js 159 | 160 | This Javascript file is served from the `/ssoDemo` page and handles most of the client-side authentication workflow. This file is broken into three main functions: 161 | 162 | 1. getClientSideToken() - 163 | This function asks Teams for an authentication token from AAD. The token is displayed so you can try it in Postman. 164 | 165 | 2. getServerSideToken() - 166 | This function sends the token to the backend to exchange for elevated permissions using AAD's [on-behalf-of flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-on-behalf-of-flow). In this case, it sends the token to the `/auth/token` route. 167 | 168 | 3. useServerSideToken() - 169 | This function uses the token to call the Microsoft Graph and display the resulting JSON. 170 | 171 | 4. requestConsent() - 172 | This function launches the consent pop-up 173 | 174 | Inline code runs these in sequence, running requestConsent only if an `invalid_grant` error is received from the server. 175 | 176 | # Additional reading 177 | 178 | For how to get started with Microsoft Teams development see [Get started on the Microsoft Teams platform with Node.js and App Studio](https://docs.microsoft.com/en-us/microsoftteams/platform/get-started/get-started-nodejs-app-studio). 179 | 180 | For further information on Single Sign-On and how it works, visit our [Single Sign-On documentation](https://docs.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/authentication/auth-aad-sso) 181 | 182 | # Contributing 183 | 184 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 185 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 186 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 187 | 188 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 189 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 190 | provided by the bot. You will only need to do this once across all repos using our CLA. 191 | 192 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 193 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 194 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 195 | 196 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "tab": { 3 | "appId": "MICROSOFT_APP_ID", 4 | "appPassword": "MICROSOFT_APP_PASSWORD" 5 | }, 6 | "port": "3333" 7 | } 8 | -------------------------------------------------------------------------------- /config/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "tab": { 3 | "appId": "MICROSOFT_APP_ID", 4 | "appPassword": "MICROSOFT_APP_PASSWORD" 5 | }, 6 | "port": "3333" 7 | } 8 | -------------------------------------------------------------------------------- /deploy.cmd: -------------------------------------------------------------------------------- 1 | @if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off 2 | 3 | :: ---------------------- 4 | :: KUDU Deployment Script 5 | :: Version: 1.0.17 6 | :: ---------------------- 7 | 8 | :: Prerequisites 9 | :: ------------- 10 | 11 | :: Verify node.js installed 12 | where node 2>nul >nul 13 | IF %ERRORLEVEL% NEQ 0 ( 14 | echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment. 15 | goto error 16 | ) 17 | 18 | :: Setup 19 | :: ----- 20 | 21 | setlocal enabledelayedexpansion 22 | 23 | SET ARTIFACTS=%~dp0%..\artifacts 24 | 25 | IF NOT DEFINED DEPLOYMENT_SOURCE ( 26 | SET DEPLOYMENT_SOURCE=%~dp0%. 27 | ) 28 | 29 | IF NOT DEFINED DEPLOYMENT_TARGET ( 30 | SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot 31 | ) 32 | 33 | IF NOT DEFINED NEXT_MANIFEST_PATH ( 34 | SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest 35 | 36 | IF NOT DEFINED PREVIOUS_MANIFEST_PATH ( 37 | SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest 38 | ) 39 | ) 40 | 41 | IF NOT DEFINED KUDU_SYNC_CMD ( 42 | :: Install kudu sync 43 | echo Installing Kudu Sync 44 | call npm install kudusync -g --silent 45 | IF !ERRORLEVEL! NEQ 0 goto error 46 | 47 | :: Locally just running "kuduSync" would also work 48 | SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd 49 | ) 50 | goto Deployment 51 | 52 | :: Utility Functions 53 | :: ----------------- 54 | 55 | :SelectNodeVersion 56 | 57 | IF DEFINED KUDU_SELECT_NODE_VERSION_CMD ( 58 | :: The following are done only on Windows Azure Websites environment 59 | call %KUDU_SELECT_NODE_VERSION_CMD% "%DEPLOYMENT_SOURCE%" "%DEPLOYMENT_TARGET%" "%DEPLOYMENT_TEMP%" 60 | IF !ERRORLEVEL! NEQ 0 goto error 61 | 62 | IF EXIST "%DEPLOYMENT_TEMP%\__nodeVersion.tmp" ( 63 | SET /p NODE_EXE=<"%DEPLOYMENT_TEMP%\__nodeVersion.tmp" 64 | IF !ERRORLEVEL! NEQ 0 goto error 65 | ) 66 | 67 | IF EXIST "%DEPLOYMENT_TEMP%\__npmVersion.tmp" ( 68 | SET /p NPM_JS_PATH=<"%DEPLOYMENT_TEMP%\__npmVersion.tmp" 69 | IF !ERRORLEVEL! NEQ 0 goto error 70 | ) 71 | 72 | IF NOT DEFINED NODE_EXE ( 73 | SET NODE_EXE=node 74 | ) 75 | 76 | SET NPM_CMD="!NODE_EXE!" "!NPM_JS_PATH!" 77 | ) ELSE ( 78 | SET NPM_CMD=npm 79 | SET NODE_EXE=node 80 | ) 81 | 82 | goto :EOF 83 | 84 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 85 | :: Deployment 86 | :: ---------- 87 | 88 | :Deployment 89 | echo Handling node.js deployment. 90 | 91 | :: 1. KuduSync 92 | IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" ( 93 | call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_SOURCE%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd" 94 | IF !ERRORLEVEL! NEQ 0 goto error 95 | ) 96 | 97 | :: 2. Select node version 98 | call :SelectNodeVersion 99 | 100 | :: 3. Install npm packages 101 | IF EXIST "%DEPLOYMENT_TARGET%\package.json" ( 102 | pushd "%DEPLOYMENT_TARGET%" 103 | call :ExecuteCmd !NPM_CMD! install --production 104 | IF !ERRORLEVEL! NEQ 0 goto error 105 | popd 106 | ) 107 | 108 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 109 | goto end 110 | 111 | :: Execute command routine that will echo out when error 112 | :ExecuteCmd 113 | setlocal 114 | set _CMD_=%* 115 | call %_CMD_% 116 | if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_% 117 | exit /b %ERRORLEVEL% 118 | 119 | :error 120 | endlocal 121 | echo An error has occurred during web site deployment. 122 | call :exitSetErrorLevel 123 | call :exitFromFunction 2>nul 124 | 125 | :exitSetErrorLevel 126 | exit /b 1 127 | 128 | :exitFromFunction 129 | () 130 | 131 | :end 132 | endlocal 133 | echo Finished successfully. 134 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ---------------------- 4 | # KUDU Deployment Script 5 | # Version: 1.0.17 6 | # ---------------------- 7 | 8 | # Helpers 9 | # ------- 10 | 11 | exitWithMessageOnError () { 12 | if [ ! $? -eq 0 ]; then 13 | echo "An error has occurred during web site deployment." 14 | echo $1 15 | exit 1 16 | fi 17 | } 18 | 19 | # Prerequisites 20 | # ------------- 21 | 22 | # Verify node.js installed 23 | hash node 2>/dev/null 24 | exitWithMessageOnError "Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment." 25 | 26 | # Setup 27 | # ----- 28 | 29 | SCRIPT_DIR="${BASH_SOURCE[0]%\\*}" 30 | SCRIPT_DIR="${SCRIPT_DIR%/*}" 31 | ARTIFACTS=$SCRIPT_DIR/../artifacts 32 | KUDU_SYNC_CMD=${KUDU_SYNC_CMD//\"} 33 | 34 | if [[ ! -n "$DEPLOYMENT_SOURCE" ]]; then 35 | DEPLOYMENT_SOURCE=$SCRIPT_DIR 36 | fi 37 | 38 | if [[ ! -n "$NEXT_MANIFEST_PATH" ]]; then 39 | NEXT_MANIFEST_PATH=$ARTIFACTS/manifest 40 | 41 | if [[ ! -n "$PREVIOUS_MANIFEST_PATH" ]]; then 42 | PREVIOUS_MANIFEST_PATH=$NEXT_MANIFEST_PATH 43 | fi 44 | fi 45 | 46 | if [[ ! -n "$DEPLOYMENT_TARGET" ]]; then 47 | DEPLOYMENT_TARGET=$ARTIFACTS/wwwroot 48 | else 49 | KUDU_SERVICE=true 50 | fi 51 | 52 | if [[ ! -n "$KUDU_SYNC_CMD" ]]; then 53 | # Install kudu sync 54 | echo Installing Kudu Sync 55 | npm install kudusync -g --silent 56 | exitWithMessageOnError "npm failed" 57 | 58 | if [[ ! -n "$KUDU_SERVICE" ]]; then 59 | # In case we are running locally this is the correct location of kuduSync 60 | KUDU_SYNC_CMD=kuduSync 61 | else 62 | # In case we are running on kudu service this is the correct location of kuduSync 63 | KUDU_SYNC_CMD=$APPDATA/npm/node_modules/kuduSync/bin/kuduSync 64 | fi 65 | fi 66 | 67 | # Node Helpers 68 | # ------------ 69 | 70 | selectNodeVersion () { 71 | if [[ -n "$KUDU_SELECT_NODE_VERSION_CMD" ]]; then 72 | SELECT_NODE_VERSION="$KUDU_SELECT_NODE_VERSION_CMD \"$DEPLOYMENT_SOURCE\" \"$DEPLOYMENT_TARGET\" \"$DEPLOYMENT_TEMP\"" 73 | eval $SELECT_NODE_VERSION 74 | exitWithMessageOnError "select node version failed" 75 | 76 | if [[ -e "$DEPLOYMENT_TEMP/__nodeVersion.tmp" ]]; then 77 | NODE_EXE=`cat "$DEPLOYMENT_TEMP/__nodeVersion.tmp"` 78 | exitWithMessageOnError "getting node version failed" 79 | fi 80 | 81 | if [[ -e "$DEPLOYMENT_TEMP/__npmVersion.tmp" ]]; then 82 | NPM_JS_PATH=`cat "$DEPLOYMENT_TEMP/__npmVersion.tmp"` 83 | exitWithMessageOnError "getting npm version failed" 84 | fi 85 | 86 | if [[ ! -n "$NODE_EXE" ]]; then 87 | NODE_EXE=node 88 | fi 89 | 90 | NPM_CMD="\"$NODE_EXE\" \"$NPM_JS_PATH\"" 91 | else 92 | NPM_CMD=npm 93 | NODE_EXE=node 94 | fi 95 | } 96 | 97 | ################################################################################################################################## 98 | # Deployment 99 | # ---------- 100 | 101 | echo Handling node.js deployment. 102 | 103 | # 1. KuduSync 104 | if [[ "$IN_PLACE_DEPLOYMENT" -ne "1" ]]; then 105 | "$KUDU_SYNC_CMD" -v 50 -f "$DEPLOYMENT_SOURCE" -t "$DEPLOYMENT_TARGET" -n "$NEXT_MANIFEST_PATH" -p "$PREVIOUS_MANIFEST_PATH" -i ".git;.hg;.deployment;deploy.sh" 106 | exitWithMessageOnError "Kudu Sync failed" 107 | fi 108 | 109 | # 2. Select node version 110 | selectNodeVersion 111 | 112 | # 3. Install npm packages 113 | if [ -e "$DEPLOYMENT_TARGET/package.json" ]; then 114 | cd "$DEPLOYMENT_TARGET" 115 | echo "Running $NPM_CMD install --production" 116 | eval $NPM_CMD install --production 117 | exitWithMessageOnError "npm failed" 118 | cd - > /dev/null 119 | fi 120 | 121 | ################################################################################################################################## 122 | echo "Finished successfully." 123 | -------------------------------------------------------------------------------- /doc/images/AAD-SSO-Tab-1-NeedsConsent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/msteams-tabs-sso-sample-nodejs/aa5635c7464068fb93b4e364bf693459e20a14c2/doc/images/AAD-SSO-Tab-1-NeedsConsent.png -------------------------------------------------------------------------------- /doc/images/AAD-SSO-Tab-2-ConsentPopup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/msteams-tabs-sso-sample-nodejs/aa5635c7464068fb93b4e364bf693459e20a14c2/doc/images/AAD-SSO-Tab-2-ConsentPopup.png -------------------------------------------------------------------------------- /doc/images/AAD-SSO-Tab-3-ConsentDeclined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/msteams-tabs-sso-sample-nodejs/aa5635c7464068fb93b4e364bf693459e20a14c2/doc/images/AAD-SSO-Tab-3-ConsentDeclined.png -------------------------------------------------------------------------------- /doc/images/AAD-SSO-Tab-4-ConsentGranted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/msteams-tabs-sso-sample-nodejs/aa5635c7464068fb93b4e364bf693459e20a14c2/doc/images/AAD-SSO-Tab-4-ConsentGranted.png -------------------------------------------------------------------------------- /doc/images/AAD-SSO-Tab-5-ConsentPreviouslyGranted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/msteams-tabs-sso-sample-nodejs/aa5635c7464068fb93b4e364bf693459e20a14c2/doc/images/AAD-SSO-Tab-5-ConsentPreviouslyGranted.png -------------------------------------------------------------------------------- /doc/images/AAD-SSO-Tab-6-Mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/msteams-tabs-sso-sample-nodejs/aa5635c7464068fb93b4e364bf693459e20a14c2/doc/images/AAD-SSO-Tab-6-Mobile.png -------------------------------------------------------------------------------- /doc/images/AAD-SSO-Tab-7-DeletingConsentForTesting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/msteams-tabs-sso-sample-nodejs/aa5635c7464068fb93b4e364bf693459e20a14c2/doc/images/AAD-SSO-Tab-7-DeletingConsentForTesting.png -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const zip = require('gulp-zip'); 3 | const del = require('del'); 4 | 5 | gulp.task('clean', function(done) { 6 | return del([ 7 | 'manifest/**/*' 8 | ], done); 9 | }); 10 | 11 | gulp.task('generate-manifest', function(done) { 12 | gulp.src(['src/static/images/contoso*', 'src/manifest.json']) 13 | .pipe(zip('aadSsoTabSample.zip')) 14 | .pipe(gulp.dest('manifest'), done); 15 | done(); 16 | }); 17 | 18 | gulp.task('default', gulp.series('clean', 'generate-manifest'), function(done) { 19 | console.log('Build completed. Output in manifest folder'); 20 | done(); 21 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msteams-nodejs-hello-world", 3 | "version": "1.0.0", 4 | "description": "Microsoft Teams Node.js Hello World App. Start here to build your first app on the Teams platform.", 5 | "scripts": { 6 | "start": "node src/app.js", 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/OfficeDev/msteams-samples-hello-world-nodejs.git" 12 | }, 13 | "author": "Microsoft Corp.", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/OfficeDev/msteams-samples-hello-world-nodejs/issues" 17 | }, 18 | "homepage": "https://docs.microsoft.com/en-us/microsoftteams/platform/get-started/get-started-nodejs", 19 | "dependencies": { 20 | "config": "^1.28.1", 21 | "express": "^4.16.2", 22 | "node-fetch": "^2.1.2", 23 | "faker": "^4.1.0", 24 | "pug": "^2.0.0-rc.4" 25 | }, 26 | "devDependencies": { 27 | "del": "^3.0.0", 28 | "gulp": "^4.0.2", 29 | "gulp-zip": "^4.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('config'); 4 | var express = require('express'); 5 | var app = express(); 6 | 7 | // Add the route for handling tabs 8 | var tabs = require('./tabs'); 9 | tabs.setup(app); 10 | 11 | // Decide which port to use 12 | var port = process.env.PORT || 13 | config.has("port") ? config.get("port") : 3333; 14 | 15 | // Listen for incoming requests 16 | app.listen(port, function() { 17 | console.log(`App started listening on port ${port}`); 18 | }); 19 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", 3 | "manifestVersion": "1.5", 4 | "version": "1.1.0", 5 | "id": "{new GUID for this Teams app - not the Azure AD App ID}", 6 | "packageName": "com.contoso.aadSsoSample", 7 | "developer": { 8 | "name": "AAD SSO Tab Sample", 9 | "websiteUrl": "https://www.microsoft.com", 10 | "privacyUrl": "https://www.microsoft.com/privacy", 11 | "termsOfUseUrl": "https://www.microsoft.com/termsofuse" 12 | }, 13 | "name": { 14 | "short": "AAD SSO Tab Sample", 15 | "full": "Auth sample app for Microsoft Teams" 16 | }, 17 | "description": { 18 | "short": "Auth sample for Microsoft Teams", 19 | "full": "This sample app provides a very simple app for Microsoft Teams which illustrates how single sign-on (SSO) should work for tabs." 20 | }, 21 | "icons": { 22 | "outline": "contoso20x20.png", 23 | "color": "contoso96x96.png" 24 | }, 25 | "accentColor": "#60A18E", 26 | "staticTabs": [{ 27 | "entityId": "com.contoso.teamsauthsample.static", 28 | "name": "Auth Tab", 29 | "contentUrl": "https://{ngrokSubdomain}.ngrok.io/ssoDemo", 30 | "scopes": [ 31 | "personal" 32 | ] 33 | }], 34 | "configurableTabs": [{ 35 | "configurationUrl": "https://{ngrokSubdomain}.ngrok.io/configure", 36 | "canUpdateConfiguration": true, 37 | "scopes": [ 38 | "team" 39 | ] 40 | }], 41 | "permissions": [], 42 | "validDomains": [], 43 | "webApplicationInfo": { 44 | "id": "{appId}", 45 | "resource": "api://{ngrokSubdomain}.ngrok.io/{appId}" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/static/images/contoso20x20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/msteams-tabs-sso-sample-nodejs/aa5635c7464068fb93b4e364bf693459e20a14c2/src/static/images/contoso20x20.png -------------------------------------------------------------------------------- /src/static/images/contoso96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/msteams-tabs-sso-sample-nodejs/aa5635c7464068fb93b4e364bf693459e20a14c2/src/static/images/contoso96x96.png -------------------------------------------------------------------------------- /src/static/scripts/config.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | // Save configuration changes 4 | microsoftTeams.settings.registerOnSaveHandler(function (saveEvent) { 5 | 6 | var tabUrl = window.location.protocol + 7 | '//' + window.location.host + '/ssoDemo'; 8 | 9 | // Let the Microsoft Teams platform know what you want to load based on 10 | // what the user configured on this page 11 | microsoftTeams.settings.setSettings({ 12 | contentUrl: tabUrl, // Mandatory parameter 13 | entityId: tabUrl // Mandatory parameter 14 | }); 15 | 16 | // Tells Microsoft Teams platform that we are done saving our settings. Microsoft Teams waits 17 | // for the app to call this API before it dismisses the dialog. If the wait times out, you will 18 | // see an error indicating that the configuration settings could not be saved. 19 | saveEvent.notifySuccess(); 20 | }); 21 | 22 | microsoftTeams.settings.setValidityState(true); 23 | 24 | 25 | })(); -------------------------------------------------------------------------------- /src/static/scripts/initTeamsTab.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | // Call the initialize API first 5 | microsoftTeams.initialize(); 6 | 7 | // Check the initial theme user chose and respect it 8 | microsoftTeams.getContext(function (context) { 9 | if (context && context.theme) { 10 | setTheme(context.theme); 11 | } 12 | }); 13 | 14 | // Handle theme changes 15 | microsoftTeams.registerOnThemeChangeHandler(function (theme) { 16 | setTheme(theme); 17 | }); 18 | 19 | 20 | // Set the desired theme 21 | function setTheme(theme) { 22 | if (theme) { 23 | // Possible values for theme: 'default', 'light', 'dark' and 'contrast' 24 | document.body.className = 'theme-' + (theme === 'default' ? 'light' : theme); 25 | } 26 | } 27 | 28 | })(); 29 | -------------------------------------------------------------------------------- /src/static/scripts/ssoDemo.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | // 1. Get auth token 5 | // Ask Teams to get us a token from AAD 6 | function getClientSideToken() { 7 | 8 | return new Promise((resolve, reject) => { 9 | 10 | display("1. Get auth token from Microsoft Teams"); 11 | 12 | microsoftTeams.authentication.getAuthToken({ 13 | successCallback: (result) => { 14 | display(result) 15 | resolve(result); 16 | }, 17 | failureCallback: function (error) { 18 | reject("Error getting token: " + error); 19 | } 20 | }); 21 | 22 | }); 23 | 24 | } 25 | 26 | // 2. Exchange that token for a token with the required permissions 27 | // using the web service (see /auth/token handler in app.js) 28 | function getServerSideToken(clientSideToken) { 29 | 30 | display("2. Exchange for server-side token"); 31 | 32 | return new Promise((resolve, reject) => { 33 | 34 | microsoftTeams.getContext((context) => { 35 | 36 | fetch('/auth/token', { 37 | method: 'post', 38 | headers: { 39 | 'Content-Type': 'application/json' 40 | }, 41 | body: JSON.stringify({ 42 | 'tid': context.tid, 43 | 'token': clientSideToken 44 | }), 45 | mode: 'cors', 46 | cache: 'default' 47 | }) 48 | .then((response) => { 49 | if (response.ok) { 50 | return response.json(); 51 | } else { 52 | reject(response.error); 53 | } 54 | }) 55 | .then((responseJson) => { 56 | if (responseJson.error) { 57 | reject(responseJson.error); 58 | } else { 59 | const serverSideToken = responseJson; 60 | display(serverSideToken); 61 | resolve(serverSideToken); 62 | } 63 | }); 64 | }); 65 | }); 66 | } 67 | 68 | // 3. Get the server side token and use it to call the Graph API 69 | function useServerSideToken(data) { 70 | 71 | display("3. Call https://graph.microsoft.com/v1.0/me/ with the server side token"); 72 | 73 | return fetch("https://graph.microsoft.com/v1.0/me/", 74 | { 75 | method: 'GET', 76 | headers: { 77 | "accept": "application/json", 78 | "authorization": "bearer " + data 79 | }, 80 | mode: 'cors', 81 | cache: 'default' 82 | }) 83 | .then((response) => { 84 | if (response.ok) { 85 | return response.json(); 86 | } else { 87 | throw (`Error ${response.status}: ${response.statusText}`); 88 | } 89 | }) 90 | .then((profile) => { 91 | display(JSON.stringify(profile, undefined, 4), 'pre'); 92 | }); 93 | 94 | } 95 | 96 | // Show the consent pop-up 97 | function requestConsent() { 98 | return new Promise((resolve, reject) => { 99 | microsoftTeams.authentication.authenticate({ 100 | url: window.location.origin + "/auth/auth-start", 101 | width: 600, 102 | height: 535, 103 | successCallback: (result) => { 104 | let data = localStorage.getItem(result); 105 | localStorage.removeItem(result); 106 | resolve(data); 107 | }, 108 | failureCallback: (reason) => { 109 | reject(JSON.stringify(reason)); 110 | } 111 | }); 112 | }); 113 | } 114 | 115 | // Add text to the display in a

or other HTML element 116 | function display(text, elementTag) { 117 | var logDiv = document.getElementById('logs'); 118 | var p = document.createElement(elementTag ? elementTag : "p"); 119 | p.innerText = text; 120 | logDiv.append(p); 121 | console.log("ssoDemo: " + text); 122 | return p; 123 | } 124 | 125 | // In-line code 126 | getClientSideToken() 127 | .then((clientSideToken) => { 128 | return getServerSideToken(clientSideToken); 129 | }) 130 | .then((serverSideToken) => { 131 | return useServerSideToken(serverSideToken); 132 | }) 133 | .catch((error) => { 134 | if (error === "invalid_grant") { 135 | display(`Error: ${error} - user or admin consent required`); 136 | // Display in-line button so user can consent 137 | let button = display("Consent", "button"); 138 | button.onclick = (() => { 139 | requestConsent() 140 | .then((result) => { 141 | // Consent succeeded - use the token we got back 142 | let accessToken = JSON.parse(result).accessToken; 143 | display(`Received access token ${accessToken}`); 144 | useServerSideToken(accessToken); 145 | }) 146 | .catch((error) => { 147 | display(`ERROR ${error}`); 148 | // Consent failed - offer to refresh the page 149 | button.disabled = true; 150 | let refreshButton = display("Refresh page", "button"); 151 | refreshButton.onclick = (() => { window.location.reload(); }); 152 | }); 153 | }); 154 | } else { 155 | // Something else went wrong 156 | display(`Error from web service: ${error}`); 157 | } 158 | }); 159 | 160 | })(); 161 | -------------------------------------------------------------------------------- /src/static/styles/custom.css: -------------------------------------------------------------------------------- 1 | html, body, div.surface, div.panel { 2 | height: 100%; 3 | margin: 0; 4 | } 5 | 6 | div.panel { 7 | padding: 15px; 8 | } 9 | -------------------------------------------------------------------------------- /src/static/styles/msteams-16.css: -------------------------------------------------------------------------------- 1 | .theme-light .surface{background-color:#F0F2F4;color:#16233A;font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-size:0.875rem;font-weight:400;line-height:1.25rem} .theme-light .panel{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;background-color:#FFFFFF;border-color:transparent;border-radius:0.1875rem;border-style:solid;border-width:0.125rem;box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden} .theme-light .panel-header{flex:0 0 auto;margin-left:2rem;margin-right:2rem;margin-top:2rem} .theme-light .panel-body{flex:1 1 auto;margin-left:2rem;margin-right:2rem;overflow:auto} .theme-light .panel-footer{flex:0 0 auto;margin-bottom:2rem;margin-left:2rem;margin-right:2rem} .theme-light .button-primary{background:#5558AF;border:0.125rem solid;border-color:transparent;border-radius:0.1875rem;color:#FFFFFF;cursor:pointer;font:inherit;height:2rem;min-width:6rem;padding:0.25rem;white-space:nowrap} .theme-light .button-primary:hover:enabled{background:#4C509D;border-color:transparent;color:#FFFFFF} .theme-light .button-primary:active{background:#454A92;border-color:transparent;color:#FFFFFF} .theme-light .button-primary:disabled{background:#F3F4F5;border-color:transparent;color:#ABB0B8} .theme-light .button-primary:focus{background:#4C509D;border-color:transparent;color:#FFFFFF;outline:0.125rem solid #FFFFFF;outline-offset:-0.25rem} .theme-light .button-secondary{background:#FFFFFF;border:0.125rem solid;border-color:#ABB0B8;border-radius:0.1875rem;color:#525C6D;cursor:pointer;font:inherit;height:2rem;min-width:6rem;padding:0.25rem;white-space:nowrap} .theme-light .button-secondary:hover:enabled{background:#ABB0B8;border-color:transparent;color:#16233A} .theme-light .button-secondary:active{background:#858C98;border-color:transparent;color:#16233A} .theme-light .button-secondary:disabled{background:#FFFFFF;border-color:#F3F4F5;color:#ABB0B8} .theme-light .button-secondary:focus{background:#ABB0B8;border-color:transparent;color:#16233A;outline:0.125rem solid #16233A;outline-offset:-0.25rem} .theme-light .radio-container{align-items:center;background:transparent;border:none;display:flex;outline:none} .theme-light .radio-container + .radio-container{margin-top:0.5rem} .theme-light .radio-button{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;background:transparent;border:0.0625rem solid;border-color:#525C6D;border-radius:100%;cursor:pointer;display:inline-block;font:inherit;height:0.75rem;margin:0.125rem;margin-left:0.375rem;padding:0;position:relative;width:0.75rem} .theme-light .radio-button:hover{background:transparent;border-color:#525C6D} .theme-light .radio-button:disabled{background:#F0F2F4;border-color:#ABB0B8} .theme-light .radio-button:disabled + label{color:#ABB0B8;cursor:default} .theme-light .radio-button:focus{box-shadow:0 0 0 0.125rem #9FA4FE;outline:none} .theme-light .hidden-input:checked + .radio-button{background:#5558AF;border-color:#5558AF} .theme-light .hidden-input:checked + .radio-button + label{color:#16233A} .theme-light .radio-label{color:#525C6D;cursor:pointer;font-size:0.75rem;line-height:1rem;margin-left:0.625rem} .theme-light .radio-group{display:inline-block} .theme-light .tab-group{border-bottom:0.0625rem solid #F3F4F5;margin:0;padding:0;width:100%} .theme-light .tab-group .tab{background:0;border:0;border-bottom:transparent 0.25rem solid;color:#525C6D;cursor:pointer;display:inline-block;font:inherit;margin:0;margin-right:1.25rem;outline:none;padding:0.25rem} .theme-light .tab-group .tab:hover{border-bottom-color:#9496CA} .theme-light .tab-group .tab:focus{background-color:#9FA4FE;color:#FFFFFF} .theme-light .tab-group .tab-active{border-bottom-color:#5558AF;color:#5558AF} .theme-light .tab-active:focus{border-bottom-color:#FFFFFF} .theme-light .hidden-input{display:none} .theme-light .toggle{display:inline-block;line-height:1} .theme-light .toggle-ball{background-color:#F0F2F4;border:0;border-radius:1.25rem;cursor:pointer;height:1.25rem;margin:0.125rem;outline:none;padding:0;position:relative;width:3.75rem} .theme-light .toggle-ball:before{background-color:#454A92;border-radius:50%;content:"";height:0.875rem;left:0.1875rem;position:absolute;top:0.18750000000000003rem;transition:0.2s;width:0.875rem} .theme-light .hidden-input:checked + .toggle-ball:before{background-color:#4C509D;transform:translateX(2.5rem)} .theme-light .toggle-ball:focus{box-shadow:0 0 0 0.125rem #5558AF;outline:none} .theme-light .hidden-input:checked + .toggle-ball{background-color:#7FBA00} .theme-light .font-title{font-size:1.5rem;line-height:2rem} .theme-light .font-title2{font-size:1.125rem;line-height:1.5rem} .theme-light .font-base{font-size:0.875rem;line-height:1.25rem} .theme-light .font-caption{font-size:0.75rem;line-height:1rem} .theme-light .font-xsmall{font-size:0.625rem;line-height:0.6875rem} .theme-light .font-semilight{font-family:'Segoe UI Light', 'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:300} .theme-light .font-regular{font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:400} .theme-light .font-semibold{font-family:'Segoe UI Semibold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:600} .theme-light .font-bold{font-family:'Segoe UI Bold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:700} .theme-light .input-container{overflow:hidden;position:relative} .theme-light .input-field{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;background:#F0F2F4;border:0.125rem solid transparent;border-radius:0.1875rem;box-sizing:border-box;color:#525C6D;font:inherit;height:2rem;margin:0;outline:none;padding:0.5rem 0.75rem;width:100%} .theme-light .input-error-icon{bottom:0.5625rem;color:#C50E2E;position:absolute;right:0.75rem} .theme-light .label{border:0;color:#4E586A;display:inline-block;font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-size:0.75rem;font-weight:400;line-height:1rem;margin-bottom:0.5rem;margin-left:0;margin-right:0;margin-top:0;padding:0} .theme-light .error-label{border:0;color:#C50E2E;float:right;font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-size:0.75rem;font-weight:400;line-height:1rem;margin-bottom:0.5rem;margin-left:0;margin-right:0;margin-top:0;padding:0} .theme-light .textarea-container{display:flex;flex-direction:column;overflow:hidden;position:relative} .theme-light .textarea-field{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;background:#F0F2F4;border:0.125rem solid transparent;border-radius:0.1875rem;box-sizing:border-box;color:#525C6D;flex:1;font:inherit;margin:0;min-height:3.75rem;outline:none;padding:0.5rem 0.75rem;resize:none} .theme-light .input-field:hover:inactive:enabled, .theme-light .textarea-field:hover:inactive:enabled{background:#F0F2F4;border-bottom-color:transparent} .theme-light .input-field:disabled, .theme-light .textarea-field:disabled{background:#F3F4F5;border-bottom-color:transparent;color:#DEE0E3} .theme-light .input-field:active:enabled, .theme-light .input-field:focus, .theme-light .textarea-field:active:enabled, .theme-light .textarea-field:focus{background:#F0F2F4;border-bottom-color:#5558AF} .theme-light .textarea-error-icon{color:#C50E2E;position:absolute;right:0.75rem;top:50%} .theme-dark .surface{background-color:#2B2B30;color:#FFFFFF;font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-size:0.875rem;font-weight:400;line-height:1.25rem} .theme-dark .panel{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;background-color:#404045;border-color:transparent;border-radius:0.1875rem;border-style:solid;border-width:0.125rem;box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden} .theme-dark .panel-header{flex:0 0 auto;margin-left:2rem;margin-right:2rem;margin-top:2rem} .theme-dark .panel-body{flex:1 1 auto;margin-left:2rem;margin-right:2rem;overflow:auto} .theme-dark .panel-footer{flex:0 0 auto;margin-bottom:2rem;margin-left:2rem;margin-right:2rem} .theme-dark .button-primary{background:#9FA4FE;border:0.125rem solid;border-color:transparent;border-radius:0.1875rem;color:#2B2B30;cursor:pointer;font:inherit;height:2rem;min-width:6rem;padding:0.25rem;white-space:nowrap} .theme-dark .button-primary:hover:enabled{background:#AEB2FF;border-color:transparent;color:#2B2B30} .theme-dark .button-primary:active{background:#B8BBFF;border-color:transparent;color:#2B2B30} .theme-dark .button-primary:disabled{background:#35353A;border-color:transparent;color:#77777A} .theme-dark .button-primary:focus{background:#9FA4FE;border-color:transparent;color:#2B2B30;outline:0.125rem solid #2B2B30;outline-offset:-0.25rem} .theme-dark .button-secondary{background:#404045;border:0.125rem solid;border-color:#77777A;border-radius:0.1875rem;color:#C8C8C9;cursor:pointer;font:inherit;height:2rem;min-width:6rem;padding:0.25rem;white-space:nowrap} .theme-dark .button-secondary:hover:enabled{background:#77777A;border-color:transparent;color:#FFFFFF} .theme-dark .button-secondary:active{background:#48484D;border-color:transparent;color:#FFFFFF} .theme-dark .button-secondary:disabled{background:#404045;border-color:#35353A;color:#77777A} .theme-dark .button-secondary:focus{background:#77777A;border-color:transparent;color:#FFFFFF;outline:0.125rem solid #FFFFFF;outline-offset:-0.25rem} .theme-dark .radio-container{align-items:center;background:transparent;border:none;display:flex;outline:none} .theme-dark .radio-container + .radio-container{margin-top:0.5rem} .theme-dark .radio-button{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;background:transparent;border:0.0625rem solid;border-color:#C8C8C9;border-radius:100%;cursor:pointer;display:inline-block;font:inherit;height:0.75rem;margin:0.125rem;margin-left:0.375rem;padding:0;position:relative;width:0.75rem} .theme-dark .radio-button:hover{background:transparent;border-color:#C8C8C9} .theme-dark .radio-button:disabled{background:#404045;border-color:#77777A} .theme-dark .radio-button:disabled + label{color:#77777A;cursor:default} .theme-dark .radio-button:focus{box-shadow:0 0 0 0.125rem #5558AF;outline:none} .theme-dark .hidden-input:checked + .radio-button{background:#9FA4FE;border-color:#9FA4FE} .theme-dark .hidden-input:checked + .radio-button + label{color:#FFFFFF} .theme-dark .radio-label{color:#C8C8C9;cursor:pointer;font-size:0.75rem;line-height:1rem;margin-left:0.625rem} .theme-dark .radio-group{display:inline-block} .theme-dark .tab-group{border-bottom:0.0625rem solid #000000;margin:0;padding:0;width:100%} .theme-dark .tab-group .tab{background:0;border:0;border-bottom:transparent 0.25rem solid;color:#C8C8C9;cursor:pointer;display:inline-block;font:inherit;margin:0;margin-right:1.25rem;outline:none;padding:0.25rem} .theme-dark .tab-group .tab:hover{border-bottom-color:#7174AA} .theme-dark .tab-group .tab:focus{background-color:#5558AF;color:#FFFFFF} .theme-dark .tab-group .tab-active{border-bottom-color:#9FA4FE;color:#9FA4FE} .theme-dark .tab-active:focus{border-bottom-color:#FFFFFF} .theme-dark .hidden-input{display:none} .theme-dark .toggle{display:inline-block;line-height:1} .theme-dark .toggle-ball{background-color:#2B2B30;border:0;border-radius:1.25rem;cursor:pointer;height:1.25rem;margin:0.125rem;outline:none;padding:0;position:relative;width:3.75rem} .theme-dark .toggle-ball:before{background-color:#C8C8C9;border-radius:50%;content:"";height:0.875rem;left:0.1875rem;position:absolute;top:0.18750000000000003rem;transition:0.2s;width:0.875rem} .theme-dark .hidden-input:checked + .toggle-ball:before{background-color:#FFFFFF;transform:translateX(2.5rem)} .theme-dark .toggle-ball:focus{box-shadow:0 0 0 0.125rem #9FA4FE;outline:none} .theme-dark .hidden-input:checked + .toggle-ball{background-color:#88BC2B} .theme-dark .font-title{font-size:1.5rem;line-height:2rem} .theme-dark .font-title2{font-size:1.125rem;line-height:1.5rem} .theme-dark .font-base{font-size:0.875rem;line-height:1.25rem} .theme-dark .font-caption{font-size:0.75rem;line-height:1rem} .theme-dark .font-xsmall{font-size:0.625rem;line-height:0.6875rem} .theme-dark .font-semilight{font-family:'Segoe UI Light', 'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:300} .theme-dark .font-regular{font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:400} .theme-dark .font-semibold{font-family:'Segoe UI Semibold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:600} .theme-dark .font-bold{font-family:'Segoe UI Bold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:700} .theme-dark .input-container{overflow:hidden;position:relative} .theme-dark .input-field{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;background:#2B2B30;border:0.125rem solid transparent;border-radius:0.1875rem;box-sizing:border-box;color:#C8C8C9;font:inherit;height:2rem;margin:0;outline:none;padding:0.5rem 0.75rem;width:100%} .theme-dark .input-error-icon{bottom:0.5625rem;color:#ED1B3E;position:absolute;right:0.75rem} .theme-dark .label{border:0;color:#FFFFFF;display:inline-block;font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-size:0.75rem;font-weight:400;line-height:1rem;margin-bottom:0.5rem;margin-left:0;margin-right:0;margin-top:0;padding:0} .theme-dark .error-label{border:0;color:#ED1B3E;float:right;font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-size:0.75rem;font-weight:400;line-height:1rem;margin-bottom:0.5rem;margin-left:0;margin-right:0;margin-top:0;padding:0} .theme-dark .textarea-container{display:flex;flex-direction:column;overflow:hidden;position:relative} .theme-dark .textarea-field{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;background:#2B2B30;border:0.125rem solid transparent;border-radius:0.1875rem;box-sizing:border-box;color:#C8C8C9;flex:1;font:inherit;margin:0;min-height:3.75rem;outline:none;padding:0.5rem 0.75rem;resize:none} .theme-dark .input-field:hover:inactive:enabled, .theme-dark .textarea-field:hover:inactive:enabled{background:#2B2B30;border-bottom-color:transparent} .theme-dark .input-field:disabled, .theme-dark .textarea-field:disabled{background:#35353A;border-bottom-color:transparent;color:#48484D} .theme-dark .input-field:active:enabled, .theme-dark .input-field:focus, .theme-dark .textarea-field:active:enabled, .theme-dark .textarea-field:focus{background:#2B2B30;border-bottom-color:#9FA4FE} .theme-dark .textarea-error-icon{color:#ED1B3E;position:absolute;right:0.75rem;top:50%} .theme-contrast .surface{background-color:#000000;color:#FFFFFF;font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-size:0.875rem;font-weight:400;line-height:1.25rem} .theme-contrast .panel{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;background-color:#000000;border-color:#FFFFFF;border-radius:0.1875rem;border-style:solid;border-width:0.125rem;box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden} .theme-contrast .panel-header{flex:0 0 auto;margin-left:2rem;margin-right:2rem;margin-top:2rem} .theme-contrast .panel-body{flex:1 1 auto;margin-left:2rem;margin-right:2rem;overflow:auto} .theme-contrast .panel-footer{flex:0 0 auto;margin-bottom:2rem;margin-left:2rem;margin-right:2rem} .theme-contrast .button-primary{background:#FFFFFF;border:0.125rem solid;border-color:transparent;border-radius:0.1875rem;color:#000000;cursor:pointer;font:inherit;height:2rem;min-width:6rem;padding:0.25rem;white-space:nowrap} .theme-contrast .button-primary:disabled{background:#30F42C;border-color:transparent;color:#000000} .theme-contrast .button-secondary{background:#000000;border:0.125rem solid;border-color:#FFFFFF;border-radius:0.1875rem;color:#FFFFFF;cursor:pointer;font:inherit;height:2rem;min-width:6rem;padding:0.25rem;white-space:nowrap} .theme-contrast .button-primary:hover:enabled, .theme-contrast .button-primary:active, .theme-contrast .button-secondary:hover:enabled, .theme-contrast .button-secondary:active{background:#FFFF00;border-color:transparent;color:#000000} .theme-contrast .button-secondary:disabled{background:#000000;border-color:#30F42C;color:#30F42C} .theme-contrast .button-primary:focus, .theme-contrast .button-secondary:focus{background:#FFFF00;border-color:transparent;color:#000000;outline:0.125rem solid transparent;outline-offset:-0.25rem} .theme-contrast .radio-container{align-items:center;background:transparent;border:none;display:flex;outline:none} .theme-contrast .radio-container + .radio-container{margin-top:0.5rem} .theme-contrast .radio-button{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;background:transparent;border:0.0625rem solid;border-color:#FFFFFF;border-radius:100%;cursor:pointer;display:inline-block;font:inherit;height:0.75rem;margin:0.125rem;margin-left:0.375rem;padding:0;position:relative;width:0.75rem} .theme-contrast .radio-button:hover{background:transparent;border-color:#FFFFFF} .theme-contrast .radio-button:disabled{background:transparent;border-color:#30F42C} .theme-contrast .radio-button:disabled + label{color:#30F42C;cursor:default} .theme-contrast .radio-button:focus{box-shadow:0 0 0 0.125rem #FFFF00;outline:none} .theme-contrast .hidden-input:checked + .radio-button{background:#00EBFF;border-color:#00EBFF} .theme-contrast .hidden-input:checked + .radio-button + label{color:#FFFFFF} .theme-contrast .radio-label{color:#FFFFFF;cursor:pointer;font-size:0.75rem;line-height:1rem;margin-left:0.625rem} .theme-contrast .radio-group{display:inline-block} .theme-contrast .tab-group{border-bottom:0.0625rem solid #30F42C;margin:0;padding:0;width:100%} .theme-contrast .tab-group .tab{background:0;border:0;border-bottom:transparent 0.25rem solid;color:#FFFFFF;cursor:pointer;display:inline-block;font:inherit;margin:0;margin-right:1.25rem;outline:none;padding:0.25rem} .theme-contrast .tab-group .tab:hover{border-bottom-color:#FFFF00} .theme-contrast .tab-group .tab:focus{background-color:#FFFF00;color:#000000} .theme-contrast .tab-group .tab-active{border-bottom-color:#00EBFF;color:#FFFFFF} .theme-contrast .tab-active:focus{border-bottom-color:#000000} .theme-contrast .hidden-input{display:none} .theme-contrast .toggle{display:inline-block;line-height:1} .theme-contrast .toggle-ball{background-color:#FFFFFF;border:0;border-radius:1.25rem;cursor:pointer;height:1.25rem;margin:0.125rem;outline:none;padding:0;position:relative;width:3.75rem} .theme-contrast .toggle-ball:before{background-color:#FFFF00;border-radius:50%;content:"";height:0.875rem;left:0.1875rem;position:absolute;top:0.18750000000000003rem;transition:0.2s;width:0.875rem} .theme-contrast .hidden-input:checked + .toggle-ball:before{background-color:#4C509D;transform:translateX(2.5rem)} .theme-contrast .toggle-ball:focus{box-shadow:0 0 0 0.125rem #30F42C;outline:none} .theme-contrast .hidden-input:checked + .toggle-ball{background-color:#7FBA00} .theme-contrast .font-title{font-size:1.5rem;line-height:2rem} .theme-contrast .font-title2{font-size:1.125rem;line-height:1.5rem} .theme-contrast .font-base{font-size:0.875rem;line-height:1.25rem} .theme-contrast .font-caption{font-size:0.75rem;line-height:1rem} .theme-contrast .font-xsmall{font-size:0.625rem;line-height:0.6875rem} .theme-contrast .font-semilight{font-family:'Segoe UI Light', 'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:300} .theme-contrast .font-regular{font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:400} .theme-contrast .font-semibold{font-family:'Segoe UI Semibold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:600} .theme-contrast .font-bold{font-family:'Segoe UI Bold', 'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-weight:700} .theme-contrast .input-container{overflow:hidden;position:relative} .theme-contrast .input-field{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;background:#000000;border:0.125rem solid #FFFFFF;border-radius:0.1875rem;box-sizing:border-box;color:#FFFFFF;font:inherit;height:2rem;margin:0;outline:none;padding:0.5rem 0.75rem;width:100%} .theme-contrast .input-error-icon{bottom:0.5625rem;color:#FFFF00;position:absolute;right:0.75rem} .theme-contrast .label{border:0;color:#FFFFFF;display:inline-block;font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-size:0.75rem;font-weight:400;line-height:1rem;margin-bottom:0.5rem;margin-left:0;margin-right:0;margin-top:0;padding:0} .theme-contrast .error-label{border:0;color:#FFFF00;float:right;font-family:'Segoe UI', Tahoma, Helvetica, Sans-Serif;font-size:0.75rem;font-weight:400;line-height:1rem;margin-bottom:0.5rem;margin-left:0;margin-right:0;margin-top:0;padding:0} .theme-contrast .textarea-container{display:flex;flex-direction:column;overflow:hidden;position:relative} .theme-contrast .textarea-field{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;background:#000000;border:0.125rem solid #FFFFFF;border-radius:0.1875rem;box-sizing:border-box;color:#FFFFFF;flex:1;font:inherit;margin:0;min-height:3.75rem;outline:none;padding:0.5rem 0.75rem;resize:none} .theme-contrast .input-field:hover:inactive:enabled, .theme-contrast .textarea-field:hover:inactive:enabled{background:#000000;border-bottom-color:transparent} .theme-contrast .input-field:disabled, .theme-contrast .textarea-field:disabled{background:#30F42C;border-bottom-color:#FFFFFF;color:#FFFFFF} .theme-contrast .input-field:active:enabled, .theme-contrast .input-field:focus, .theme-contrast .textarea-field:active:enabled, .theme-contrast .textarea-field:focus{background:#000000;border-bottom-color:#FFFF00} .theme-contrast .textarea-error-icon{color:#FFFF00;position:absolute;right:0.75rem;top:50%} -------------------------------------------------------------------------------- /src/tabs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fetch = require("node-fetch"); 3 | const querystring = require("querystring"); 4 | var config = require('config'); 5 | 6 | module.exports.setup = function(app) { 7 | var path = require('path'); 8 | var express = require('express') 9 | 10 | // Configure the view engine, views folder and the statics path 11 | app.use(express.static(path.join(__dirname, 'static'))); 12 | app.set('view engine', 'pug'); 13 | app.set('views', path.join(__dirname, 'views')); 14 | 15 | // Use the JSON middleware 16 | app.use(express.json()); 17 | 18 | // Setup home page 19 | app.get('/', function(req, res) { 20 | res.render('hello'); 21 | }); 22 | 23 | // Setup the configure tab, with first and second as content tabs 24 | app.get('/configure', function(req, res) { 25 | res.render('configure'); 26 | }); 27 | 28 | // ------------------ 29 | // SSO demo page 30 | app.get('/ssodemo', function(req, res) { 31 | res.render('ssoDemo'); 32 | }); 33 | 34 | // Pop-up dialog to ask for additional permissions, redirects to AAD page 35 | app.get('/auth/auth-start', function(req, res) { 36 | var clientId = config.get("tab.appId"); 37 | res.render('auth-start', { clientId: clientId }); 38 | }); 39 | 40 | // End of the pop-up dialog auth flow, returns the results back to parent window 41 | app.get('/auth/auth-end', function(req, res) { 42 | var clientId = config.get("tab.appId"); 43 | res.render('auth-end', { clientId: clientId }); 44 | }); 45 | 46 | // On-behalf-of token exchange 47 | app.post('/auth/token', function(req, res) { 48 | var tid = req.body.tid; 49 | var token = req.body.token; 50 | var scopes = ["https://graph.microsoft.com/User.Read"]; 51 | 52 | var oboPromise = new Promise((resolve, reject) => { 53 | const url = "https://login.microsoftonline.com/" + tid + "/oauth2/v2.0/token"; 54 | const params = { 55 | client_id: config.get("tab.appId"), 56 | client_secret: config.get("tab.appPassword"), 57 | grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", 58 | assertion: token, 59 | requested_token_use: "on_behalf_of", 60 | scope: scopes.join(" ") 61 | }; 62 | 63 | fetch(url, { 64 | method: "POST", 65 | body: querystring.stringify(params), 66 | headers: { 67 | Accept: "application/json", 68 | "Content-Type": "application/x-www-form-urlencoded" 69 | } 70 | }).then(result => { 71 | if (result.status !== 200) { 72 | result.json().then(json => { 73 | // TODO: Check explicitly for invalid_grant or interaction_required 74 | reject({"error":json.error}); 75 | }); 76 | } else { 77 | result.json().then(json => { 78 | resolve(json.access_token); 79 | }); 80 | } 81 | }); 82 | }); 83 | 84 | oboPromise.then(function(result) { 85 | res.json(result); 86 | }, function(err) { 87 | console.log(err); // Error: "It broke" 88 | res.json(err); 89 | }); 90 | }); 91 | 92 | }; 93 | -------------------------------------------------------------------------------- /src/views/auth-end.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | title Auth End Page 6 | 7 | body 8 | script(src="https://unpkg.com/@microsoft/teams-js@1.5.0/dist/MicrosoftTeams.min.js", crossorigin="anonymous") 9 | script(src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.15/js/adal.min.js", integrity="sha384-lIk8T3uMxKqXQVVfFbiw0K/Nq+kt1P3NtGt/pNexiDby2rKU6xnDY8p16gIwKqgI", crossorigin="anonymous") 10 | 11 | script. 12 | microsoftTeams.initialize(); 13 | 14 | localStorage.removeItem("auth.error"); 15 | let hashParams = getHashParameters(); 16 | 17 | if (hashParams["error"]) { 18 | // Authentication/authorization failed 19 | localStorage.setItem("auth.error", JSON.stringify(hashParams)); 20 | microsoftTeams.authentication.notifyFailure(hashParams["error"]); 21 | } else if (hashParams["access_token"]) { 22 | // Get the stored state parameter and compare with incoming state 23 | let expectedState = localStorage.getItem("auth.state"); 24 | if (expectedState !== hashParams["state"]) { 25 | // State does not match, report error 26 | localStorage.setItem("auth.error", JSON.stringify(hashParams)); 27 | microsoftTeams.authentication.notifyFailure("StateDoesNotMatch"); 28 | } else { 29 | // Success -- return token information to the parent page. 30 | // Use localStorage to avoid passing the token via notifySuccess; instead we send the item key. 31 | let key = "auth.result"; 32 | // TODO: not sure why this isn't being set 33 | localStorage.setItem(key, JSON.stringify({ 34 | idToken: hashParams["id_token"], 35 | accessToken: hashParams["access_token"], 36 | tokenType: hashParams["token_type"], 37 | expiresIn: hashParams["expires_in"] 38 | })); 39 | microsoftTeams.authentication.notifySuccess(key); 40 | } 41 | } else { 42 | // Unexpected condition: hash does not contain error or access_token parameter 43 | localStorage.setItem("auth.error", JSON.stringify(hashParams)); 44 | microsoftTeams.authentication.notifyFailure("UnexpectedFailure"); 45 | } 46 | // Parse hash parameters into key-value pairs 47 | function getHashParameters() { 48 | let hashParams = {}; 49 | location.hash.substr(1).split("&").forEach(function(item) { 50 | let s = item.split("="), 51 | k = s[0], 52 | v = s[1] && decodeURIComponent(s[1]); 53 | hashParams[k] = v; 54 | }); 55 | return hashParams; 56 | } -------------------------------------------------------------------------------- /src/views/auth-start.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | title Auth Start Page 6 | 7 | body 8 | script(src="https://unpkg.com/@microsoft/teams-js@1.5.0/dist/MicrosoftTeams.min.js", crossorigin="anonymous") 9 | script(src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.15/js/adal.min.js", integrity="sha384-lIk8T3uMxKqXQVVfFbiw0K/Nq+kt1P3NtGt/pNexiDby2rKU6xnDY8p16gIwKqgI", crossorigin="anonymous") 10 | 11 | script. 12 | microsoftTeams.initialize(); 13 | // Get the tab context, and use the information to navigate to Azure AD login page 14 | microsoftTeams.getContext(function (context) { 15 | // Generate random state string and store it, so we can verify it in the callback 16 | let state = _guid(); 17 | localStorage.setItem("auth.state", state); 18 | localStorage.removeItem("auth.error"); 19 | // See https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols-implicit 20 | // for documentation on these query parameters 21 | let queryParams = { 22 | client_id: "#{clientId}", 23 | response_type: "id_token token", 24 | response_mode: "fragment", 25 | scope: "https://graph.microsoft.com/User.Read email openid profile offline_access", 26 | redirect_uri: window.location.origin + "/auth/auth-end", 27 | nonce: _guid(), 28 | state: state, 29 | login_hint: context.loginHint, 30 | }; 31 | // Go to the AzureAD authorization endpoint (tenant-specific endpoint, not "common") 32 | // For guest users, we want an access token for the tenant we are currently in, not the home tenant of the guest. 33 | let authorizeEndpoint = `https://login.microsoftonline.com/${context.tid}/oauth2/v2.0/authorize?${toQueryString(queryParams)}`; 34 | window.location.assign(authorizeEndpoint); 35 | }); 36 | // Build query string from map of query parameter 37 | function toQueryString(queryParams) { 38 | let encodedQueryParams = []; 39 | for (let key in queryParams) { 40 | encodedQueryParams.push(key + "=" + encodeURIComponent(queryParams[key])); 41 | } 42 | return encodedQueryParams.join("&"); 43 | } 44 | // Converts decimal to hex equivalent 45 | // (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js) 46 | function _decimalToHex(number) { 47 | var hex = number.toString(16); 48 | while (hex.length < 2) { 49 | hex = '0' + hex; 50 | } 51 | return hex; 52 | } 53 | // Generates RFC4122 version 4 guid (128 bits) 54 | // (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js) 55 | function _guid() { 56 | // RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or 57 | // pseudo-random numbers. 58 | // The algorithm is as follows: 59 | // Set the two most significant bits (bits 6 and 7) of the 60 | // clock_seq_hi_and_reserved to zero and one, respectively. 61 | // Set the four most significant bits (bits 12 through 15) of the 62 | // time_hi_and_version field to the 4-bit version number from 63 | // Section 4.1.3. Version4 64 | // Set all the other bits to randomly (or pseudo-randomly) chosen 65 | // values. 66 | // UUID = time-low "-" time-mid "-"time-high-and-version "-"clock-seq-reserved and low(2hexOctet)"-" node 67 | // time-low = 4hexOctet 68 | // time-mid = 2hexOctet 69 | // time-high-and-version = 2hexOctet 70 | // clock-seq-and-reserved = hexOctet: 71 | // clock-seq-low = hexOctet 72 | // node = 6hexOctet 73 | // Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx 74 | // y could be 1000, 1001, 1010, 1011 since most significant two bits needs to be 10 75 | // y values are 8, 9, A, B 76 | var cryptoObj = window.crypto || window.msCrypto; // for IE 11 77 | if (cryptoObj && cryptoObj.getRandomValues) { 78 | var buffer = new Uint8Array(16); 79 | cryptoObj.getRandomValues(buffer); 80 | //buffer[6] and buffer[7] represents the time_hi_and_version field. We will set the four most significant bits (4 through 7) of buffer[6] to represent decimal number 4 (UUID version number). 81 | buffer[6] |= 0x40; //buffer[6] | 01000000 will set the 6 bit to 1. 82 | buffer[6] &= 0x4f; //buffer[6] & 01001111 will set the 4, 5, and 7 bit to 0 such that bits 4-7 == 0100 = "4". 83 | //buffer[8] represents the clock_seq_hi_and_reserved field. We will set the two most significant bits (6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively. 84 | buffer[8] |= 0x80; //buffer[8] | 10000000 will set the 7 bit to 1. 85 | buffer[8] &= 0xbf; //buffer[8] & 10111111 will set the 6 bit to 0. 86 | return _decimalToHex(buffer[0]) + _decimalToHex(buffer[1]) + _decimalToHex(buffer[2]) + _decimalToHex(buffer[3]) + '-' + _decimalToHex(buffer[4]) + _decimalToHex(buffer[5]) + '-' + _decimalToHex(buffer[6]) + _decimalToHex(buffer[7]) + '-' + 87 | _decimalToHex(buffer[8]) + _decimalToHex(buffer[9]) + '-' + _decimalToHex(buffer[10]) + _decimalToHex(buffer[11]) + _decimalToHex(buffer[12]) + _decimalToHex(buffer[13]) + _decimalToHex(buffer[14]) + _decimalToHex(buffer[15]); 88 | } 89 | else { 90 | var guidHolder = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; 91 | var hex = '0123456789abcdef'; 92 | var r = 0; 93 | var guidResponse = ""; 94 | for (var i = 0; i < 36; i++) { 95 | if (guidHolder[i] !== '-' && guidHolder[i] !== '4') { 96 | // each x and y needs to be random 97 | r = Math.random() * 16 | 0; 98 | } 99 | if (guidHolder[i] === 'x') { 100 | guidResponse += hex[r]; 101 | } else if (guidHolder[i] === 'y') { 102 | // clock-seq-and-reserved first hex is filtered and remaining hex values are random 103 | r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0?? 104 | r |= 0x8; // set pos 3 to 1 as 1??? 105 | guidResponse += hex[r]; 106 | } else { 107 | guidResponse += guidHolder[i]; 108 | } 109 | } 110 | return guidResponse; 111 | } 112 | }; -------------------------------------------------------------------------------- /src/views/configure.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | div(class="font-semibold font-title") Azure AD SSO Sample Tab 5 | p 6 | div 7 | label(for="tabChoice") 8 | | Click Save below to set up the tab 9 | script(src="/scripts/initTeamsTab.js") 10 | script(src="/scripts/config.js") -------------------------------------------------------------------------------- /src/views/hello.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | div(class="font-semibold font-title") Azure AD SSO Sample 5 | p This sample is meant to run as a tab in Microsoft Teams 6 | -------------------------------------------------------------------------------- /src/views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title Microsoft Teams Hello World Sample App 5 | link(rel="stylesheet", type="text/css", href="/styles/msteams-16.css") 6 | link(rel="stylesheet", type="text/css", href="/styles/custom.css") 7 | script(src="https://unpkg.com/@microsoft/teams-js@1.5.0/dist/MicrosoftTeams.min.js" crossorigin="anonymous") 8 | block scripts 9 | 10 | body(class="theme-light") 11 | div(class="surface") 12 | div(class="panel") 13 | block content 14 | -------------------------------------------------------------------------------- /src/views/ssoDemo.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | 3 | block content 4 | div(class="font-semibold font-title") Azure AD SSO Tab Demo 5 | div(id="logs" style="overflow-x: hidden; overflow-y: scroll;") 6 | script(src="/scripts/initTeamsTab.js") 7 | script(src="/scripts/ssoDemo.js") --------------------------------------------------------------------------------