Skip to content

Microsoft Teams integration

A Microsoft Teams integration can be added to your Drovio Server installation. It lets users start and join Drovio sessions from within Microsoft Teams, using the compose box or bot commands. It comes as a JAR file that you plug into Drovio Server and configure. You will need to create an Azure Bot and an app in Microsoft Entra ID.

Register the bot in Azure

The bot registration can be done from the Azure portal, the Azure CLI, or Terraform. All three produce the same result: an app registration, a client secret, and an Azure Bot resource with the Teams channel enabled.

Create the Azure Bot resource

In the Azure portal, Create a resource, find Azure Bot, then Create.

Field Value
Bot handle A unique name, for instance Drovio
Subscription The subscription to bill
Resource group A new group, for instance Drovio
Pricing tier Free (F0)
Type of app Single Tenant
Creation type Create new Microsoft App ID

Point the bot at your server

Open the resource, then Settings and Configuration. Set the Messaging endpoint to:

https://<your server>/msteams/api/messages

Public endpoint

The /msteams/api/messages route must be reachable from Microsoft's servers over HTTPS. If your Drovio Server is behind a firewall or a reverse proxy, make sure this path is open to the internet.

Apply, then go to Settings and Channels, and add Microsoft Teams.

Collect the three values the plugin needs

Still under Settings and Configuration, click Manage Password next to the Microsoft App ID. This opens the app registration in Microsoft Entra ID.

On the Overview pane, record:

Value in Azure Used as
Application (client) ID client_id
Directory (tenant) ID tenant_id

Then go to Certificates & secrets, tab Client secrets, and click New client secret. Copy the Value column straight away: it is displayed once, and a new secret has to be generated if you miss it. This is the client_secret.

Expose the API and grant access to Graph

Under Expose an API, click Set next to the Application ID URI, then Save.

Under API permissions, add a permission on Microsoft Graph, choose Application permissions, and select the two permissions listed below. Grant admin consent for them.

Register the redirect URI

Only needed if you intend to publish the app from the administration panel rather than uploading it yourself.

Under Authentication, Add a platform, choose Web, and add the redirect URI:

https://<your server>/admin/access/msteams/publish/callback

Publishing fails with AADSTS500113: No reply address is registered for the application when it is missing. If administration access is restricted by IP address on this server, the address the browser comes back from has to be allowed as well, otherwise the return leg is refused with a 403.

Replace <your server> with the public URL of your instance, and pick your own resource group and region.

The app comes before the bot here

The portal creates the app registration and the bot in one form. The CLI cannot: az bot create takes the application ID as a required argument, so the app registration has to exist first. The end result is the same.

Set the five names first, then the rest of the commands can be pasted as they are:

Variable What it names
SERVER Public URL of your Drovio Server
APP_NAME Display name of the app registration
BOT_NAME The bot resource, which has to be free across Azure
GROUP Resource group holding the bot
LOCATION Region of the resource group
SERVER=https://<your server>
APP_NAME=Drovio
BOT_NAME=Drovio
GROUP=Drovio
LOCATION=westeurope

Create the app registration, its service principal, its Application ID URI and its client secret:

APP_ID=$(az ad app create \
  --display-name "$APP_NAME" \
  --sign-in-audience AzureADMyOrg \
  --web-redirect-uris "$SERVER/admin/access/msteams/publish/callback" \
  --query appId --output tsv)

TENANT_ID=$(az account show --query tenantId --output tsv)

az ad sp create --id "$APP_ID"

az ad app update --id "$APP_ID" --identifier-uris "api://$APP_ID"

CLIENT_SECRET=$(az ad app credential reset --id "$APP_ID" --append \
  --display-name drovio-server --query password --output tsv)

The az ad sp create line is the step the portal hides from you. It creates the service principal the permissions are granted to and admin consent fails without it. The identifier URI is what the Teams manifest points at. The client secret is displayed once, make sure to keep what the last command captures.

Ask for the two Graph permissions, resolved by name so no identifier has to be copied around. Consenting to them comes later, at the end of this tab:

GRAPH_ID=00000003-0000-0000-c000-000000000000

