Office365 afspraken uit agenda ophalen ?>

Office365 afspraken uit agenda ophalen

Een probleem wat wij als ontwikkelaars tegenkomen is het verantwoorden van onze werkzaamheden. De afgelopen jaren hebben we een switch gemaakt van het plannen van uren naar het plannen van deliverables. Bij sommige van onze projecten worden onze uren verkocht in plaats van deliverables. Met name voor deze projecten is het goed om inzicht te hebben in de uren die we gemaakt hebben.

Wij houden er allemaal niet van om na afloop van een werkweek onze gewerkte uren te verwerken in het administratiepakket (AFAS), waardoor het nog wel eens voorkomt dat we een herinnering krijgen om van een aantal weken onze gewerkte uren bij te werken in het systeem.

Om beide partijen tevreden te stellen hebben we een oplossing bedacht door een webapplicatie te maken die onze agenda uitleest en de afspraken die daarin staan te verwerken naar een database. Vervolgens moet het mogelijk zijn om deze afspraken door te zetten naar het administratiepakket als urenregels, waardoor ‘het schrijven van uren’ voor ons niet meer nodig moet zijn. Ik heb hierin de eerste stap gezet door een webapplicatie te maken die de agenda kan uitlezen door middel van de Outlook REST API van Microsoft. In deze blog leg ik alle stappen uit die ik uitgevoerd heb, met als uitkomst een webapplicatie waarvoor je moet inloggen en waarbij op een pagina een lijst met de laatste 10 gemaakte afspraken getoond wordt. Ik maak gebruik van Microsoft Visual Studio 2017. Deze software is hier gratis te downloaden. Bij het schrijven van deze blog ga ik er vanuit dat je Visual Studio geïnstalleerd hebt en ook al een aantal keer hebt gebruikt.

Eerst maken we een nieuwe ASP.NET Web app aan zonder authentication.

Je app registreren

