Teams HTTP-Analyse

Auf dieser Seite versuche ich etwas hinter die Kommunikation des Teams Clients zum Teams Backend zu schauen. Anders als früher Outlook-RPC nutzen quasi alle modernen Applikationen das Protokoll HTTPS und wenn der Hersteller kein "Certificate Pinning" umgesetzt hat, kann ich mit Fiddler und anderen Tools mich dazwischen klemmen und etwas zuschauen.

Push-Benachrichtigung /events/poll

Die einfachste Aufgabe sollte es sein, die "Push"-Meldungen zu erkennen. Wenn Teams "untätig" ist, dann muss es ja dennoch sofort auf eingehende Nachrichten oder Telefonanrufe reagieren. Da mein Teams aber hinter Firewalls, NAT-Router und Proxy-Server versteckt sein kann, ist es dem Server unmöglich eine Verbindung zum Teams Client aufzubauen. Daher kann es nur so laufen, dass der Teams-Client eine HTTPS-Verbindung startet und der Server einfach verspätet darauf antwortet.

Solche "ausstehenden" HTTP-Antworten kennen wir ja schon von "Push-Mail" bei ActiveSync und sind keine Besonderheit mehr. In Fiddler erkennen Sie dies an einem Request, zu dem es noch keinen Statuscode gibt und die Datenmenge mit 0 Bytes angezeigt wird.

Outlook nutzt das gleich Prinzip für EWS und Postfachzugriffe:

Der Request ist sehr einfach gehalten und ist ein HTTP-GET ohne Body. Einzige die URL und natürlich die Authentication im Header sind relevant. (Umbrüche addiert)

GET /users/8:orgid:b39bb717-xxxx-xxxx-xxxx-xxxxxxxxxxxx/endpoints/0fd4abdf-b1xxxx-xxxx-xxxx-xxxxxxxxxxxx/events/poll HTTP/1.1
Host: uksouth-prod.notifications.teams.microsoft.com
Connection: keep-alive
clientinfo: os=windows; osVer=NT 10.0; proc=x86; lcid=en-US; deviceType=1; country=US; 
              clientName=skypeteams; clientVer=1.0.0.2021121106; utcOffset=+01:00; timezone=Europe/Berlin
behavioroverride: redirectAs404
x-ms-query-params: cursor=1639746960&lsid=xxxxxxxxxxx&lsv=xxxxxxxx&epfs=srt&sca=0&activeTimeout=135
authentication: skypetoken=xxxxxxxxxxxxx
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) 
            Teams/1.4.00.32771 Chrome/85.0.4183.121 Electron/10.4.7 Safari/537.36
