Kalender-items opvragen via Exchange - Exchange.asmx werkt niet meer

Ingediend door Dirk Hornstra op 19-dec-2022 21:02

Bij TRES hebben we op de 2e verdieping een aantal vergaderzalen. Als je een vergadering in zo'n zaal hebt, dan leg je "de zaal" zelf ook vast in Outlook. Dat werkt met een e-mailadres per zaal. Een collega heeft in het verleden ingeregeld dat een centraal mailadres/user toegang krijgt tot die mailboxen en zo de kalenders kan bekijken. Een stagiair heeft er een mooi stuk front-end voor gemaakt, zodat je een nette afspraaklijst op de ipads kunt bekijken. Je kunt ook nog bladeren, dus alvast even kijken of de zaal morgen of overmorgen beschikbaar is.

Een tijd geleden werkte het niet meer, omdat ik met de centrale user ook niet meer kon inloggen in outlook.office365.com was het duidelijk: het wachtwoord moest aangepast worden.

Het tonen van de afspraken werkte nu weer niet, maar ik kon nog wel succesvol inloggen in de mailbox, dit lijkt dus een ander probleem te zijn. Zoals je op deze pagina ziet doe je dat "gewoon" met Webcredentials: link. Klik je echter door dan kom je op deze pagina waar in het paarse blok gemeld wordt dat per oktober 2022 Basic Authentication uitgeschakeld wordt en je via OAuth moet gaan connecten. Nu was Basic Authentication volgens mij dat je zelf een header toevoegde met naam "Authentication" en de waarde "Basic base64-waarde" is, waarbij base64-waarde een base-64 waarde is van "username:password". Maar "mogelijk" dat webcredentials ook zoiets onder water doen en Microsoft het nu blokkeert? Ik heb geen idee. Maar goed, een username-password-combinatie over de lijn sturen, ook al is het HTTPS, dat wil je eigenlijk niet. Een OAuth-alternatief heeft dan natuurlijk de voorkeur.

Voor een ander project heb ik ooit in Azure Active Directory een app aangemaakt, waarmee ik de websites en tenants kan indexeren, dezelfde flow wil ik nu gaan gebruiken voor Microsoft Graph. Met de "Calendar zaken" zou ik ook de kalenders van de vergaderruimtes moeten kunnen uitlezen.

Je hebt code-bibliotheken die je kunt gebruiken, maar vaak werkt dat met een ingelogde gebruiker of iets dergelijks, dus ik ben zelf maar "door de flow heen gelopen".
Je begint met een  redirect naar een inlog-url van Microsoft met de benodigde gegevens. Als je daar succesvol inlogt (en voldoende rechten hebt) wordt je teruggestuurd naar je "redirect_uri" met een code:


var requestUrl = $"https://login.microsoftonline.com/{TenantId}/oauth2/v2.0/authorize?client_id={ClientId}&response_type=code&redirect_uri={HttpUtility.UrlEncode(RedirectUri)}&response_mode=query&scope={Scope}&state={RandomKey}&prompt=select_account";
return Redirect(requestUrl);

Met die code voer je een POST actie uit:

     var code = Request.QueryString["code"];
            if (string.IsNullOrEmpty(code))
            {
                throw new NotImplementedException();
            }
            var url =  $"https://login.microsoftonline.com/{TenantId}/oauth2/v2.0/token";

            var wr = (HttpWebRequest)WebRequest.Create(url);
            wr.Method = "POST";
            wr.ContentType = "application/x-www-form-urlencoded";
            var writer = new StreamWriter(wr.GetRequestStream());
            writer.Write($"client_id={ClientId}&scope={Scope}&code={code}&redirect_uri={HttpUtility.UrlEncode(RedirectUri)}&grant_type=authorization_code&client_secret={ClientSecret}");
            writer.Close();
            var resp = (HttpWebResponse)wr.GetResponse();
            var sr = new StreamReader(resp.GetResponseStream());
            var data = sr.ReadToEnd();
            sr.Close();
            resp.Close();

            return new ContentResult() { Content = data };

Daarmee krijg je een stuk JSON terug die ik opsla als tekstbestand in de App_Data map. Die map is vanuit een browser nooit op te vragen, dus prima om je access-token en refresh-token in op te slaan.

In de "scope" parameter geef je mee welke rechten je met dit token moet hebben. Dat zijn er maar 2: Calendars.Read offline_access
Die Calendars.Read is duidelijk: je moet Outlook agenda's kunnen lezen. Die offline_access is nodig om een refresh-token te krijgen. Je access-token is slechts een beperkte periode geldig. Je wilt natuurlijk niet elke keer weer handmatig die authenticatie-flow doorlopen. Als je token bijna verloopt doe je een aanvraag met het refresh-token in plaats van het access-token. Je ontvangt dan een nieuw access-token (die weer een tijdje geldig is) en een nieuw refresh-token.

Omdat we een aantal vergaderzalen hebben, is het natuurlijk wel handig dat één ipad het verversen doet. Anders doen misschien twee ipads het verversen van het token en zul je net zien dat de tokens die je opslaat niet meer geldig zijn, omdat een andere call de eerdere aanvraag ongeldig gemaakt heeft.

Je moet ook goed kijken welke graph-methode je aanroept. Mijn eerste acties waren niet goed, want daarbij kreeg ik herhalende afspraken niet binnen, kwamen geannuleerde afspraken niet door. Heel stuk code om afspraken van een paar jaar geleden al op te vragen om ook die herhalende binnen te krijgen. Waardoor de boel traag werd. Uiteindelijk, na wat trial-en-error op een werkende, correcte methode uitgekomen.

Even een fragment van de code hoe de data nu opgevraagd wordt:


var data = "";
var url = $"https://graph.microsoft.com/v1.0/users/{mail}/calendarView?$top=500&startDateTime={DateTime.Now.AddDays(-14).ToString("yyyy-MM-dd")}T00:00:00&endDateTime={DateTime.Now.AddDays(14).ToString("yyyy-MM-dd")}T00:00:00";
var sb = new StringBuilder();
var wr = (HttpWebRequest)WebRequest.Create(url);
wr.Headers.Add("Authorization", $"Bearer {AccessToken}");
wr.Headers.Add("Prefer", "outlook.timezone=\"W. Europe Standard Time\"");
wr.Method = "GET";
wr.ContentType = "application/json";
var wresp = wr.GetResponse();
using (var textReader = new StreamReader(wresp.GetResponseStream()))
{
     data = textReader.ReadToEnd();
}
wresp.Close();
if (string.IsNullOrEmpty(data))
{
      return appointmentList;
}

Je ziet dat ik de functie calendarView gebruik. Met een startDateTime en endDateTime om de afspraken van de afgelopen 2 weken en komende 2 weken op te vragen.
Deze staat natuurlijk wel binnen een try-catch statement om bij een verlopen token te zorgen dat er met een refresh-token een nieuw access- en refresh-token opgevraagd wordt.

Ook zie je dat ik een outlook.timezone in de headers mee geef, anders werd de tijd in UTC teruggegeven.

Mocht je een zelfde probleem hebben, dan  hoop ik dat je met deze stappen ook je applicatie weer werkend krijgt!