Om gebruik te maken van de REST API van Microsoft moet je je app registreren. Na het registreren van je app krijg je een ApplicationID en ApplicationSecret, waarmee altijd bijgehouden kan worden welke app toegang vraagt tot de gegevens van iemand. Je kunt een nieuwe app registreren het nieuwe Application Registration Portal of in het Azure Management Portal. Voor onze app moeten we de volgende stappen uitvoeren.

  1. Log in met je eigen gegevens in het Application Registration Portal.
  2. Klik op de link Een app toevoegen (Add an app) en voer de naam van je app in. In ons voorbeeld kiezen we hier voor ‘DevOfficeApp’.
  3. Onder de kop Toepassingsgeheimen (Application Secrets) klik je op de knop Nieuw wachtwoord genereren (Generate New Password) om een wachtwoord aan te maken die we zometeen nodig hebben voor onze app.Registration DeOfficeApp
  4. Onder de kop Platforms klik je op de knop Platform toevoegen (Add Platform). Kies hier voor Web en voer de url in van je app die je zojuist aangemaakt hebt (deze kun je makkelijk vinden door het project te selecteren en rechts onderin het eigenschappenveld staat de URL. wanneer je dit niet ziet, kun je rechtermuisknop op het project klikken en eigenschappen selecteren. onder het kopje web staat vervolgens de project url. In ons project is dit http://localhost:51870Registration DevOfficeApp
  5. Onder de kop Machtigingen voor Microsoft Graph (Microsoft Graph Permissions moeten de volgende machtigingen toegestaan worden: Calendars.Read, Mail.Read en User.Read.Registration DevOfficeApp
  6. Klik vervolgens op opslaan en kopieer de ApplicationID van deze app die we zometeen samen met de ApplicationSecret nodig hebben in onze app.

OAuth2 Implementeren in het project

We hebben OAuth2 nodig om dadelijk toegang te krijgen tot de gebruikersgegevens en vervolgens tot de agenda van de betreffende persoon. De volgende stappen zorgen ervoor dat we netjes OAuth2 implementeren in ons project.

Open de Web.Config file en voeg onderstaande sleutels toe aan de sectie <appSettings>:

<add key="ida:AppID" value="YOUR APP ID">
<add key="ida:AppPassword" value="YOUR APP PASSWORD">
<add key="ida:RedirectUri" value="http://localhost:10800">
<add key="ida:AppScopes" value="User.Read Mail.Read">

Wijzig hierbij de value van AppID en AppPassword met de gegevens die je zojuist gekopieerd hebt vanuit de registratie van je eigen app. De value van de RedirectUri stel je in op de url van je project die je zojuist ook hebt ingevoerd bij de redirectUrl bij je App.

Via de NuGet Package Manager moeten de volgende pakketjes geïnstalleerd worden:

Install-Package Microsoft.Owin.Security.OpenIdConnect
Install-Package Microsoft.Owin.Security.Cookies
Install-Package Microsoft.Owin.Host.SystemWeb
Install-Package Microsoft.Identity.Client -Pre
Install-Package Microsoft.Graph

Na de installatie gaan we terug naar het project en maken we een een map “App_Start” aan en voegen we hier een nieuw OWIN Startup Class item aan met de naam Startup.cs.

De inhoud van dit bestand kun je vervolgens overschrijven met de volgende code:

using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
using Microsoft.IdentityModel.Protocols;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using System.IdentityModel.Tokens;
using Microsoft.Owin.Security.Notifications;
using System.Configuration;
using Microsoft.Identity.Client;
using OfficeApp.TokenStorage;
using System.IdentityModel.Claims;
using System.Web;

[assembly: OwinStartup(typeof(OfficeApp.App_Start.Startup))]

namespace OfficeApp.App_Start
{
 public class Startup
 {
 public static string appId = ConfigurationManager.AppSettings["ida:AppId"];
 public static string appPassword = ConfigurationManager.AppSettings["ida:AppPassword"];
 public static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
 public static string[] scopes = ConfigurationManager.AppSettings["ida:AppScopes"]
 .Replace(' ', ',').Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

 public void Configuration(IAppBuilder app)
 {
 app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

 app.UseCookieAuthentication(new CookieAuthenticationOptions());

 app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
 {
 ClientId = appId,
 Authority = "https://login.microsoftonline.com/common/v2.0",
 Scope = "openid offline_access profile email " + string.Join(" ", scopes),
 RedirectUri = redirectUri,
 PostLogoutRedirectUri = "/",
 TokenValidationParameters = new TokenValidationParameters
 {
 ValidateIssuer = false
 },
 Notifications = new OpenIdConnectAuthenticationNotifications
 {
 AuthenticationFailed = OnAuthenticationFailed,
 AuthorizationCodeReceived = OnAuthorizationCodeReceived
 }
 });
 }

 private Task OnAuthenticationFailed(AuthenticationFailedNotification&lt;OpenIdConnectMessage,
 OpenIdConnectAuthenticationOptions&gt; notification)
 {
 notification.HandleResponse();
 string redirect = "/Home/Error?message=" + notification.Exception.Message;
 if (notification.ProtocolMessage != null && !string.IsNullOrEmpty(notification.ProtocolMessage.ErrorDescription))
 {
 redirect += "&debug=" + notification.ProtocolMessage.ErrorDescription;
 }
 notification.Response.Redirect(redirect);
 return Task.FromResult(0);
 }

 private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
 {
 // Get the signed in user's id and create a token cache
 string signedInUserId = notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
 SessionTokenCache tokenCache = new SessionTokenCache(signedInUserId,
 notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase);

 ConfidentialClientApplication cca = new ConfidentialClientApplication(
 appId, redirectUri, new ClientCredential(appPassword), tokenCache.GetMsalCacheInstance(), null);

 try
 {
 var result = await cca.AcquireTokenByAuthorizationCodeAsync(notification.Code, scopes);
 }
 catch (MsalException ex)
 {
 string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
 string debug = ex.Message;
 notification.HandleResponse();
 notification.Response.Redirect("/Home/Error?message=" + message + "&debug=" + debug);
}
}
}
}

Bovenstaande code bevat de gegevens voor het inloggen bij je Microsoft of je werk- / schoolaccount. Vervolgens maken we in onze HomeController twee Actions aan SignIn en SignOut. Deze acties zorgen ervoor dat iemand kan in- en uitloggen op de webapplicatie.

public void SignIn()
{
 if (!Request.IsAuthenticated)
 {
 HttpContext.GetOwinContext().Authentication.Challenge(
 new AuthenticationProperties { RedirectUri = "/" },
 OpenIdConnectAuthenticationDefaults.AuthenticationType);
 }
}

public void SignOut()
{
 if (Request.IsAuthenticated)
 {
 string userId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;

 if (!string.IsNullOrEmpty(userId))
 {
 // Get the user's token cache and clear it
 SessionTokenCache tokenCache = new SessionTokenCache(userId, HttpContext);
 tokenCache.Clear();
 }
 }
 // Send an OpenID Connect sign-out request. 
 HttpContext.GetOwinContext().Authentication.SignOut(
 CookieAuthenticationDefaults.AuthenticationType);
 Response.Redirect("/");
}

Met bovenstaande code kunnen we nu iemand laten in- en uitloggen. Na het inloggen wordt er een accessToken opgeslagen, waarmee we vervolgens meer gegevens kunnen verzamelen van de betreffende persoon. Met de volgende code kunnen we deze accessToken eenvoudig opslaan en hergebruiken bij andere acties. Hiervoor maken we een nieuwe map “TokenStorage” aan in de projectfolder en in deze map een nieuw item met de naam SessionTokenCache.cs. Vervang de inhoud van dit bestand met het volgende:

using Microsoft.Identity.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;

namespace OfficeApp.TokenStorage
{
 public class SessionTokenCache
 {
 private static ReaderWriterLockSlim sessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
 string userId = string.Empty;
 string cacheId = string.Empty;
 HttpContextBase httpContext = null;

 TokenCache tokenCache = new TokenCache();

 public SessionTokenCache(string userId, HttpContextBase httpContext)
 {
 this.userId = userId;
 cacheId = userId + "_TokenCache";
 this.httpContext = httpContext;
 Load();
 }

 public TokenCache GetMsalCacheInstance()
 {
 tokenCache.SetBeforeAccess(BeforeAccessNotification);
 tokenCache.SetAfterAccess(AfterAccessNotification);
 Load();
 return tokenCache;
 }

 public bool HasData()
 {
 return (httpContext.Session[cacheId] != null && ((byte[])httpContext.Session[cacheId]).Length > 0);
 }

 public void Clear()
 {
 httpContext.Session.Remove(cacheId);
 }

 private void Load()
 {
 sessionLock.EnterReadLock();
 tokenCache.Deserialize((byte[])httpContext.Session[cacheId]);
 sessionLock.ExitReadLock();
 }

 private void Persist()
 {
 sessionLock.EnterReadLock();

 // Optimistically set HasStateChanged to false. 
 // We need to do it early to avoid losing changes made by a concurrent thread.
 tokenCache.HasStateChanged = false;

 httpContext.Session[cacheId] = tokenCache.Serialize();
 sessionLock.ExitReadLock();
 }

 // Triggered right before ADAL needs to access the cache. 
 private void BeforeAccessNotification(TokenCacheNotificationArgs args)
 {
 // Reload the cache from the persistent store in case it changed since the last access. 
 Load();
 }

 // Triggered right after ADAL accessed the cache.
 private void AfterAccessNotification(TokenCacheNotificationArgs args)
 {
 // if the access operation resulted in a cache update
 if (tokenCache.HasStateChanged)
 {
 Persist();
 }
 }
 }
}

Het gebruik van de Calendar API en de resultaten tonen

Vervolgens kunnen we starten met het aanroepen van de Calender API om afspraken vanuit onze agenda’s op te halen en te tonen in onze webpagina. Hiervoor maken we in onze HomeController een nieuwe Action aan met de naam Calendar. De inhoud van deze action ziet er als volgt uit:


public async Task<ActionResult> Calendar()
{
 string token = await GetAccessToken();
 if (string.IsNullOrEmpty(token))
 {
 // If there's no token in the session, redirect to Home
 return Redirect("/");
 }

 string userEmail = await GetUserEmail();

 GraphServiceClient client = new GraphServiceClient(
 new DelegateAuthenticationProvider(
 (requestMessage) =>
 {
 requestMessage.Headers.Authorization =
 new AuthenticationHeaderValue("Bearer", token);

 requestMessage.Headers.Add("X-AnchorMailbox", userEmail);

 return Task.FromResult(0);
 }));

 try
 {
 var calendarResults = await client.Me.Calendar.Events.Request()
 .OrderBy("createdDateTime DESC")
 .GetAsync();

 return View(calendarResults);
 }
 catch (ServiceException ex)
 {

 return RedirectToAction("Error", "Home", new { message = "ERROR retrieving messages", debug = ex.Message });
 }

}

Allereerst moeten we in deze actie een AccessToken ophalen van de ingelogde persoon. Dat doen we met het eerste stukje code. Vervolgens kunnen we ook het mailadres van de ingelogde persoon ophalen, die we kunnen meenemen in het de header van de uitvraag. Dit zorgt ervoor dat de routing naar de API’s efficiënter uitgevoerd wordt.

Vervolgens maken we een nieuwe GraphServiceClient aan waarin we aangeven dat we middels een bearer authorizationcode ons identificeren en in de header nemen we ook het mailadres van de ingelogde persoon mee.

in de try..catch gaan we vervolgens de afspraken ophalen van de persoon uit zijn agenda. Deze sorteren we op aanmaakdatum van nieuw naar oud. Vervolgens sturen we de uitkomst hiervan naar onze View met de return View(calendarResults);

Nadat we een nieuwe view aangemaakt hebben met de naam Calendar kun je hierin de code vervangen door onderstaande:


@model IEnumerable<Microsoft.Graph.Event>
@{
 ViewBag.Title = "Agenda";
}


<h2>Agenda</h2>


<table class="table table-hover">

<thead>

<tr>

<th>@Html.DisplayNameFor(model => model.Subject)</th>


<th>@Html.DisplayNameFor(model => model.Start)</th>


<th>@Html.DisplayNameFor(model => model.End)</th>


<th>@Html.DisplayNameFor(model => model.Categories)</th>

 </tr>

 </thead>


<tbody>
 @foreach (var item in Model)
 {

<tr>

<td>@Html.DisplayFor(modelItem => item.Subject)</td>


<td>@Html.DisplayFor(modelItem => item.Start.DateTime)</td>


<td>@Html.DisplayFor(modelItem => item.End.DateTime)</td>


<td>
 @foreach (var category in item.Categories)
 {
 <label class="label label-default">@category</label>
 }
 </td>

 </tr>

 }
 </tbody>

</table>



We moeten nog twee kleine aanpassingen doen om alles goed te laten draaien. Eerst passen we onze homepagina aan met een knop om in te loggen:

Home

Ook moeten we de navigatiebalk nog aanpassen met een link naar onze Action “Calendar”. Deze link mag samen met de UitlogActie alleen getoond worden wanneer de aanvraag geauthenticeerd is (dus er moet iemand ingelogd zijn).

Home (ingelogd)

Als we nu onze app starten en op de homepagina op de knop inloggen klikken komen we in een inlogscherm van Microsoft, waar we met ons Microsoftaccount of werk- / schoolaccount kunnen inloggen. De app zal vervolgens vragen of er toegang verkregen mag worden tot de gegevens van de gebruiker en of de email en agenda uitgelezen mag worden. Wanneer je dit toestaat zul je op de homepagina terugkomen en zien dat je ingelogd bent. Bovenin het menu staat nu een link naar agenda. Wanneer we hierop klikken zal er een lijst met afspraken getoond worden die als laatste aangemaakt zijn.

Calendar

Tot slot

We zien nu een lijst met de laatste 10 afspraken die gemaakt zijn door de gebruiker. In mijn volgende blog zal ik verdergaan hoe we deze afspraken kunnen opslaan in een database en deze vervolgens voor andere doeleinden gebruikt kunnen worden.

Voor meer informatie over de tutorial die ik ook deels gevolgd heb kun je deze pagina van Microsoft bezoeken.

Klik hier om naar onze site te gaan

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *