UCWA - Unified Communication Web API

Seit Lync 2013 CU1, quasi mit der Verfügbarkeit des "Mobility-Clients" gibt es eine JSON/WebAPI, mit der ein authentifizierter Client allein per HTTP/HTTP die Dienste von Lync nutzen kann. Diese offene API hat nichts mit COM, .NET, Win32API o.ä. zu tun und ist damit auch unabhängig vom Betriebssystem des Clients. Und es gibt mit Arduino, RaspberryPI, Netduino und anderen Plattformen durchaus interessante Clients für Lync, die aber kein SIP sprechen. Ehe diese Clients aber den Status eines Benutzer anzeigen oder vielleicht sogar Kurzmitteilungen senden und empfangen können, gilt es die API zu verstehen. für solche "interaktiven" Dinge ist PowerShell ein passables Werkzeug.

Diese API ist erst ab Lync 2013 verfügbar und wird z.B. von Lync 2013 Mobile intensiv genutzt. Sie steht aber auch jedem Client offen, der Lync Dienste per WebService nutzen möchte. Als Authentifizierung meldet sich der Benutzer selbst an, es ist also kein Zugriff wie UCMA, über den ein privilegierter Prozess auch

Microsoft Quellen

UCWA Helper Files für JavasScript
http://go.microsoft.com/fwlink/?LinkId=313244

UCWA und Office 365

Solange es noch "Skype for Business Online" gegeben hat, konnten Sie UCWA sogar gegen die Cloud nutzen. Mittlerweile ist das aber nicht mehr möglich. Für Microsoft Teams gibt es mit Graph eine passende API

Trace einer IPhone Suche

Als es noch wenige Quellen zu UCWA gab und meine ersten Tests nicht funktioniert haben, habe ich einfach mein iPhone mit Fiddler verbunden und die HTTPS-Kommunikation mitgeschnitten. Hier die Suche nach "geheim":

INFO APPLICATION CPersonsAndGroupsSearchQuery.cpp/1323:Remote search started. queryType=0; lookupKeyword=geheim; lookupUri=sip:geheim; lookupEmail=geheim; lookupPhone=434346

INFO TRANSPORT CAuthenticationResolver.cpp/266:Using endpoint address https://lyncweb.netatwork.de/UCWA/v1/applications/213101205102/people/search?query=geheim&limit=20 as the server address

INFO TRANSPORT TransportUtilityFunctions.cpp/638:
<SentRequest>
GET https://lyncweb.netatwork.de/UCWA/v1/applications/213101205102/people/search?query=geheim&limit=20
Request Id: 0x94869a8
HttpHeader:Accept application/vnd.microsoft.com.UCWA+xml
HttpHeader:Content-Type application/vnd.microsoft.com.UCWA+xml
HttpHeader:X-MS-Namespace internal
HttpHeader:X-MS-WebTicket xxxxxxxxxx
</SentRequest>

2013-06-26 11:36:32.894 Lync[17482:7b4d000] INFO TRANSPORT TransportUtilityFunctions.cpp/907:<ReceivedResponse>
GET https://lyncweb.netatwork.de/UCWA/v1/applications/213101205102/people/search?query=geheim&limit=20
Request Id: 0x94869a8
HttpHeader:Cache-Control no-cache
HttpHeader:Connection Keep-Alive
HttpHeader:Content-Length 265
HttpHeader:Content-Type application/vnd.microsoft.com.UCWA+xml; charset=utf-8
HttpHeader:Date Wed, 26 Jun 2013 09:36:31 GMT
HttpHeader:Server Microsoft-IIS/8.0
HttpHeader:StatusCode 200
HttpHeader:X-AspNet-Version 4.0.30319
HttpHeader:X-Ms-Namespace internal
HttpHeader:X-MS-Server-Fqdn NAWLYNC001.netatwork.de
HttpHeader:X-Powered-By ASP.NET

<?xml version="1.0" encoding="utf-8"?>
<resource rel="search" href="/UCWA/v1/applications/213101205102/people/search?query=geheim&amp;limit=20" xmlns="http://schemas.microsoft.com/rtc/2012/03/UCWA">
<property name="moreResultsAvailable">False</property>
</resource>
</ReceivedResponse>

Anmeldung mit PowerShell

Das schöne an PowerShell ist, dass Sie alle Schritte interaktiv selbst durchgehen können. Daher beschreibe ich hier sequentiell die Schritte und sie können jeden Code in der PowerShell Box z.B. über die Zwischenablage direkt ausführen.

Die Anmeldung erfolgt mit folgenden Schritten.

  1. Zugriff auf Lyncdiscover.<sipdomain>
    Der Client bekommt ohne weitere Authentifizierung einen JSON-Datensatz, aus dem uns die User-URI interessiert
  2. Zugriff auf die User-URI
    Ein anonymer Zugriff liefert hier einen 401, aber das ist erwartet, denn uns interessiert der "www-authenticate"-Header, in dem die OAuth-URI enthalten ist.
  3. Zugriff auf OAuth-Service mit Anmeldedaten
    Nun besorgen wir uns bei diesem Server mit unseren Anmeldedaten ein OAuth-Token
  4. Anmeldung an UCWA mit OAuth-Token.
    Erst dann kann ich mit dem Token die weiteren Aktionendurchführen
  5. weitere Aktionen

