Als ontwikkelaars worden we geconfronteerd met een uitdaging: het verantwoorden van onze werkzaamheden. In de afgelopen jaren hebben we een verschuiving doorgemaakt van het plannen van uren naar het plannen van deliverables. Hoewel we meestal deliverables leveren voor onze projecten, worden in sommige gevallen onze uren verkocht. Het is met name belangrijk om inzicht te hebben in de uren die we hebben besteed aan deze specifieke projecten. We hebben allemaal een hekel aan het achteraf verwerken van onze gewerkte uren in het administratiepakket (AFAS) aan het einde van een werkweek. Hierdoor gebeurt het wel eens dat we een herinnering krijgen om de uren van meerdere weken bij te werken in het systeem. Om zowel onszelf als onze klanten tevreden te stellen, hebben we een oplossing bedacht: het ontwikkelen van een webapplicatie die onze agenda uitleest en de afspraken verwerkt in een database. Vervolgens kunnen deze afspraken worden geïmporteerd als urenregels in het administratiepakket, waardoor het handmatig invoeren van uren overbodig wordt. Ik ben begonnen met het maken van een webapplicatie die de agenda kan uitlezen met behulp van de Outlook REST API van Microsoft. In deze blog zal ik alle stappen beschrijven die ik heb genomen om tot deze webapplicatie te komen. Het resultaat is een beveiligde webpagina waarop de laatste 10 gemaakte afspraken worden weergegeven. Voor deze ontwikkeling maak ik gebruik van Microsoft Visual Studio 2017, dat gratis te downloaden is. Ik ga ervan uit dat je Visual Studio al hebt geïnstalleerd en er al een aantal keer mee hebt gewerkt, terwijl ik deze blog schrijf.
Daarom, laten we beginnen met het maken van een nieuwe ASP.NET Web-applicatie zonder authenticatie.
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 in het nieuwe Application Registration Portal of in het Azure Management Portal. Voor onze app moeten we echter de volgende stappen uitvoeren:
- Log in met je eigen gegevens in het Application Registration Portal.
- 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’.
- 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.
- Onder de kop Platforms, wanneer je op de knop “Platform toevoegen” klikt (Add Platform), kies hier voor Web en voer de URL in van je app die je zojuist aangemaakt hebt. Je kunt deze gemakkelijk vinden door het project te selecteren. Rechts onderin het eigenschappenveld staat de URL. Als je dit niet ziet, klik dan met de rechtermuisknop op het project en selecteer “eigenschappen”. Onder het kopje “web” staat vervolgens de project-URL. In ons project is dit http://localhost:51870.
- Onder de kop Machtigingen voor Microsoft Graph (Microsoft Graph Permissions moeten de volgende machtigingen toegestaan worden: Calendars.Read, Mail.Read en User.Read.
- 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. Daarom zijn de volgende stappen essentieel om OAuth2 netjes te implementeren in ons project.
Open de Web.Config file en voeg onderstaande sleutels toe aan de sectie <appSettings>:
[code lang=”xml”]
<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”>
[/dm_code_snippet]
Hierbij dien je de waarde van AppID en AppPassword aan te passen met de gegevens die je zojuist hebt gekopieerd vanuit de registratie van je eigen app. Vervolgens stel je de waarde van de RedirectUri in op de URL van je project die je eerder hebt ingevoerd bij de redirectUrl in je App.
Daarnaast dienen de volgende pakketjes geïnstalleerd te worden via de NuGet Package Manager:
[code lang=”powershell”]
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
[/dm_code_snippet]
Na de installatie gaan we terug naar het project en maken we een map “App_Start” aan. Vervolgens voegen we hier een nieuw OWIN Startup Class item aan met de naam Startup.cs.
Daarna kunnen we de inhoud van dit bestand overschrijven met de volgende code:
[code lang=”csharp”]
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<OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> 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);
}
}
}
}
[/dm_code_snippet]
Bovenstaande code bevat de gegevens voor het inloggen bij je Microsoft- of je werk-/schoolaccount. Daarnaast maken we in onze HomeController twee Actions aan: SignIn en SignOut. Deze acties zorgen ervoor dat iemand kan in- en uitloggen op de webapplicatie.
[code lang=”csharp”]
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(“/”);
}
[/dm_code_snippet]
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 behulp van de volgende code kunnen we deze accessToken eenvoudig opslaan en hergebruiken bij andere acties. Daarom maken we een nieuwe map genaamd “TokenStorage” aan in de projectfolder, en binnen deze map creëren we een nieuw item met de naam SessionTokenCache.cs. We vervangen de inhoud van dit bestand met het volgende:
[code lang=”csharp”]
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();
}
}
}
}
[/dm_code_snippet]
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:
[code language=”csharp”]
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 });
}
}
[/dm_code_snippet]
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:
[code language=”csharp”]
@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>
[/dm_code_snippet]
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:
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).
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.
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.
Meer weten over onze oplossingen?
Onze consultants hebben veel ervaring binnen een grote verscheidenheid aan branches.
Eens verder brainstormen over de mogelijkheden voor jouw organisatie?
Maak kennis met onze specialist Arnoud van der Heiden.