USER_READ=$(az ad sp show --id $GRAPH_ID \
  --query "appRoles[?value=='User.Read.All'].id | [0]" --output tsv)
CATALOG_READ=$(az ad sp show --id $GRAPH_ID \
  --query "appRoles[?value=='AppCatalog.Read.All'].id | [0]" --output tsv)

az ad app permission add --id "$APP_ID" --api $GRAPH_ID \
  --api-permissions "$USER_READ=Role" "$CATALOG_READ=Role"

This command prints a hint inviting you to run az ad app permission grant: ignore it, that command covers delegated permissions, and these two are application permissions.

Then the bot resource itself, and the Teams channel on it:

az group create --name "$GROUP" --location "$LOCATION"

az bot create \
  --name "$BOT_NAME" \
  --resource-group "$GROUP" \
  --app-type SingleTenant \
  --appid "$APP_ID" \
  --tenant-id "$TENANT_ID" \
  --endpoint "$SERVER/msteams/api/messages" \
  --sku F0

az bot msteams create --name "$BOT_NAME" --resource-group "$GROUP"

echo "client_id     $APP_ID (Application ID)"
echo "tenant_id     $TENANT_ID (Directory ID)"
echo "client_secret $CLIENT_SECRET"

Consenting is the final step, and it has to be verified. Microsoft Entra ID needs the app registration, its service principal and its requested permissions to have propagated before it can act on them. Run too early, the command reports no error and grants nothing:

az ad app permission admin-consent --id "$APP_ID"

SP_ID=$(az ad sp show --id "$APP_ID" --query id --output tsv)
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_ID/appRoleAssignments" \
  --query "length(value)" --output tsv

The last command has to print 2. If it prints 0, wait a minute and run both again. Granting the consent needs the Privileged Role Administrator or the Global Administrator role.

The portal is the reliable fallback

If the count stays at 0, open the app registration in Microsoft Entra ID, go to API permissions and click Grant admin consent for <your tenant>. The Status column then reads Granted, and the command above prints 2.

The same setup, for an infrastructure already described as code. It needs the azuread provider in version 3 or above, which is where the small composable application resources appeared. The files below form one root module.