Da das alles per HTTPS verschlüsselt ist, ist Fiddler ein ideales Werkzeug um die Anfragen und Antworten zu analysieren. Eine erfolgreiche Verbindung, wie Sie diese gleich selbst per PowerShell aufbauen können, sieht dann in etwas so aus

Sind diese Daten nicht geheim ?
Die meisten nicht, denn wie bei Exchange Autodiscover kann über "lyncdiscover.<sipdomain> jeder den Service finden, einen Request absenden und damit die Lync Webservice URI erfahren und dann sogar noch den OAuth-Service erfahren. Um aber ein Ticket zu erhalten, muss man sich anmelden. DoS oder DDoS-Attacken sind denkbar.

Beachten Sie die Einschränkungen von Invoke-Webrequest auf PS HTTPClient, warum ich dann doch wieder die "alte" Klasse genutzt habe

Parameter für die Anmeldung

Zuerst muss das Script natürlich ein paar Parameter bekommen. können. Die habe ich als "param"-Block angegeben, damit man später bei Aufruf des Powershell diese schnell überschreiben kann

param (
   [string]$sipaddress = "frank.carius@netatwork.de",
   [string]$Username = "frank.carius@netatwork.de",
   [string]$Userpassword = $( Read-Host "Input password für User $sipaddress" )
)

Auf der Seite PS Passworte habe ich verschiedene Optionen beschrieben wie man sicher das Kennwort hinterlegen kann 

Eingang per Lyncdiscover ermitteln

Aus den Werten in der Variablen "$sipaddress" kann ich die SIP-Domain ableiten und damit die URL für Lyncdiscover generieren

[string]$sipdomain = $sipaddress.split("@")[1]
[string]$lyncdiscoverURL = ("https://lyncdiscover."+$sipdomain)
Write-host "LyncdiscoverURL: $lyncdiscoverURL"

Und dann kann ich auch schon den ersten "anonymen" Request absetzen, um die URL der Lync WebDienste zu bekommen: (gekürzt, ohne Fehlerbehandlung und daraus die UserURL holen:

$result1 = Invoke-WebRequest -uri $lyncdiscoverURL -Method GET
$jsondata = convertfrom-json $result1.content
$UCWAUserURL = $jsondata._links.User.href
Write-host "UCWAUserURL: $UCWAUserURL"

Ermitteln der OAUTH-Anmeldung

Die Nutzung von UCWA erfordert natürlich eine Anmeldung. das virtuelle Verzeichnis /OUWA) akzeptiert aber erst mal keine normale Anmeldung, sondern erwarer eine OAUTH.Token. Das bekommt ich mit dem nächsten Zugriff auf gesagt. Fiddler zeigt den GET und den 401. Wichtig ist der "WWW-Authenticate:"-Header in der Antwort.

 

Das Problem mit PowerShell ist aber, jede HTTP-Methode hier versagt. Auf PS HTTPClient habe ich die verschiedenen Optionen einer HTTP-Anfragen unter PowerShell beschrieben und alle liefern einen 401-Fehler aber keine Header.

Some Internet protocols, such as HTTP, return otherwise valid responses indicating that an error has occurred at the protocol level. When the response to an Internet request indicates an error, WebRequest..::..GetResponse sets the Status property to WebExceptionStatus..::..ProtocolError and provides the WebResponse that contains the error message in the Response property of the WebException that was thrown. The application can examine the WebResponse to determine the actual error.
Quelle Response Property https://msdn.microsoft.com/en-us/library/system.net.webexception.response.aspx

Daher ist der nächste Aufruf etwas aufwändiger:

Try {
   $result2 = Invoke-WebRequest  `
               -uri $UCWAUserURL   `
               -Method GET  `
} 
Catch [System.Net.WebException] {
   Write-host " Got a expected Web Exception
   $result2 = $_.Exception
}

if (( $result2.Response.StatusCode -eq 401) `
   -and ($result2.Response.Headers["WWW-Authenticate"] -ne $null )) {
   [string]$oauthURL = (($result2.Response.headers['WWW-Authenticate'] -split '.*MsRtcOAuth\shref="(.*?)".*'))
   $oauthURL=$oauthURL.trim()  # remove spaces
}
write-host " OAuthURL =  $oauthURL"

Damit sollte ich nun aber eine gültige OAUTH-UR- erhalten haben. Viele anderen Beispiele für UCWA sparen sich den Schritt und erraten die URL einfach aus den Hostnamen im vorherigen Request.

Form-POST für WebTicket

unter der ich mir im nächsten Schritt ein WebTicket besorgen kann., was ich umgehend tue. Hier muss ich nun quasi ein "Formular"-POST machen.