Accept: */*
Origin: https://teams.microsoft.com
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://teams.microsoft.com/statics/hashed/precompiled-shared-worker-9b39bc9d9a084828.js
Accept-Encoding: gzip, deflate, br
Accept-Language: en-us

Die URL enthält die OrgID und meine Endpunkt-ID. Der direkte Aufruf der URL ohne Authentication liefert einen "401 Uauthorized" mit folgender JSON-Meldung

{
   "Code":"UnAuthorized",
   "Subcode":"None",
   "Message":"No Skype token or certificate. No authentication can be done. Returning UnAuthorized."
}

Interessant finde ich hier die Übermittlung im Feld "clientinfo", über welches Microsoft quasi indirekt mit jedem Request die Version erkennen kann.

Authentication skypetoken

Auf der Suche nach der Authentication finde ich aber kein "authentication: Bearer" sondern ein "authentication: skypetoken". Die Payload ist aber ebenfalls ein OAUTH-Token, welches ich mit jwt.io, jwt.ms und anderen Webseiten schnell decodieren kann:

Warnung: So ein Token ist der "Schlüssel" zu den Daten. Geben Sie nie ein Token weiter, welches noch gültig ist. jwt.io und jwt.ms haben zum Zeitpunkt der Analyse die Decodierung lokal per JavaScript gemacht und nicht an den Server gesendet. Das kann sich aber auch mal ändern.

Ein genauerer Blick auf die "Claims" zeigt, dass das Token etwa 20h und 45 Min gültig ist.

Erst dann muss der Teams Client das Token erneuern. Allerdings ist die Signatur des Token ungültig oder die von mir verwendeten Decoder verstehen die Signatur nicht.

Antwort nach 40 Sek

Fiddler protokolliert auch mit, wie lange so ein Request dauert. Über die Karteikarte "Statistics" kann ich gut sehen, wann der Request beim Server angekommen und wann die Antwort geliefert wurde:

So lange lässt der Server den Client quasi im "Unklaren". Das stimmt natürlich so nicht, denn sobald auf dem Server eine Nachricht vorliegt, die der Client "sehen" muss, kommt die Antwort sofort. Die 40 Sekunden sind wahrscheinlich ein Kompromiss zwischen

Dieses Verfahren gilt erst einmal für den Teams Windows Client und den Teams Browser Client. Bei den mobilen Clients wäre es natürlich kontraproduktiv, mindestens alle 40 Sekunden einen Request mit ca. 2 kByte zu senden. Neben den 4,3 MB/Tag würde das wohl auch auf die Batterieleistung gehen. Hier gibt es dann schon "Push"-Funktionen, über die der Client das Smartphone aufweckt. Das habe ich aber noch nicht weiter untersucht, da der Server "in der Cloud" steht und daher ein Mitschnitt zwischen Smartphone im LTE-Netz und Server in der Cloud nur über Proxy-Server in der Cloud funktionieren würde. Ein Smartphone zuhause per WLAN könnte sich ja anders verhalten.

Die Antwort ist aber mit 812 Bytes nur halb so groß wie der ausgehende Request. Das fehlende SkypeToken ist sicher ein Faktor. (Inhalt umgebrochen)

HTTP/1.1 200 OK
Cache-Control: no-store,no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Vary: Origin
Server: Microsoft-HTTPAPI/2.0
X-Content-Type-Options: nosniff
Access-Control-Expose-Headers: X-Content-Type-Options,Cache-Control,
                               Pragma,ContextId,Content-Length,Connection,MS-CV,Date
x-ms-environment: UK South-prod,_cnsVMSS23_7
MS-CV: 20OC9nnOqEGtK6j1lRtAWg.0
x-ms-latency: 40063.3412
Timing-Allow-Origin: *
Access-Control-Allow-Origin: https://teams.microsoft.com
Access-Control-Allow-Credentials: true
Date: Fri, 17 Dec 2021 13:16:40 GMT
Content-Length: 208

{"next":"https://uksouth-prod.notifications.teams.microsoft.com/users/
                 8:orgid:b39bb717-xxxx-xxxx-xxxx-xxxxxxxxxxxx/endpoints/
                 0fd4abdf-xxxx-xxxx-xxxx-xxxxxxxxxxxx/events/poll?
                 cursor=1639xxxxxx&epfs=srt&sca=0"}

Es ist ein "200 OK" und die Payload enthält einfach die URL, die der Client für den nächsten Request verwenden sollte. Allerdings nutzt mein Client nur die URL aber entfernt im nächsten Request die Parameter "cursor=1639746961&epfs=srt&sca=0". Der "Cursor" ist aber eine Zahl, die mit jeder Antwort um 1 erhöht wird. Vermutlich kann damit der Client ermitteln, ob Antworten verloren gegangen sind oder nicht.

Eingehende Nachricht

Gespannt war ich nun auf die erste eingehende Nachricht. Von einem anderen Benutzer habe ich mir eine Instant Message senden lassen.

{
   "next": "https://uksouth-prod.notifications.teams.microsoft.com/users/
            8:orgid:b39bb717-xxxx-xxxx-xxxx-xxxxxxxxxxxx/
            endpoints/0fd4abdf--xxxx-xxxx-xxxx-xxxxxxxxxxxx/events/poll?
            cursor=1639xxxxxxx&lsid=xxxxxxxxxx&lsv=xxxxxxx&epfs=srt&sca=0",
   "eventMessages": [
      {
         "time": "2021-12-17T13:14:50.2288877Z",
         "type": "EventMessage",
         "resourceLink": "https://notifications.skype.net/v1/users/ME/conversations/
                           19:242a0513-xxxx-xxxx-xxxx-xxxxxxxxxxxx_
                           b39bb717-xxxx-xxxx-xxxx-xxxxxxxxxxxx@unq.gbl.spaces/messages/1639xxxxxxxxx",
         "resourceType": "NewMessage",
         "resource": {
            "clientmessageid": "1480xxxxxxxx",
            "content": "<p>Test202112170001</p>",
            "from": "https://notifications.skype.net/v1/users/ME/contacts/
                     8:orgid:242a0513--xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            "imdisplayname": "Carius, Frank2",
            "id": "1639xxxxxxxx",
            "messagetype": "RichText/Html",
            "originalarrivaltime": "2021-12-17T13:14:50.208Z",
            "properties": {
               "importance": "",
               "subject": "",
               "languageStamp": "languages=en:100;fr:90;it:88;&detector=Bling"
            },
            "sequenceId": 2,
            "version": "1639xxxxxxxx",
            "composetime": "2021-12-17T13:14:50.208Z",
            "type": "Message",
            "conversationLink": "https://notifications.skype.net/v1/users/ME/conversations/
                               19:242a0513-xxxx-xxxx-xxxx-xxxxxxxxxxxx_
                                b39bb717-xxxx-xxxx-xxxx-xxxxxxxxxxxx@unq.gbl.spaces",
            "to": "19:242a0513-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                   _b39bb717-xxxx-xxxx-xxxx-xxxxxxxxxxxx@unq.gbl.spaces",
            "contenttype": "text",
            "threadtype": "chat",
            "isactive": true
         }
      }
   ]
}

Hier sind schon alle Informationen enthalten, dass Teams das Popup anzeigen kann. Ich sehe auch den Displayname des Absenders und sogar die Message in "content": "<p>Test202112170001</p>"

Wenn Sie solche Traces anfertigen, sollten Sie immer eindeutige Strings verwenden, die in Fiddler und anderen Tools einfach per Volltextsuche gefunden werden können.

Das ist aber nur die "Benachrichtigung". Teams nutzt diese Info zur Anzeige des Popup aber dann folgen sehr schnell weitere Requests um Zusatzinformationen zu laden, ehe dann wieder der "Ruhepuls" eintritt

Gelb markiert sind hier die Requests, in denen der Text "Test202112170001" vorkommt.

Eingehender Anruf / CallRecord

Aktuell habe ich noch nicht gefunden, wie Teams einen eingehenden Call meldet. Die Meldung des Anrufs ist zumindest keine Antwort auf einen ausstehenden HTTP-Request. Da muss der Teams-Client noch eine andere Kommunikationsebene nutzen, die zumindest in Fiddler noch nicht analysiert wird. Vielleicht ignoriert dieser Teil die Proxy-Einstellungen, wenn eine Verbindung auch ohne Proxy möglich ist. Aber ich finde zumindest dann die Meldung zu einem "CallLog" als Antwort auf die Anfragae

{
   "next": "https://uksouth-prod.notifications.teams.microsoft.com/
                       users/8:orgid:b39bb717-xxxx-xxxx-xxxxxxxxxxxx/
					   endpoints/0fd4abdf-xxxx-xxxx-xxxxxxxxxxxx/events/poll
					   ?cursor=1640271030&lsid=xxxxxx&lsv=xxxxxx==&epfs=srt&sca=5",
   "eventMessages": [
      {
         "time": "2021-12-23T14:50:30.1323566Z",
         "type": "EventMessage",
         "resourceLink": "https://notifications.skype.net/v1/users/ME/conversations/48:calllogs/messages/1640xxxxxxxxx",
         "resourceType": "NewMessage",
         "resource": {
            "clientmessageid": "1640271030032",
            "content": "Call Logs for Call 1c845145-xxxx-xxxx-xxxxxxxxxxxx",
            "from": "https://notifications.skype.net/v1/users/ME/contacts/8:orgid:b39bb717-xxxx-xxxx-xxxxxxxxxxxx",
            "imdisplayname": "",
            "id": "1640xxxxxxxxx",
            "messagetype": "Text",
            "originalarrivaltime": "2021-12-23T14:50:30.097Z",
            "properties": {
               "call-log": "{
                   \"startTime\":\"2021-12-23T14:50:17.634098Z\",
                   \"connectTime\":\"2021-12-23T14:50:23.9313098Z\",
                   \"endTime\":\"2021-12-23T14:50:30.0124817Z\",
                   \"callDirection\":\"incoming\",
                   \"callType\":\"twoParty\",
                   "callState\":\"accepted\",
                   \"userParticipantId\":\"357829b9-xxxx-xxxx-xxxxxxxxxxxx\",
                   \"originator\":\"4:+49160xxxxxxxx\",
                   \"target\":\"8:orgid:b39bb717-xxxx-xxxx-xxxxxxxxxxxx\",
                   \"originatorParticipant\":{
                      \"id\":\"4:+49160xxxxxxxx\",
                      \"type\":\"default\",
                      \"displayName\":\"Carius, Frank (Mobil)\"
                   },
                   \"targetParticipant\":{
                        \"id\":\"8:orgid:b39bb717-xxxx-xxxx-xxxxxxxxxxxx\",
                        \"type\":\"default\",
                        \"displayName\":null
                    },
                    \"callId\":\"1c845145-2ee0-47cc-b9ef-2b1d064485f9\",
                    \"callAttributes\":null,
                    \"forwardingInfo\":null,
                    \"transferInfo\":null,
                    \"participants\":null,
                    \"participantList\":null,
                    \"threadId\":null,
                    \"sessionType\":\"default\",
                    \"sharedCorrelationId\":\"465d29a2-xxxx-xxxx-xxxxxxxxxxxx\",
                    \"messageId\":null,
                    \"spamProperties\":{
                        \"riskLevel\":\"low\"
                    }
                }",
               "s2spartnername": "concore_gvc",
               "languageStamp": "languages=en:100;nl:84;id:77;&detector=Bling",
               "importance": "",
               "subject": ""
            },
            "sequenceId": 1596465xxxx,
            "version": "1640xxxxxxxxx",
            "composetime": "2021-12-23T14:50:30.097Z",
            "type": "Message",
            "conversationLink": "https://notifications.skype.net/v1/users/ME/conversations/48:calllogs",
            "to": "48:calllogs",
            "contenttype": "text",
            "threadtype": "streamofcalllogs",
            "isactive": true
         }
      }
   ]
}

Der Teams Client bekommt also durchaus umfangreiche Daten, die aber nicht im Client angezeigt werden

Ausgehend Presence

Wenn ich ein Telefonat annehme, dann ändert sich mein Präsenzstatus. Auch das ist ein HTTP-Requests. Hier kommt aber ein klassisches "Bearer"-Token zum Einsatz und nicht das "SkypeToken" beim Benachrichtigungsdienst. Auch die Payload ist quasi "lesbar"

PUT https://presence.teams.microsoft.com/v1/me/endpoints/ HTTP/1.1
Host: presence.teams.microsoft.com
Connection: keep-alive
Content-Length: 143
x-ms-session-id: b424b37e-6aac-fd4e-edb3-053ae708a6e3
x-ms-endpoint-id: 75b54eea-ffff-ffff-4f11-fcfb6e82459f
BehaviorOverride: redirectAs404
x-ms-scenario-id: 52178
x-ms-client-env: pds-prod-azsc-euwe-01
x-ms-client-type: desktop
Authorization: Bearer xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Accept: json
x-ms-correlation-id: a85e9506-f6cf-4223-bc29-432590278987
x-ms-request-id: b424b37e6aacfd4eedb3053ae708a6e3_52179
x-ms-client-version: 27/1.0.0.2021121106
x-ms-user-type: null
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Teams/1.4.00.32771 Chrome/85.0.4183.121 Electron/10.4.7 Safari/537.36
Origin: https://teams.microsoft.com
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://teams.microsoft.com/_
Accept-Encoding: gzip, deflate, br
Accept-Language: en-us

{
   "id":"75b54eea-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
   "activity":"InACall",
   "availability":"Busy",
   "activityReporting":"Transport",
   "deviceType":"Desktop"
}

Als Antwort kommt einfach ein HTTP 200 OK zurück

HTTP/1.1 200 OK
Server: Microsoft-HTTPAPI/2.0
x-ms-correlation-id: a85e9506-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Access-Control-Expose-Headers: Location,Content-Length,x-ms-correlation-id
Access-Control-Allow-Origin: *
Date: Thu, 23 Dec 2021 14:35:27 GMT
Content-Length: 0

Missed Call

Wenn ich den eingehenden Anruf aber nicht annehme, dann ist es nicht die Aufgabe des Clients diese Information zu verarbeiten, sondern das Backend sendet an alle Clients die Information, dass es einen verpassten Anruf gab.

{
   "next": "https://uksouth-prod.notifications.teams.microsoft.com/users/
                8:orgid:b39bb717-xxxx-xxxx-xxxx-xxxxxxxxxxxx/
                endpoints/0fd4abdf-xxxx-xxxx-xxxx-xxxxxxxxxxxx/events/poll
                ?cursor=1639746858&lsid=xxxxx&lsv=xxxxx==&epfs=srt&sca=4",
   "eventMessages": [
      {
         "time": "2021-12-17T13:14:18.2362902Z",
         "type": "EventMessage",
         "resourceLink": "https://notifications.skype.net/v1/users/ME/conversations/48:notifications/messages/1639xxxxxxxx",
         "resourceType": "NewMessage",
         "resource": {
            "clientmessageid": "16397xxxxx",
            "content": "",
            "from": "https://notifications.skype.net/v1/users/ME/contacts/8:orgid:b39bb717-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            "imdisplayname": "",
            "id": "1639746858221",
            "messagetype": "Text",
            "originalarrivaltime": "2021-12-17T13:14:18.221Z",
            "properties": {
               "activity": {
                  "activityType": "call",
                  "activitySubtype": "missedCall",
                  "activityTimestamp": "2021-12-17T13:13:56.275Z",
                  "activityId": 1044705,
                  "sourceThreadId": "",
                  "sourceMessageId": 1,
                  "sourceUserId": "4:+49160xxxxxx",
                  "sourceUserImDisplayName": "Carius, Frank",
                  "targetUserId": "8:orgid:b39bb717-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
                  "messagePreview": "",
                  "messagePreviewTemplateOption": "previewUnavailable",
                  "activityContext": {
                     "spamProperties": "{\"riskLevel\":\"low\"}",
                     "activityProcessingLatency": "21904.9812"
                  },
                  "sourceThreadIsPrivateChannel": "false"
               },
               "s2spartnername": "skypespaces",
               "importance": "",
               "subject": ""
            },
            "sequenceId": 15968xxxxxx,
            "version": "1639xxxxx",
            "composetime": "2021-12-17T13:14:18.221Z",
            "type": "Message",
            "conversationLink": "https://notifications.skype.net/v1/users/ME/conversations/48:notifications",
            "to": "48:notifications",
            "contenttype": "text",
            "threadtype": "streamofnotifications",
            "isactive": true
         }
      }
   ]
}

Allerdings ist das auch nur die Information, um den Zähler bei den entsprechenden Icons anzuzeigen oder hochzuzählen. Die eigentliche Voicemail wird so nicht angezeigt.

Voicemail

Wer mit Teams telefoniert, hat auch eine Sprachmailbox, die aber in Exchange hinterlegt ist. Entsprechend versucht Teams auch diese Informationen zu erhalten. Dabei umgeht der Teams Client aber das Teams Backend und kommuniziert direkt mit dem ExchangeOnline-Postfach. Wundern Sie sich also nicht, wenn hier manchmal eine etwas größere Anfrage erscheint, insbesondere beim Wechsel in den "Telefonbereich" oder die "Calling"-Karteikarte eines ResponseGroup-aktivieren Kanals im Team

Der Request ist ein Aufruf an die Outlook RestAPI:

GET https://outlook.office.com/api/v2.0/me/mailfolders/voicemail/messages?
        $expand=SingleValueExtendedProperties
           ($filter=PropertyId%20eq%20%27Integer%20{00020328-0000-0000-C000-000000000046}%20Id%200x6801%27
               %20or%20
                    PropertyId%20eq%20%27String%20{00020386-0000-0000-C000-000000000046}%20Name%20X-VoiceMessageConfidenceLevel%27
               %20or%20
                    PropertyId%20eq%20%27String%20{00020386-0000-0000-C000-000000000046}%20Name%20X-VoiceMessageTranscription%27)
           &$top=30
           &$select=From,Body,IsRead,Id,ReceivedDateTime,InternetMessageHeaders
Authorization: Bearer xxxxxClient-Request-Id: ca8d9c10-1f24-4ed5-8a2c-9bc460bedbe7
Accept: application/json; odata.metadata=none
x-ms-request-id: b424b37e6aacfd4eedb3053ae708a6e3_19471
x-ms-client-version: 27/1.0.0.2021121106
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) 
               Teams/1.4.00.32771 Chrome/85.0.4183.121 Electron/10.4.7 Safari/537.36
Origin: https://teams.microsoft.com
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://teams.microsoft.com/_

Interessanterweise nutzt Teams hier nicht die GraphAPI und das verwendete Bearer-Token ist für die App "Teams" zum Zugriff auf outlook.office365.com ausgestellt. Die Antwort ist dann wieder eine JSON-Struktur.

Diese Information wird von Teams dann im Telefonmenü angezeigt. Weitere Details dazu finden Sie auf Teams Voicemail.

Einschätzung

Wer Lust, Laune und etwas Zeit hat, kann mit Fiddler und anderen Tools der Kommunikation zwischen dem Teams Client und den verschiedenen Backends durchaus auf die Finger schauen. Die Daten sind gut lesbar und vielleicht kann es bei einer genaueren Fehlersuche durchaus hilfreich sein. Ich habe schon das ein oder andere Mal damit in den Rückantworten eine Fehlerbeschreibung gefunden, die der Client nicht angezeigt hat, z.B. bei Kommunikation mit Exchange per EWS und REST.

Aber das ist sicher kein Standardverfahren für 1st Level-Administratoren und schon gar nicht für Endanwender. Zumal Fiddler nicht nur installiert sondern auch als Proxy mit einer RootCA betrieben werden muss um die HTTPS-Verbindungen aufzubrechen. Das ist eine Analyse mit dem Chromium Debugger in den Teams Devtools (Siehe auch Teams Admin Center API) mit weniger Aufwand möglich.

Weitere Links