terraform {
  required_version = ">= 1.0.0, < 2.0.0"

  required_providers {
    azuread = {
      source  = "hashicorp/azuread"
      version = "~> 3.0"
    }
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}
provider "azurerm" {
  subscription_id = var.subscription_id
  features {}
}
variable "subscription_id" {
  description = "Azure subscription hosting the bot resource"
  type        = string
}

variable "server_url" {
  description = "Public URL of the Drovio Server instance, without a trailing slash"
  type        = string
}
data "azuread_client_config" "current" {}

data "azuread_application_published_app_ids" "well_known" {}

data "azuread_service_principal" "msgraph" {
  client_id = data.azuread_application_published_app_ids.well_known.result["MicrosoftGraph"]
}
resource "azuread_application_registration" "drovio" {
  display_name     = "Drovio"
  sign_in_audience = "AzureADMyOrg"
}

resource "azuread_application_identifier_uri" "drovio" {
  application_id = azuread_application_registration.drovio.id
  identifier_uri = "api://${azuread_application_registration.drovio.client_id}"
}

resource "azuread_application_redirect_uris" "drovio" {
  application_id = azuread_application_registration.drovio.id
  type           = "Web"
  redirect_uris  = ["${var.server_url}/admin/access/msteams/publish/callback"]
}

resource "azuread_application_api_access" "drovio" {
  application_id = azuread_application_registration.drovio.id
  api_client_id  = data.azuread_application_published_app_ids.well_known.result["MicrosoftGraph"]

  role_ids = [
    data.azuread_service_principal.msgraph.app_role_ids["User.Read.All"],
    data.azuread_service_principal.msgraph.app_role_ids["AppCatalog.Read.All"],
  ]
}

resource "azuread_application_password" "drovio" {
  application_id = azuread_application_registration.drovio.id
}

resource "azuread_service_principal" "drovio" {
  client_id = azuread_application_registration.drovio.client_id
}

# The two assignments below are the admin consent
resource "azuread_app_role_assignment" "user_read_all" {
  app_role_id         = data.azuread_service_principal.msgraph.app_role_ids["User.Read.All"]
  principal_object_id = azuread_service_principal.drovio.object_id
  resource_object_id  = data.azuread_service_principal.msgraph.object_id
}

resource "azuread_app_role_assignment" "app_catalog_read_all" {
  app_role_id         = data.azuread_service_principal.msgraph.app_role_ids["AppCatalog.Read.All"]
  principal_object_id = azuread_service_principal.drovio.object_id
  resource_object_id  = data.azuread_service_principal.msgraph.object_id
}

resource "azurerm_resource_group" "drovio" {
  name     = "Drovio"
  location = "West Europe"
}

resource "azurerm_bot_service_azure_bot" "drovio" {
  name                    = "Drovio"
  resource_group_name     = azurerm_resource_group.drovio.name
  location                = "global"
  sku                     = "F0"
  microsoft_app_id        = azuread_application_registration.drovio.client_id
  microsoft_app_type      = "SingleTenant"
  microsoft_app_tenant_id = data.azuread_client_config.current.tenant_id
  endpoint                = "${var.server_url}/msteams/api/messages"
}

resource "azurerm_bot_channel_ms_teams" "drovio" {
  bot_name            = azurerm_bot_service_azure_bot.drovio.name
  location            = azurerm_bot_service_azure_bot.drovio.location
  resource_group_name = azurerm_resource_group.drovio.name
}
output "client_id" {
  description = "Application (client) ID, for the plugin configuration"
  value       = azuread_application_registration.drovio.client_id
}

output "tenant_id" {
  description = "Directory (tenant) ID, for the plugin configuration"
  value       = data.azuread_client_config.current.tenant_id
}

output "client_secret" {
  description = "Client secret, for the plugin configuration"
  value       = azuread_application_password.drovio.value
  sensitive   = true
}

Three things to know before applying this:

  • The client secret ends up in the Terraform state, which then holds a credential to your tenant and has to be protected as such. Drop the azuread_application_password resource and create the secret by hand if that is not acceptable where you keep your state.
  • The two azuread_app_role_assignment resources are what Grant admin consent does in the portal. The identity running Terraform needs the Privileged Role Administrator or the Global Administrator role to create them.
  • azurerm_bot_service_azure_bot is the Azure Bot resource. Don't use the older azurerm_bot_channels_registration, it describes the previous generation of bot registration.

A single-tenant bot serves your own tenant, which is what a self-hosted deployment needs. Multi-tenant distribution across customer tenants is a separate matter, handled through AppSource.

Whichever method you use, the bot name is between 4 and 42 characters and has to be free across Azure, so Drovio alone may be taken. The bot resource itself lives at the global location, whatever the region of its resource group.

The client secret expires

The same secret authenticates the bot when it answers in Teams and the plugin when it calls Graph. Once it expires, the app stops responding and the panel loses sight of the catalog until a new secret is entered in the administration panel. How long it lasts depends on how you created it:

Method Lifetime
Portal Your choice, 24 months at most, and Microsoft advises under 12
Azure CLI One year, --years on az ad app credential reset changes it
Terraform Two years, the default Microsoft Entra ID applies when end_date_relative is left out

Note the expiry date somewhere and plan the rotation: create the new secret first then paste it into the panel.

Free vs Standard

The Free tier (F0) allows 10,000 messages per month on premium channels, which is enough for most self-hosted deployments. Switch to Standard (S1) if you expect higher volumes.

Graph permissions

Permission What it buys Without it
User.Read.All Names and pictures of the participants The integration works, cards show no picture
AppCatalog.Read.All The panel reads the app catalog of your organization The panel says nothing about the catalog, and its button offers publishing or updating without knowing which

Both are Application permissions, and both need admin consent. The plugin asks for a fresh token every time it reads the catalog, so a consent granted while the server runs applies to the next read, with no restart to perform.

Publishing needs a third permission, the delegated AppCatalog.ReadWrite.All and it works differently. It is not declared on the app registration: the administration panel asks for it when you try to publish the app.

Install the plugin

Copy drovio-teams.jar into the packages folder of the server, then declare it in settings.conf:

"plugin_management": {
  "plugins": [
    {
      "classpath": "/opt/drovio-server/packages/drovio-teams.jar",
      "path": "com.drovio.server.teams",
      "ext": "java",
      "base_address": "drovio.server.msteams"
    }
  ]
}

The key is plugin_management. Writing plugin_manager gives a plugin that never loads, and no error says so. The base_address is drovio.server.msteams, which the Drovio application and the server both address in code.

Restart the server. A Microsoft Teams entry appears in the administration panel, and only appears when the plugin is loaded.

Why a restart here

Most of the Drovio Server configuration is applied live, but plugins are loaded at startup only. Any change to plugin_management, this plugin included, takes effect on the next restart.

Configure the integration

Open the Microsoft Teams entry of the administration panel.

Field Where it comes from
Status Enabled or Disabled
Application ID The client_id recorded in Azure
Client secret The secret you copied
Tenant ID The tenant_id recorded in Azure
Launch URI Prefilled from the server URL
Join URI Prefilled from the server URL

Saving writes the values into the server configuration and takes effect on the next call, with no restart to perform. The client secret is never displayed again: leaving the field empty keeps the stored one.

Before 3.6.6, changing the Application ID needed a restart

Earlier versions read it once when the server started, so the bot kept checking incoming calls against the previous one and answered every request with 401, logging Bot authentication failed: Invalid JWT audience.

Turning the status to Disabled stops the bot and the actions coming from the Drovio application, while leaving this panel available to configure the integration.

Configuration reference

These are the keys of the msteams element of settings.conf, should you prefer to edit it directly.

The plugin writes this element itself on the first startup that finds none, so a server can be deployed with the plugin and configured entirely from the panel.

Key Default Meaning
enabled false Enable the integration. The default if missing is false
client_id Application (client) ID of the bot
client_secret Client secret of the bot
tenant_id Directory (tenant) ID. Empty selects a multi-tenant bot (deprecated on Microsoft end as of July 2025)
launch_uri <server url>/launch Where a Teams card sends a user to start a call
join_uri <server url>/join/app Where a Teams card sends a user to join a call

Build and publish the Teams app

The app is a zip archive holding the manifest and the icons. The App package section of the admin panel allows to build it.

Field Notes
Application ID Prefilled from the configuration
Application ID URI Prefilled with api://<app id>. Change it only if Entra ID exposes another one
Version Shipped with the plugin, and displayed for information. Updating the plugin is what publishes a new version

Download gives you the archive to upload yourself, through the Teams admin center.

The other button signs you in to Microsoft and puts the app in the catalog of your organization directly. Its label follows what the catalog holds: Publish when the app is not there, Update to x.y.z when it holds an older version, and Up to date when there is nothing to send. Reading that state is what AppCatalog.Read.All buys. Without it the button reads Publish or update and covers both cases, which works just as well.

Publishing requires an account holding the Teams administrator role: any other account can only submit the app for review, which then waits for an approval in the Teams admin center. Nothing of that sign-in is kept: publishing again signs in again.

The catalog reflects the app management policies of the tenant which Microsoft applies within 24 to 48 hours of a publication. An app published moments ago may therefore not be listed yet and the panel may keep offering to publish it.

Allow the app and make it easy to find

In the Teams admin center, go to Teams apps then Manage apps, search for Drovio, and set its status to Allowed.

The app displays the profile picture of the participants. On the Permissions tab, click Review permissions and grant them.

To put the app in front of your users, go to Teams apps then Setup policies. Edit the Global (Org-wide default) policy, or create one and assign it to a group. Under Pinned apps, add Drovio, and order it under the Messaging extensions scope.

Things to know about pinning policies

  • The app opens from the + of the compose box. The current Teams client no longer shows message extension icons next to the compose box. There is no icon to pin in front of it. The policy sets the order inside the + menu, which is what puts Drovio first.
  • A policy change takes a few hours to reach the clients, and so does a rollback.
  • Leaving User pinning off removes the pins your users made themselves.