Teams WebHooks

Eine sehr interessante und einfache API zur Interaktion mit Teams sind die Webhooks. Auf dieser Seite stelle ich die Funktionsweise anhand von Beispielen vor.

Aufgabenstellung

Die Arbeit in Teams mit dem normalen Client ist nichts besonderes. Auch weitere Dienste als Webseite über eine URL einzubinden oder einen der Microsoft Connectoren zu nutzen, die z.B. einen RSS-Feed regelmäßig abfragen, geht noch einfach von der Hand. Aber es gibt Anforderungen, bei denen etwas mehr Interaktion und schnellere Reaktion wünschenswert ist. Es gibt dazu natürlich auch die Funktion eines Teams Bot, der von einem Anwender kontaktiert werden kann mehr oder weniger interaktiv mit dem Anwender kommuniziert. eine Bot-Entwicklung ist aber etwas umfangreicher und es gibt sehr oft die Anforderung, etwas dazwischen nutzen zu können, z.B.

  • Ich möchte eine Nachricht in einem Kanal posten
    Das kann z.B. ein Monitoring-System sein, welches einen Status dort meldet. Auch ein Helpdesk-System könnte dort ein neues Ticket bereitstellen und ein Anwender kann dieses dann "annehmen". Eine gewisse Interaktion zur Reaktion wäre interessant
  • Benachrichtigung bei Aktionen im Kanal
    Auch in der Gegenrichtung kann es interessant sein, Meldungen in einem Kanal automatisch zu erfassen. Das unterscheidet sich von einem Bot, der aktiv angesprochen wird. Über WebHooks kann ein Prozess quasi informiert werden, wenn in einem Channel was passiert ist. Denkbar sind Aktionen zu Compliance, Archivierung, Reporting aber auch die Funktion als trigger

Die Optionen sind Vielfältig und anders als bei Graph oder anderen "high level APIs", die man am besten mit entsprechenden Libraries nutzt, sind Webhooks einfache HTTP-Aufrufe. Die sind so einfach, dass Sie sogar von Kleinstrechner wie einem  Arduino oder ESP8266 oder aus Sprachen wie PHP, Perl, JavaScript etc. angesprochen werden können, für die es keine Libraries gibt.

Incoming Webhook einrichten

Zuerst beschäftige ich mich mit den eingehenden WebHooks. Dazu gibt es schon einige Hinweise im Internet und der Weg ist sehr einfach. Sie müssen sich zuerst in einem Team-Kanal einen WebHook erstellen. Dazu gehe ich in das Team unter Apps.

Dort suche ich dann auf "Weitere Apps" nach den WebHooks und schon nach den ersten Buchstaben sollten sie den Eintrag sehen:

Ansonsten können Sie auch direkt den Link https://teams.microsoft.com/l/app/203a1e2c-26cc-47ca-83ae-be98f960b6b2 ansurfen. Im nächsten Dialog müssen Sie das Team auswählen, in dem der Webhook installiert wird.

Nach dem Klick auf "Öffnen" wählen Sie dann den Kanal aus:

Damit sind wir aber noch nicht fertig. Sie können dann noch ein Bild für die Nachrichten hinterlegen

Erst mit den Klick auf "Erstellen bekommen Sie dann eine sehr lange URL, die nicht mal in das Fenster passt aber hier kopiert werden kann.

Hier ein Beispiel:

https://outlook.office.com/webhook/ee883945-c42f-4703-836f-5f6087e4e408@de21c301-a4ae-4292-aa09-6db710a590a6/IncomingWebhook/77e0b356b3a5469987fe0474411841d2/b39bb717-ea64-46cd-ab57-00186effe82c

Aufbau
https://outlook.office.com/webhook/<guid des Office Group>@<TenantID>/IncomingWebhook/<zeichenfolge>/<guid>

Zwei Dinge finde ich da interessant:

  • Hostname outlook.office.com
    Der Hostname für den Service ist nicht teams.com oder teams.microsoft.com sondern eigentlich der Exchange Zugang. Das ist aber auch logisch, denn Nachrichten in Kanälen liegen ja in Office Groups und das ist ein Ablageort in Exchange.
  • Keine Authentifizierung
    Die Angabe der URL reicht komplett aus. Unsicher ist das aber nicht, denn auch die langen Zeichenketten können ja wie Anmeldeinformationen gewertet werden und sind sicher, solange niemand den HTTPS-Kanal aufbricht. Wer die URL kennt kann natürlich Daten so in den Kanal senden.

So eine Veränderung bleibt natürlich nicht unbemerkt. Im Unterhaltung-Kanal landet eine entsprechende Meldung

Und auch auf dem Team-Level erscheint die neue "App"

Incoming Webhook verwalten

