-
Notifications
You must be signed in to change notification settings - Fork 189
How to integrate Azure Functions with your Teams app
Azure Functions is a server-less, event-driven compute solution that allows you to write less code. It's a great way to add server-side behaviors to any Teams application. Learn more from Azure Functions Overview
- Install Azure Functions Core Tools.
- .NET SDK 6.0, which is required by TeamsFx binding extension for authorization.
- A Teams tab app with Single Sign On enabled. Enabled SSO for Teams tab app
Create a function app project with HTTP trigger in the api
folder with Azure Function Core Tools.
```
> mkdir api
> cd ./api
> func new --template "Http Trigger" --name getUserProfile --authlevel anonymous
```
After adding function app project, your folder structure may be like:
```
.
|-- .vscode/
|-- env/
|-- infra/
|-- api/ <!--function app source code-->
| |-- getUserProfile/ <!--HTTP trigger name-->
| | |-- function.json
| | |-- index.ts
| |-- package.json
| |-- tsconfig.json
|-- src/ <!--your current source code-->
| |-- index.ts
|-- package.json
|-- teamsapp.yml
|-- tsconfig.json
```
For TypeScript developer, it is recommended to specify the typeRoots
in api/tsconfig.json
to avoid type conflict.
Adding following config may help when there are compile errors like Module '"xxx"' has no exported member
.
{
"compilerOptions": {
...
"typeRoots": ["./node_modules/@types"]
}
}
You can find a complete sample debug profile for VSC here.
-
In
launch.json
file, addAttach to Backend
configuration and ensure the it is cascaded byAttach to Frontend
and be depended byDebug
compounds."configurations": [ ... { "name": "Attach to Frontend (Edge)", "cascadeTerminateToConfigurations": [ "Attach to Backend" ], ... }, { "name": "Attach to Backend", "type": "node", "request": "attach", "port": 9229, "restart": true, "presentation": { "group": "all", "hidden": true }, "internalConsoleOptions": "neverOpen" } ], "compounds": [ { "name": "Debug (Edge)", "configurations": [ "Attach to Frontend (Edge)", "Attach to Backend" ], }, ... ]
-
In
tasks.json
file, addStart backend
andWatch backend
tasks.※
Watch backend
task is dedicated for TypeScript project. It is no need to add it if you are using JavaScript.{ "label": "Start backend", "type": "shell", "command": "npm run dev:teamsfx", "isBackground": true, "options": { "cwd": "${workspaceFolder}/api", "env": { "PATH": "${workspaceFolder}/devTools/func:${env:PATH}" } }, "problemMatcher": { "pattern": { "regexp": "^.*$", "file": 0, "location": 1, "message": 2 }, "background": { "activeOnStart": true, "beginsPattern": "^.*(Job host stopped|signaling restart).*$", "endsPattern": "^.*(Worker process started and initialized|Host lock lease acquired by instance ID).*$" } }, "presentation": { "reveal": "silent" }, "dependsOn": ["Watch backend"] }, { "label": "Watch backend", "type": "shell", "command": "npm run watch", "isBackground": true, "options": { "cwd": "${workspaceFolder}/api" }, "problemMatcher": "$tsc-watch", "presentation": { "reveal": "silent" } }
-
Add
Start backend
task to the dependsOn list ofStart application
task.{ "label": "Start application", "dependsOn": [ "Start frontend", "Start backend" ] },
-
Add
file/createOrUpdateEnvironmentFile
actions to deploy lifecycle in./teamsapp.local.yml
file. This action generates environment variables.deploy: - uses: file/createOrUpdateEnvironmentFile # Generate runtime environment variables with: target: ./api/.localConfigs envs: M365_CLIENT_ID: ${{AAD_APP_CLIENT_ID}} M365_CLIENT_SECRET: ${{SECRET_AAD_APP_CLIENT_SECRET}} M365_TENANT_ID: ${{AAD_APP_TENANT_ID}} M365_AUTHORITY_HOST: ${{AAD_APP_OAUTH_AUTHORITY_HOST}} ALLOWED_APP_IDS: 1fec8e78-bce4-4aaf-ab1b-5451cc387264;5e3ce6c0-2b1f-4285-8d4b-75ee78787346;0ec893e0-5785-4de6-99da-4ed124e5296c;4345a7b9-9a63-4910-a426-35363201d503;4765445b-32c6-49b0-83e6-1d93765276ca;d3590ed6-52b3-4102-aeff-aad2292ab01c;00000002-0000-0ff1-ce00-000000000000;bc59ab01-8403-45c6-8796-ac3ef710b3e3
-
Add NPM scripts to
./api/package.json
for the function app start. It is recommended to leverage env-cmd to using the environment from the env file."scripts": { "dev:teamsfx": "env-cmd --silent -f .localConfigs npm run dev", "dev": "func start --typescript --language-worker=\"--inspect=9229\" --port \"7071\" --cors \"*\"", ... }
-
Add a cli/runNpmCommand action to deploy lifecycle in
./teamsapp.local.yml
file. This action triggernpm install
before launching your function app.deploy: - uses: cli/runNpmCommand # Run npm command with: args: install --no-audit workingDirectory: api
-
Start debugging, you will find your tab app launching with function app running behind.
Yet, your Azure Function app is public to any client. With TeamsFx binding extension, your function is able to reject unauthorized client. Here are the steps to add authorization.
-
Remove the
extensionBundle
section inhost.json
file. -
Install TeamsFx binding extension.
> cd api > func extensions install --package Microsoft.Azure.WebJobs.Extensions.TeamsFx --version 1.0.*
-
Refer TeamsFx binding in
function.json
.{ "bindings": [ ..., { "direction": "in", "name": "teamsfxContext", "type": "TeamsFx" } ] }
Remember in ./teamsapp.local.yml, we have set variables M365_CLIENT_ID and ALLOWED_APP_IDS in environment. Now the Function App can only be called by those client whose client id is in the list of ALLOWED_APP_IDS or equals to M365_CLIENT_ID. M365_CLIENT_ID should be the client id of your Teams app. So that your Teams tab app is able to call your function.
-
We recommend setting function endpoint and function name in environment variables. In
teamsapp.local.yml
file, find the actionfile/createOrUpdateEnvironmentFile
and add new envs.- uses: file/createOrUpdateEnvironmentFile # Generate runtime environment variables with: target: ./.localConfigs envs: BROWSER: none HTTPS: true PORT: 53000 SSL_CRT_FILE: ${{SSL_CRT_FILE}} SSL_KEY_FILE: ${{SSL_KEY_FILE}} REACT_APP_CLIENT_ID: ${{AAD_APP_CLIENT_ID}} REACT_APP_START_LOGIN_PAGE_URL: ${{TAB_ENDPOINT}}/auth-start.html REACT_APP_FUNC_NAME: getUserProfile REACT_APP_FUNC_ENDPOINT: http://localhost:7071
-
Call your Azure Function with TeamsFx SDK.
const functionName = process.env.REACT_APP_FUNC_NAME; const functionEndpoint = process.env.REACT_APP_FUNC_ENDPOINT; const teamsfx = useContext(TeamsFxContext).teamsfx const credential = teamsfx.getCredential(); const apiClient = createApiClient( `${functionEndpoint}/api/`, new BearerTokenAuthProvider(async () => (await credential.getToken(""))!.token)); const response = await apiClient.get(functionName);
The
createApiClient
will handle the authorization header. You can find a complete React component for calling Azure Function here. -
Start debugging to test the logic.
You can find a complete sample here.
-
Install TeamsFx SDK and isomorphic-fetch, which is required by msgraph-sdk-javascript. More information
> cd api/ > npm i @microsoft/teamsfx isomorphic-fetch
-
Here is an example for calling the Graph API with TeamsFx SDK. We have set the environment variables in ./teamsapp.local.yml.
import "isomorphic-fetch"; import { createMicrosoftGraphClientWithCredential, OnBehalfOfCredentialAuthConfig, OnBehalfOfUserCredential, } from "@microsoft/teamsfx"; const httpTrigger: AzureFunction = async function ( context: Context, req: HttpRequest, teamsfxContext: { [key: string]: any }, ): Promise<void> { // Get accessToken from TeamsFxContext. const accessToken = teamsfxContext["AccessToken"]; const oboAuthConfig: OnBehalfOfCredentialAuthConfig = { authorityHost: process.env.M365_AUTHORITY_HOST, tenantId: process.env.M365_TENANT_ID, clientId: process.env.M365_CLIENT_ID, clientSecret: process.env.M365_CLIENT_SECRET, }; const oboCredential = new OnBehalfOfUserCredential(accessToken, oboAuthConfig); // Create a graph client with default scope to access user's Microsoft 365 data after user has consented. const graphClient = createMicrosoftGraphClientWithCredential( oboCredential, [".default"] ); const profile: any = await graphClient.api("/me").get(); context.res.body = { graphClientMessage: profile }; }
-
Update bicep to configure Azure Function App. You will need a Storage Account, an App Service Plan and a Function App Service. You can find the complete sample here.
param resourceBaseName string param functionStorageSKU string param functionAppSKU string param aadAppClientId string param aadAppTenantId string param aadAppOauthAuthorityHost string @secure() param aadAppClientSecret string param location string = resourceGroup().location param serverfarmsName string = resourceBaseName param functionAppName string = resourceBaseName param functionStorageName string = '${resourceBaseName}api' var oauthAuthority = uri(aadAppOauthAuthorityHost, aadAppTenantId) var teamsMobileOrDesktopAppClientId = '1fec8e78-bce4-4aaf-ab1b-5451cc387264' var teamsWebAppClientId = '5e3ce6c0-2b1f-4285-8d4b-75ee78787346' var officeWebAppClientId1 = '4345a7b9-9a63-4910-a426-35363201d503' var officeWebAppClientId2 = '4765445b-32c6-49b0-83e6-1d93765276ca' var outlookDesktopAppClientId = 'd3590ed6-52b3-4102-aeff-aad2292ab01c' var outlookWebAppClientId = '00000002-0000-0ff1-ce00-000000000000' var authorizedClientApplicationIds = '${teamsMobileOrDesktopAppClientId};${teamsWebAppClientId};${officeWebAppClientId1};${officeWebAppClientId2};${outlookDesktopAppClientId};${outlookWebAppClientId}' var allowedClientApplications = '"${teamsMobileOrDesktopAppClientId}","${teamsWebAppClientId}","${officeWebAppClientId1}","${officeWebAppClientId2}","${outlookDesktopAppClientId}","${outlookWebAppClientId}"' var tabEndpoint = ${TabAppEndpoint} var aadApplicationIdUri = 'api://${TabAppDomain}/${aadAppClientId}' // Compute resources for Azure Functions resource serverfarms 'Microsoft.Web/serverfarms@2021-02-01' = { name: serverfarmsName location: location sku: { name: functionAppSKU // You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionServerfarmsSku property to provisionParameters to override the default value "Y1". } properties: {} } // Azure Functions that hosts your function code resource functionApp 'Microsoft.Web/sites@2021-02-01' = { name: functionAppName kind: 'functionapp' location: location properties: { serverFarmId: serverfarms.id httpsOnly: true siteConfig: { alwaysOn: true cors: { allowedOrigins: [ tabEndpoint ] } appSettings: [ { name: ' AzureWebJobsDashboard' value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting } { name: 'AzureWebJobsStorage' value: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting } { name: 'FUNCTIONS_EXTENSION_VERSION' value: '~4' // Use Azure Functions runtime v4 } { name: 'FUNCTIONS_WORKER_RUNTIME' value: 'node' // Set runtime to NodeJS } { name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' value: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${listKeys(storage.id, storage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' // Azure Functions internal setting } { name: 'WEBSITE_RUN_FROM_PACKAGE' value: '1' // Run Azure Functions from a package file } { name: 'WEBSITE_NODE_DEFAULT_VERSION' value: '~16' // Set NodeJS version to 16.x } { name: 'M365_CLIENT_ID' value: aadAppClientId } { name: 'M365_CLIENT_SECRET' value: aadAppClientSecret } { name: 'M365_TENANT_ID' value: aadAppTenantId } { name: 'M365_AUTHORITY_HOST' value: aadAppOauthAuthorityHost } { name: 'M365_APPLICATION_ID_URI' value: aadApplicationIdUri } { name: 'WEBSITE_AUTH_AAD_ACL' value: '{"allowed_client_applications": [${allowedClientApplications}]}' } ] ftpsState: 'FtpsOnly' } } } var apiEndpoint = 'https://${functionApp.properties.defaultHostName}' resource authSettings 'Microsoft.Web/sites/config@2021-02-01' = { name: '${functionApp.name}/authsettings' properties: { enabled: true defaultProvider: 'AzureActiveDirectory' clientId: aadAppClientId issuer: '${oauthAuthority}/v2.0' allowedAudiences: [ aadAppClientId aadApplicationIdUri ] } } // Azure Storage is required when creating Azure Functions instance resource functionStorage 'Microsoft.Storage/storageAccounts@2021-06-01' = { name: functionStorageName kind: 'StorageV2' location: location sku: { name: functionStorageSKU// You can follow https://aka.ms/teamsfx-bicep-add-param-tutorial to add functionStorageSKUproperty to provisionParameters to override the default value "Standard_LRS". } } // The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. output API_FUNCTION_ENDPOINT string = apiEndpoint output API_FUNCTION_RESOURCE_ID string = functionApp.id
Add new parameters in azure.parameter.json file.
{ "parameters": { "aadAppClientId": { "value": "${{AAD_APP_CLIENT_ID}}" }, "aadAppClientSecret": { "value": "${{SECRET_AAD_APP_CLIENT_SECRET}}" }, "aadAppTenantId": { "value": "${{AAD_APP_TENANT_ID}}" }, "aadAppOauthAuthorityHost": { "value": "${{AAD_APP_OAUTH_AUTHORITY_HOST}}" }, "functionAppSKU": { "value": "B1" }, "functionStorageSKU": { "value": "Standard_LRS" } } }
-
Run
Teams: Provision in the cloud
command in Visual Studio Code to apply the bicep to Azure. -
Add new actions in
teamsapp.yaml
to setup deployment. You can find the complete sample heredeploy: - uses: cli/runNpmCommand # Run npm command with: workingDirectory: ./api args: install - uses: cli/runNpmCommand # Run npm command with: workingDirectory: ./api args: run build --if-present - uses: azureFunctions/deploy with: # deploy base folder artifactFolder: ./api # the resource id of the cloud resource to be deployed to resourceId: ${{API_FUNCTION_RESOURCE_ID}}
-
Add function name and function endpoint to tab app's environment, so that your tab app can call your function http trigger.
deploy: - uses: cli/runNpmCommand # Run npm command env: REACT_APP_CLIENT_ID: ${{AAD_APP_CLIENT_ID}} REACT_APP_START_LOGIN_PAGE_URL: ${{TAB_ENDPOINT}}/auth-start.html REACT_APP_FUNC_NAME: getUserProfile REACT_APP_FUNC_ENDPOINT: ${{API_FUNCTION_ENDPOINT}}
-
Run
Teams: Deploy to cloud
command in Visual Studio Code to deploy your Tab app code to Azure. -
Open the
Run and Debug Activity Panel
and selectLaunch Remote (Edge)
orLaunch Remote (Chrome)
. Press F5 to preview your Teams app.
Build Custom Engine Copilots
- Build a basic AI chatbot for Teams
- Build an AI agent chatbot for Teams
- Expand AI bot's knowledge with your content
Scenario-based Tutorials
- Send notifications to Teams
- Respond to chat commands in Teams
- Respond to card actions in Teams
- Embed a dashboard canvas in Teams
Extend your app across Microsoft 365
- Teams tabs in Microsoft 365 and Outlook
- Teams message extension for Outlook
- Add Outlook Add-in to a Teams app
App settings and Microsoft Entra Apps
- Manage Application settings with Teams Toolkit
- Manage Microsoft Entra Application Registration with Teams Toolkit
- Use an existing Microsoft Entra app
- Use a multi-tenant Microsoft Entra app
Configure multiple capabilities
- How to configure Tab capability within your Teams app
- How to configure Bot capability within your Teams app
- How to configure Message Extension capability within your Teams app
Add Authentication to your app
- How to add single sign on in Teams Toolkit for Visual Studio Code
- How to enable Single Sign-on in Teams Toolkit for Visual Studio
Connect to cloud resources
- How to integrate Azure Functions with your Teams app
- How to integrate Azure API Management
- Integrate with Azure SQL Database
- Integrate with Azure Key Vault
Deploy apps to production