Flexibel ApiKey-gebruik bij WCF Data Services

Een APIKey is een unieke identificatie die meegestuurd wordt bij iedere
aanroep van een service. Hiermee kan op server niveau de toegang gereguleerd
worden. Een APIKey is niet zo zeer voor security bedoeld, de key wordt namelijk
onversleuteld meegestuurd in de url van de aanroep, maar het is een eenvoudige
maar krachtige bescherming voor de beschikbaarheid van de service.

Flexible gymnist

Door een API-Key te eisen van de gebruikers van jouw service kun je de
‘heavy’ users eruit pikken en zij die echt een bedreiging zijn voor de
beschikbaarheid van de service, kunnen geblokkeerd worden. Ik denk niet dat dit
ook gelijk de oplossing voor DDos aanvallen zijn, maar hiermee wordt intensief
gebruik/misbruik voorkomen.

Nu is dit op zich niet nieuw. Ik wilde het toepassen en kwam op de
volgende link uit: http://blogs.msdn.com/b/rjacobs/archive/2010/06/14/how-to-do-api-key-verification-for-rest-services-in-net-4.aspx . Voor iedereen die in WCF een APIKey moet implementeren, is dit een prima startpunt. Ik vat hier even samen hoe dit we hier eenvoudig gebruik van kunnen maken:

  • Download het voorbeeld project van Ron Jacobs
  • Rip de APIKeyAuthorization.cs class en plaats die in jouw eigen WCF Service solution in een apart project.
  • Leg een referentie naar dit project.
  • Rip de APIKeys.xml en plaats die in de App_Data map. Bij voorkeur pas je ook de guid’s aan.
  • Als je gebruik wilt maken van de mogelijkheid om de APIKey verficatie uit en aan te schakelen, implementeer dan de Global.APIKeyVerification property in de
    global.asax. Anders moet verwijzing uit de APIKeyAuthorization.cs class weggehaald worden
  • Voeg aan de WCF Behaviour de volgende verwijzing toe (let op de namespace en dll-verwijzing):
<serviceAuthorization
  serviceAuthorizationManagerType="WCFWebHttpLibrary.APIKeyAuthorization,
  WCFWebHttpLibrary" />

De volgende call wordt nu dus goedgekeurd:

http://localhost/AspNetWebSite/WcfDataService/$metadata/?APIkey=918704ec-4811-45b6-a169-16bae3df69a8

Hiermee is de service nu dus van een API-key beveiliging voorzien. Alleen
gebruikers die een APIKey uit het lijstje meegeven, kunnen gebruikmaken van de
dienst. Dus mocht een gebruiker de server laten roken, haal dan zijn API-Key uit
de xml in de App-Data map.

Bekijk hier een filmpje over de implementatie:
http://channel9.msdn.com/shows/Endpoint/endpointtv-How-to-do-API-Key-Verification-with-a-WCF-WebHttp-REST-service/

Ik heb dit zelf ook zo geïmplementeerd en in eerste instantie werkte het
prima. De (Restful) service werkt perfect icm. Javascript (Lees: jQuery). Maar
helaas liep ik later tegen een basaal probleem aan. Ik wilde de WCF Data Service
gaan consumeren via een Service Reference in een C# Client applicatie. De
aanroep ziet er dan zo uit:

Uri serviceUri = new Uri("htt p://localhost/AspNetWebSite/WcfDataService");
ServiceReference.Entities service = new ServiceReference.Entities(serviceUri);
var items = (from item in service.Clients select  item).ToList();

Ok, hoe geef ik nu mijn APIKey door, waar laat ik ‘m? Want bij iedere
aanroep moet deze APIKey doorgegeven worden. Misschien werkt het ook wel als ik ‘m in de Uri doorgeef? Maar de aanroep met:

Uri serviceUri =new
Uri("htt p://localhost/AspNetWebSite/WcfDataService?
     APIkey=918704ec-4811-45b6-a169-16bae3df69a8");

Resulteerde in:

{“Expected an absolute, well formed http URL without a query or
fragment.\r\nParameter name: serviceRoot”}

Het probleem is dus dat ik de request wil verrijken met de APIKey. Tot nu
toe hebben we geprobeerd met de querystring, is die op een andere manier uit te
breiden? Na wat verder zoeken kwam ik uit op het SendingRequest event van de
proxy uit:

service.SendingRequest += new
  EventHandler<System.Data.Services.Client.SendingRequestEventArgs>
              (service_SendingRequest);

Hierbij wordt de mogelijkheid geboden om de request te bekijken en te
manipuleren. Helaas blijkt de querystring hier readonly te zijn. Dit viel dus
tegen maar het bleek wel mogelijk om de header van request te manipuleren! Dus we kunnen wel:

static void service_SendingRequest(object
  sender,System.Data.Services.Client.SendingRequestEventArgs e)
{
  // when using api in the header...
  e.Request.Headers.Add("APIkey", "918704ec-4811-45b6-a169-16bae3df69a8");
}

Is het dan ook mogelijk om de header aan de serverkant uit te lezen?

Ja, dat is dus mogelijk. Naast de querystring kan ook de header
uitgelezen worden. Ik heb dus de volgende kleine aanpassing gedaan in de code
van Ron Jacobs:

public string GetAPIKey(OperationContext operationContext)
{
  // Get the request message
  var request = operationContext.RequestContext.RequestMessage;
  // Get the HTTP Request
  var requestProp =
    (HttpRequestMessageProperty)request.
      Properties[HttpRequestMessageProperty.Name];
  // Get the query string
  NameValueCollection queryParams =
  HttpUtility.ParseQueryString(requestProp.QueryString);
  // Return the API key (if present, null if not)
  string apiKey = queryParams[APIKEY];
  // Is the API Key available in the querystring?
  if (apiKey == null)
  {
    // Is the API Key available in the header?
    apiKey = requestProp.Headers[APIKEY];
  }
  return apiKey;
}

Mocht de APIKey niet in de querystring voorkomen, dan onderzoeken we de
header… Hiermee is het toepassen van de APIKey heel flexibel geworden.

Hoewel minder elegant is er ook een eenvoudiger mogelijkheid om een APIKey te implementeren, zonder de ServiceAuthorization implementatie. Bij een WCF
DataService kan ook de methode OnStartProcessingRequest ge-override worden:

protected override void OnStartProcessingRequest(ProcessRequestArgs args)
{
  var queryParams = HttpUtility.ParseQueryString(
                args.OperationContext.AbsoluteRequestUri.Query);
  string apiKey = queryParams[APIKEY];
  if (apiKey == null)
  {
    apiKey = args.OperationContext.RequestHeaders[APIKEY];
  }
  if (CheckValidAPIKey(apiKey)) // TODO : Implement your own check
  {
    base.OnStartProcessingRequest(args);
  }
  else
  {
    throw new System.Web.Services.Protocols.SoapException();
  }
}

.Net ontwikkelaars kunnen nu via de header de APIKey doorgeven aan de WCF
Data Service zonder dat hun framework roet in het eten gooit. En ook
ontwikkelaars die de OData service aanroepen met bv. Javascript of Objective C
(IPhone) kunnen prima uit de voeten met de “fat url” met daarin de APIKey.

Advertentie

Een landkaart op je site? Kijk eens naar VirtualEarth

Tegenwoordig heeft het toepassen van geografische informatie voor user
interfaces een grote vlucht genomen. Terwijl enkele jaren gelden nog enorm
geïnvesteerd moest worden voor een GIS systeem, is dit tegenwoordig met tooling van oa. Miscrosoft en Google voor iedereen beschikbaar gekomen.

VirtualEarth voorbeeld

Als je hierbij optelt dat met de komst van GPS in allerlei producten
(lees: mobieltjes) er ook enorm veel informatie “location-aware” is geworden,
kunnen er hele leuke toepassingen rond landkaarten gebouwd worden.

Voor mijn huidige klant heb ik ook een representatie van data gebouwd op
basis van VirtualEarth 6. Ik ben door http://www.nerddinner.com/ , het perfecte startpunt om Asp.Net Mvc2 te ontdekken, met deze oplossing in aanraking gekomen. Dit is overigens niet de nieuwste versie (ik gebruik namelijk versie 6.2) maar ik pas deze graag toe. Want dit control wordt gewoon door JavaScript (lees jQuery) aangeroepen en voorzien van data. En de representatie ziet er prima uit, zowel in Internet Explorer (..), Opera als Firefox. Maar het grootste voordeel is dat ik geen API-key via een registratie moet aanvragen en dit is dus goed overdraagbaar op langere termijn. Een eenvoudige verwijzing via script is voldoende om deze te gebruiken.

De basis is altijd hetzelfde: eerst stel je een html DIV vast als container voor de map. Let er op dat de style op position:relative wordt gezet anders geeft de positionering vreemd gedrag.

Vervolgens moet de map ingesteld en getoond worden, en er valt echt veel
in te stellen. Denk hierbij aan het menu op de kaart, de soort representatie,
zoomfactor, mogelijkheid tot herschalen etc. Ik heb in dit voorbeeld het
inzoomen uitgezet. Daardoor is ook de ‘dashboard’ niet getoond. Verander de
‘true’ in ‘false’ in de map.LoadMap() om zoomen weer te activeren. Kijk voor
meer uitleg op http://msdn.microsoft.com/en-us/library/bb412546.aspx

We hebben nu dus al een aardige landkaart. Maar om echt indruk te maken
op je baas, moet je iets tonen op de landkaart. Er zijn verschillende
mogelijkheden, van punten via eenvoudige lijnen tot complete routenavigatie. In
deze blog beperk ik mij tot ‘PushPins’, kleine iconen die een locatie aanduiden.
Hoewel in later versies van VirtualEarth de pushpin overtroffen wordt door de
Shape, voldoet de PushPin vaak al voldoende.

Voordat ik de PushPin demonstreer, wil ik nog even op layers wijzen.
Hoewel ik hier geen gebruik van een layer maak, alle PushPins kunnen al direct
op de map geplaatst worden, kan dit een welkome aanvulling zijn. Met layers
kunnen PushPins gegroepeerd worden en als groep zichtbaar of onzichtbaar gemaakt worden. Let hierbij op dat PushPins en layers die onzichtbaar zijn, zich nog wel degelijk in het geheugen bevinden. Layers en PushPins hebben ook een unieke identificatie en het aanmaken van een layer of pin met een al bestaande
identificatie resulteert in een foutmelding.

Een PushPin wordt op een bepaalde positie geplaatst door het opgeven van
een latitude en longitude. De standaard icoon van een PushPin kan vervangen
worden door een ander icoontje door de url op te geven. In mijn voorbeeld geef
ik ook een titel en details op welke getoond worden bij een “Mouse-over”. De
details kunnen uit html bestaan. Hierin kunnen ook images en links opgenomen
worden. Zie http://msdn.microsoft.com/en-us/library/aa737188(msdn.10).aspx
voor meer informatie over de constructie van een PushPin.

Helaas weet ik in vaak niet de coördinaten van de te plaatsen PushPin,
maar wel de bijbehorende adresgegevens. Uiteindelijk komt het er dus op aan hoe we aan de locatie van het adres, land of plaats komen. Hiervoor gebruik ik de
functie map.Find(). In mijn voorbeeld achterhaal ik de positie van “Nederland”.
Dit had bv. ook “broadstreet, new york” kunnen zijn. Aangezien er van beide
mogelijkheden maar één locatie op de wereld aanwezig zal zijn, beperk ik mijn
gewenste selectie tot slecht één locatie.

Uiteindelijk krijg ik in de callback functie ‘processResults’ een
‘places’ parameter mee. Van de eerste positie vraag ik de LatLong op welke bij
de PushPin laat invullen. Zie voor meer uitleg over de de Find functie: http://msdn.microsoft.com/en-us/library/bb429645.aspx

Dit geeft uiteindelijk:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Virtual Earth demo</title>
</head>
<body onload="getMap();">
<script
src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"
type="text/javascript"></script>
<script type="text/javascript">
  var map = null;
  function getMap() {
    map = new VEMap('divMap');
    map.SetDashboardSize(VEDashboardSize.Small);
    var veCenterLocation = new VELatLong(30,
      0, 0, VEAltitudeMode.RelativeToGround);
    map.LoadMap(veCenterLocation, 2, VEMapStyle.Hybrid, true, 1);
    showPins();
  }

  function showPins() {
    try {
      map.DeleteAllPushpins();
      map.Find(null, "Netherlands", null, null, 0, 1, true, true,
        true, false, processResults);
    }
    catch (e) {
      alert("showPins message: " + e.message);
    }
  }

  function processResults(layer, resultsArray, places, hasMore,
      veErrorMessage) {
    try {
      var pin = new VEPushpin(1, places[0].LatLong,
        "htt p://www.112kampen.nl/icon/television.png",
        places[0].Name,
        '<table><tr><td>Header<ul><li>Field
         1</li><li>Field
         2</li></ul></td></tr></table>', 1);
      map.AddPushpin(pin);
      if (veErrorMessage != null) {
        alert("process error: " + veErrorMessage);
      }
    }
    catch (e) {
      alert("processResults message: " + e.message);
    }
  }
</script>

    <h2 id="h2">Virtual Earth demo</h2>

    <div id="divMap" style="position:relative; width:1000px;
        height:550px;"></div>
  </body>
</html>

Natuurlijk is dit slecht een vogelvlucht langs enkele mogelijkheden maar
hiermee is al een hele leuke interactieve interface te bouwen. Zie voor meer
demo’s van VirtualEarth ook http://www.microsoft.com/maps/isdk/ajax/