Bereits eingerichtete Webhooks können Sie über die Konfiguration der Connectoren auf dem Kanal sehen.

Hier lassen Sie sich dann die bereits konfigurierten Connectoren anzeigen und bestehende Webhooks erneut anzeigen und löschen.

Nachricht als Adaptive Card erstellen

Ehe wir nun eine Message an den Kanal senden können, müssen wir natürlich noch die Nachricht selbst erstellen. Microsoft hat sich hier eine Lösung einfallen lassen, die plattformübergreifend werden soll. "Adaptive Cards" ist der Schlüsselbegriff, über die Microsoft Benachrichtigungen nicht nur schön formatieren, sondern auch mit Interaktion versehen will und die nicht nur ein Teams, sondern auch in Outlook, Auf Webseiten und anderen Plattformen immer passende angezeigt werden sollen.

Skype for Business is not on our roadmap at this time as Microsoft's investments on collaboration software is targeted at Microsoft Teams, which now fully supports adaptive cards.
Quelle: Kommentare auf https://docs.microsoft.com/en-us/adaptive-cards/

Adaptive Cards in Bots, Windows, Outlook and your own applications : Build 2018
https://www.youtube.com/watch?time_continue=48&v=GJkep8wToVA

Für die Erstellung von Adaptive Cards gibt es verschiedene Editoren im Internet

Für meine Beispielanwendung habe ich mir das Leben einfach gemacht und einfach mal nur eine Zeile und einen Button mit Link generiert. Beachten Sie dabei eine Besonderheit, dass Teams ein Feld "Summary" erfordert, die von den meisten Generatoren nicht angelegt wird. Das Feld können Sie aber einfach manuell zwischen "Version" und "Body" einfügen

Ich habe dennoch mit "Action Cards" weiter gemacht:

$webhookuri = 'https://outlook.office.com/webhook/ee883945-c42f-4703-836f-5f6087e4e408@de21c301-a4ae-4292-aa09-6db710a590a6/IncomingWebhook/77e0b356b3a5469987fe0474411841d2/b39bb717-ea64-46cd-ab57-00186effe82c'
$body = @"
{
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "type": "AdaptiveCard",
    "version": "1.0",
    "summary": "Update der MSXFAQ", 
    "body": [
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "width": "32px",
                    "items": [
                        {
                            "type": "Image",
                            "width": "32px",
                            "horizontalAlignment": "Center",
                            "url": "https://www.msxfaq.de/images/kachel/msxfaq70x70.png",
                            "altText": "Trello Logo"
                        }
                    ],
                    "type": "Column"
                },
                {
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "MSXFAQ-Update"
                        }
                    ],
                    "type": "Column"
                }
            ]
        },
        {
            "type": "ColumnSet",
            "spacing": "Large",
            "separator": true,
            "columns": [
                {
                    "width": "32px",
                    "items": [
                        {
                            "type": "Image",
                            "width": "32px",
                            "style": "Person",
                            "horizontalAlignment": "Center",
                            "url": "https://www.msxfaq.de/images/logbuchn.gif",
                            "altText": "Neue Seite: Samplecode"
                        }
                    ],
                    "type": "Column"
                },
                {
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Es gibt was neues auf der MSXFAQ"
                        }
                    ],
                    "type": "Column"
                }
            ]
        }
    ],
    "actions": [
        {
            "type": "Action.OpenUrl",
            "title": "Webseite öffnen",
            "url": "https://www.msxfaq.de/sonst/logbuch.htm",
            "iconUrl": "https://www.msxfaq.de/images/kachel/msxfaq70x70.png"
        }
    ]
}
"@

$result = invoke-webrequest `
   -method POST  `
   -uri $webhookuri `
   -contenttype 'application/json' `
   -body $body

Die Rückgabe ist unspektakulär und im Body steht einfach nur eine "1" drin, was Sie als "Wahr" oder Erfolg interpretieren können:

PS C:\> $result.RawContent

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
request-id: 54f99598-9fc5-4924-b12f-7af4fbf4be69
X-CalculatedBETarget: DB8PR04MB5641.eurprd04.prod.outlook.com
X-BackEndHttpStatus: 200
X-AspNet-Version: 4.0.30319
X-CafeServer: DB6P191CA0016.EURP191.PROD.OUTLOOK.COM
X-BEServer: DB8PR04MB5641
X-Proxy-BackendServerStatus: 200
X-Powered-By: ASP.NET
X-FEServer: DB6P191CA0016
X-MSEdge-Ref: Ref A: 280814FC49894AB39360465BA0349FE7 Ref B: FRAEDGE0616 Ref C: 2019-09-07T11:38:31Z
Date: Sat, 07 Sep 2019 11:38:31 GMT
Content-Length: 1
Content-Type: text/plain; charset=utf-8
Expires: -1