$jsonrequest = @{grant_type = "password";Username = $Username;password = $Userpass;} 
$result3 = Invoke-WebRequest `
               -uri $oauthURL  `
               -Method POST `
               -Body $jsonrequest
$jsondata3 = convertfrom-json $result3.content
write-host ("Got token:" + $jsondata3.access_token)
write-host ("Got token:" + $jsondata3.token_type)

PS C:\> $jsondata3

access_token        expires_in ms_rtc_identityscope token_type
------------        ---------- -------------------- ----------
cwt=AAEBHAEFAAAA... 28356      local                Bearer

Hier wieder die Ansicht in Fiddler:

Sind diese Daten nicht geheim ?
Sie sehen, dass ich das Kennwort natürlich unkenntlich gemacht habe. Sie wissen aber nun auch, dass HTTPS keine Kür sondern Pflicht ist. Das "access_token" für einen authentifizierten Benutzer selbst ist aber nur acht (8) Stunden gültig. Tokens für anonyme Meetings sind sogar nur eine (1) Stunde gültig. Der Client muss sich selbst darum kümmern das Token zu erneuern.
Quelle: Authentication in UCWA http://msdn.microsoft.com/en-us/library/office/dn356686(v=office.15).aspx

Das Ergebnis ist dann ein OAuth-Token, was mich für die nächste Zeit authentifiziert.

Das Token sollte ich mit lokal speicherm, wenn die Applikation z.B. angehalten oder beendet wird. Solange es gültig ist, kann die Applikation damit immer wieder die Verbindung fortsetzen und auch der Server "erkennt" die Applikation wieder und nutzt den bestehenden Status. Das ist insbesondere für eingehende Nachrichten erforderlich, die per UCWA vom Server gepuffert aber nur an die gleiche Session ausgeleifert werden.

Die Applikation sollte sich aber auch merken, wann Sie das Token angefordert hat und ablaufende Tokens erneuern bzw. nach dem Ablaufen wieder eine Anmeldung zu wiederholen.

Ermitteln der Application URI

Mit diesem Token werde ich nun erst noch mal die User-Uri ansprechen, denn noch habe ich ja keine Informationen über den Anwender und dessen Anwendungen: Zudem kann ich so schon mal sehen, ob das Token funktioniert.

$UCWAauth = @{Authorization = ($jsondata3.token_type+" "+$jsondata3.access_token);} 
Try {
   $result4 = Invoke-WebRequest `
                 -uri $UCWAUserURL  `
                 -Method GET `
                 -headers $UCWAauth
   $httperrorcode = $result4.Statuscode
} 
Catch [System.Net.WebException]{
   [System.Net.HttpWebResponse]$result4 = [System.Net.HttpWebResponse] $_.Exception.Response
   $httperrorcode  = $result4.StatusCode.Value__
   $error.clear()
}

In Fiddler ist im Header die Authentication zu sehen.

Die Ausgabe von $result4.content enthält den JSON-String, die man mit "convertfrom-json" konvertieren kann.

$jsondata4 = convertfrom-json $result4.content
[string]$UCMAAppsURL=$jsondata4._links.applications.href
write-host "UCMAAppsURL = $UCMAAppsURL"

Liste der Applikationen ermitteln

Erst jetzt kann ich mit dieser URL dann die nächste Anfragen stellen, um die komplette Liste der Dienste zu erhalten. Diesmal nutze ich die "Applications"-URI und wieder das bereits erhaltene OAuth-Token im Header.

$UCWAauth = @{Authorization = ($jsondata3.token_type+" "+$jsondata3.access_token);}
$jsonrequest5 = convertto-json @{'culture'='en-us';'endpointId'='1235637';'UserAgent'='MSXFAQ UCMA-PSSample';} 
Try {
   $result5 = Invoke-WebRequest `
                 -uri $UCMAAppsURL `
                 -Method POST `
                 -headers $UCWAauth `
                 -ContentType "application/json" `
                 -body $jsonrequest5
   $httperrorcode = $result5.Statuscode
} 
Catch [System.Net.WebException]{
   [System.Net.HttpWebResponse]$result5 = [System.Net.HttpWebResponse] $_.Exception.Response
   $httperrorcode  = $result5.StatusCode.Value__
   $error.clear()
}

Nun bin ich sicher, dass ich quasi "authentifiziert" bin und auf die Daten zugreifen kann. Sie sehen schon hier unter "JSON._embedded.me" meine eigene Information, d.h. Name, SIP-Adresse, Mailadresse etc. Weiter unten unter "people" finden Sie dann die URLs um per REST z.B. meine Kontakte abzurufen.

Wichtiger Hinweis
Die URLs hier sind "relative" URLs, in denen kein Hostname mehr enthalten ist.

Meine Kontakte auslesen

Zuerst sollten wir erst mal nur "lesend" über UCWA zugreifen. Also ist die erste Übung meine Kontakte auszulesen. Auch hierzu setze ich nun einen REST-Request auf die gelieferte URL ab. Ich muss natürlich zu jedem Request auch immer wieder das Authentifizierungstoken mit senden.

Samples

http://go.microsoft.com/fwlink/?LinkId=313244

Weitere Links