1

Allerdings entsprach die Anzeige im Team dann immer noch nicht genau meinen Vorstellungen:

Da ist auf jeden Fall noch Feintuning erforderlich.

Fehlerbehandlung

Ich verzichte mal auf all die Fehler, die nicht direkt mit Teams etwas zu tun haben, z.B. fehlende Namensauflösung, Probleme beim HTTPProxy oder Firewall, PowerShell Code Signing u.a. Wenn Sie grundsätzlich schon mal einen HTTP-Post zu outlook.office365.com absetzen können, dann kann es fast nur noch an der URL oder dem Payload liegen

Wenn ihre eigene Message nicht auf Anhieb funktioniert, dann gibt es mehrere Ansatzpunkte:

Fehler Lösung
Invalid webhook URL

Klassischer Cut-Copy-Paste-Fehler. Die URL ist nicht korrekt. Prüfen Sie noch mal die komplette URL, nicht dass Sie abgeschnitten wurde.

Summary or Text is required.

Sie haben entgegen meiner Beschreibung vergessen, zwischen den Properties "Version" und "Body" eine "Summary

Bad payload received by generic incoming webhook.

JSON ist einfach aber Menschen überlesen doch mal das ein oder andere Sonderzeichen. Wenn ich meine Message in der Variable "$Body" habe, dann kann ich einfach folgende Funktion nutzen, um die Gültigkeit des JSON-Strings zu prüfen.

$body | convertfrom-json -asHashtable

Name                           Value
----                           -----
actions                        {System.Collections.Hashtable}
body                           {System.Collections.Hashtable, System.Collections.Hashtable}
version                        1.0
type                           AdaptiveCard
summary                        Update der MSXFAQ
                               http://adaptivecards.io/schemas/adaptive-card.json

Sollt hier schon ein Fehler vorliegen, dann haben Sie irgendwo ein Zeichen vergessen, z.B. ein Komma am Ende der Zeilen etc.

IoT und WebHooks

Aufgrund der sehr einfachen Funktion dieser Webhooks ganz ohne Username, Kennwort o.ä. bietet es sich gerade zu an, diese Funktion auch in Geräten zu verwenden, die andere APIs nicht nutzen können. Sie könnten den Code in PHP-Seiten ihrer Webseite einbinden und z.B. einen "Form Post" eines Kontaktformulars so in einen Teams Channel senden. Interessant ist aber auch, dass noch kleinere Systeme entsprechende Meldungen generieren können. Theoretisch könnten Sie auch einen MQTT zu Teams-Gateway bauen, bei dem ein Prozess einen "Subscribe" auf bestimmte MQTT-Kanäle macht und die Werte dann in Teams sendet.

Das sind natürlich alles Beispiele, die ihre Daten am besten in ein Monitoring-System einspielen und nicht direkt zu Teams. Aber auch Monitoring-Systeme könnten solche Meldungen dann zu Teams weiter leiten.

Aktuell habe ich noch keinen konkreten Fall, um mit meinen Arduinos, ESP8266, ESP32 u.a. mal einen Webhook von Teams zu füttern. Aber mit den entsprechenden HTTP-Libraries ist das sehr einfach umzusetzen.

Outgoing Webhook

Auch die Gegenrichtung ist möglich. Dazu müssen Sie einen WebService erstellen und über eine öffentlich erreichbare URL publizieren, die dann Teams bei einer Änderung im Kanal aufruft. Dies ist neben der Entwicklung eines Bot eine einfache Möglichkeit Informationen aus einem Kanal zu erhalten und sogar umgehend darauf zu reagieren. Die Antwort auf den HTTP-Request erscheint direkt in der Konversation zur Anfrage. Allerdings muss ihr Service diese Antwort innerhalb von 5 Sekunden zurück senden. Zudem hat der Service keine Möglichkeit proaktiv in einem Kanal aktiv zu werden und er kann zwar Cards zurücksenden, die aber keine Action-Buttons enthalten dürfen. Voraussetzung ist natürlich, dass ihr Service erreichbar ist. Es gibt hier nach meinem Wissen kein Queuing oder Retry-Versuche. Wenn ihr Service offline ist, dann verpassen Sie entsprechende Meldungen

Solche Einschränkungen sind sicher auch als Schutz gedacht. Denn missbrauch ist schon denkbar, dass jemand einen Office 365 Test-Tenant nutzt und über die URL eines Outgoing Webhook eine ganz fremde Seite angreift oder versucht die Teams-Plattform zu überlasten. Bei Exchange gab es da über den Jahreswechsel 2018/2019 ja auch einen Issue, dass EWS Callback-URls sogar als Sicherheitslücke genutzt werden konnten.

Die Einrichtung kann wieder der Besitzer eines Teams selbst machen. Sie gehen wieder in das Team und dann auf Apps und finden ganz unten fast unsichtbar am Rand den Punkt "Ausgehenden Webhook erstellen"

Im ersten Dialog ist die URL der wichtigste Punkt. Name und Beschreibung dienen eher der Dokumentation:

Der Link mit weiteren Informationen geht nur auf "https://teams.microsoft.com/_". Im nächsten Dialog liefert ihnen Teams dann quasi das "Kennwort", mit dem sich

Der Link hier geht auf https://aka.ms/microsoftteamscustombotssecurity/, welcher im Sep 2019 aber auch nur auf https://developer.microsoft.com/en-us/microsoft-teams weiter geleitet hat. Dieses Token sollten Sie sich merken, da es später nicht mehr sichtbar ist. Teams sendet es als Authentifizierung mit jedem Request an ihre URL und ihr Applikation sollte dieses Token natürlich prüfen.

Technisch liefert Teams einen HMAC-Code im Header des Requests. Um diesen selbst zu errechnen, müssen Sie den Body in ein UTF8 byte array wandeln und einen SHA256 HMAC Hash mit dem Security Token erstellen und das Ergebnis vergleichen. Wenn das Ergebnis stimmt, dann kam der Request von Teams. Das Security Token geht also nie über die Leitung. Dennoch sollten Sie die Verbindung per HTTP verschlüseln.

Auch ein ausgehender Webhook erscheint dann wieder in der App-Übersicht des Team.

Ich kann hier aber nur Namen, URL und Beschreibung ändern aber habe keinen Zugriff mehr aus das Secret.

Der ausgehende Webhook wird also nicht pro Kanal sondern für das komplette Team gebunden. Der Webhook erscheint nun auch als mögliche Empfänger und genau das ist der Trigger.

Ich kann über den Weg eine Message in jedem beliebigen Kanal über eine "@Erwähnung" (Mentioning) auch dem Webhook mitteilen. Ich habe dazu einen ganz trivialen Webhook-Responder in PHP geschrieben und temporär auf der MSXFAQ hinterlegt:

<?php
// Header setzen
header_remove();
http_response_code(200);
header("Cache-Control: no-transform,public,max-age=300,s-maxage=900");
header('Content-Type: application/json');
header('Status: 200');

// JSON Body ausgeben
echo '
{
    "type": "message",
    "text": "Webhook received"
}';
?>

Das PHP-Script prüft nun natürlich weder den HMAC Header noch macht es einen SHA256 Hash und es wertet schon gar nicht die Daten im Request aus um entsprechend sinnvoll zu reagieren. Es ist einfach nur ein Sample zur Anzeige der Antwort in Teams. So sieht dann die Antwort aus

Sollte der WebHook nicht reagieren, dann bekomme ich das in wenigen Sekunden mit:

Ein Webhook ist also nicht so eng eingebunden wie ein Teams Bot aber für die ein oder andere Funktion durchaus ausreichend und viel einfacher zu implementieren. Wer aber eine engere Integration möchte, muss sich eher einem BOT zuwenden.

Ein Beispiel dazu habe ich aber nicht entwickelt. Sicher könnte ich per PowerShell einfach einen Webserver betreiben, aber er muss ja von Teams aus dem Internet erreichbar sein. Dazu benötigt er einen DNS-Namen, eine IP-Adresse und ein Zertifikat. Solche Dienste werden daher eher als Service oder VMs in Azure gehostet oder im eigenen RZ mit Firewall und Loadbalancer hochverfügbar bereitgestellt und sind doch sehr spezifisch.

Meine Entwickler-Kollegen bei Net at Work können ihnen aber hier gerne bei der Analyse ihrer Anforderungen und der Entwicklung und Bereitstellung solchen Lösungen helfen.

Push only, kein Polling

Sowohl ausgehende Webhooks als auch eingehende Webhooks sind getriggerte Aktionen. Ein eingehender Webhook kann einfach eine Meldung in einen Kanal ablegen. Ein ausgehender Webhooks kann auf eine neue Meldung quasi nur in Echtzeit reagieren und genau eine Antwort darauf liefern. Beide Schnittstellen erlauben aber weder einen Zugriff als frühere Chats und Nachrichten noch ein Polling durch einen Client. Es fehlt also die Funktion, wie diese in EWS vorhanden ist, damit ein Service nach einer Unterbrechung z.B. frühere Nachrichten aufarbeiten kann. Dies ist nicht das Ziel von Webhooks. Für solche Anforderungen sind anderen APIs, insbesondere die Graph API erforderlich. Diesen APIs fehlt dann natürlich die "Echtzeit-Komponente".

Weitere Links