Cookie authentication in Blazor

  1. Inleiding
  2. Terminologie
    1. Authenticatie
    2. Autorisatie
    3. Security Context
    4. Principal
    5. Identity
    6. Claim
  3. Web Applicatie
  4. Server
    1. Modeldefinitie
    2. Identity Provider
    3. Host
      1. ConfigureServices
      2. Configure
    4. SportschoolController
      1. aanmelden
      2. afmelden
      3. ongeautoriseerd
    5. ProfielController
  5. Client
    1. Components.Authorization
    2. AuthenticationStateProvider
    3. App.Razor
    4. AuthorizeView
    5. CascadingParameter
    6. Profiel.Razor
    7. Groepslessen.Razor
  6. Slot

Inleiding

Je website gaat ‘live’. Iedereen met een internetverbinding kan nu kennis nemen van je website. Maar wil je dat wel? Stel dat bepaalde delen van je website een content hebben dat niet iedereen mag zien? Cookie Authentication kan dan een oplossing zijn.

We zullen het één en ander toelichten aan de hand van een fictieve sportschool die een website heeft. Verschillende pagina’s op de website hebben een content dat alleen maar zichtbaar is voor leden die ingelogd zijn. Binnen een enkele pagina geldt zelfs een extra (autorisatie) restrictie. Leden moeten ingelogd zijn én ze moeten een geldig abonnement hebben alvorens ze bepaalde content te zien krijgen. De website logt mensen automatisch uit als gedurende 30 seconden of langer op de website geen activiteit meer wordt geregistreerd.

De voorbeeldcode vind je in deze Github repository. De download/clone kan je in detail bekijken en je kunt met de download/clone dingen zelf uitproberen.

up | down

Terminologie

Authenticatie, Autorisatie, Principals, Identities en Claims? Het zijn termen die we tegen gaan komen en te maken hebben met het regelen van de toegang tot onze web applicatie. We laten deze termen de revue passeren alvorens verder te gaan.

up | down

Authenticatie

Authenticatie is het hele proces van bewijzen wie je bent. Je zegt dat je die en die bent, maar ben je inderdaad ook die persoon? De controlerende instantie zal je echt niet op je mooie ogen geloven. Je moet dan ook iets te overleggen waarmee je je identiteit bevestigt.

Maar wat kan je als bewijs overleggen? Dat is afhankelijk van de situatie en de van toepassing zijnde identiteit. Zo kan je meerdere identiteiten hebben zoals bijvoorbeeld die van toerist. In de hoedanigheid van toerist zal een geldig paspoort voor de douane meestal volstaan als zijnde een geldig bewijsstuk waarmee je je identiteit kunt bewijzen.

Je kunt echter ook de identiteit hebben van een bestuurder van een motorvoertuig. Een geldig paspoort zal dan niet volstaan bij een controle door de verkeerspolitie. Je zal in zo’n situatie een rijbewijs moeten zien te overleggen dat je identiteit bevestigt als zijnde een bestuurder die gemachtigd is tot het besturen van die motorvoertuig.

Het authenticatieproces bij een web applicatie zal meestal bestaan uit de invoer van een userid en een wachtwoord. De (web) applicatie zal je identiteit als gebruiker van de (web) applicatie controleren aan de hand van wat bekend is over jou bij haar identity provider (meestal een database waar gebruikersgegevens in zijn opgeslagen). De (web) applicatie zal je toelaten tot haar (web) applicatie zodra ze de door jou aangeleverde userid en wachtwoord heeft geverifieerd.

up | down

Autorisatie

Autorisatie is het hele proces van bepalen wat wel of niet mag. Je hebt jezelf succesvol weten te identificeren als een bezoeker die gemachtigd is tot het mogen bezoeken van bijvoorbeeld vliegveld Schiphol. Je bezoek kan echter aan restricties gebonden zijn omdat niet alle delen van Schiphol voor jou toegankelijk zijn.

Idem dito voor een (web) applicatie. Je hebt je weliswaar succesvol geïdentificeerd als gebruiker die gemachtigd is tot het gebruik van de (web) applicatie, maar dat hoeft niet automatisch te betekenen dat alle delen van de applicatie voor jou toegankelijk zijn.

up | down

Security Context

Als bezoeker krijg je bij veel bedrijven en overheidsinstanties een badge nadat je jezelf hebt geïdentificeerd bij de toegangsbalie. De badge moet je weer inleveren zodra je het terrein of het gebouw verlaat en vaak wordt van je verlangd dat je de badge zichtbaar draagt tijdens je bezoek. Een security context kan beschouwd worden als een badge.

Zo krijg je via je badge toegang tot (bepaalde delen van) het gebouw of terrein en een security context zal net zoals een badge toegangsinformatie bevatten waarmee je toegang krijgt tot (bepaalde delen van) een (web) applicatie. Een badge heeft meestal de gedaante van een creditcard en een security context zal de gedaante hebben van een cookie of (JWT) token.

De periode van het bezoek hoeft zich niet te beperken tot een dag. Het bezoek kan meerdere dagen duren omdat je bijvoorbeeld bij dat bedrijf een meerdaagse cursus volgt. Inleveren van de badge hoeft dan pas na die periode. De badge kan dan een “houdbaarheidsdatum” hebben dat de badge na een bepaalde datum niet meer gebruikt kan worden. Bij een security context kan hetzelfde van toepassing zijn dat na een bepaalde datum de cookie of (JWT) token niet meer geldig is.

up | down

Principal

Een (web) applicatie heeft weer haar eigen jargon. Een gebruiker wordt aangeduid als zijnde een Principal en een security context (de badge) zal de vorm hebben van een ClaimsPrincipal-object waarbij het object betrekking heeft op een bepaalde gebruiker (principal).

up | down

Identity

Een persoon kan meerdere identiteiten hebben. Een persoon kan bijvoorbeeld de identiteit hebben van een toerist of van een bestuurder van een motorvoertuig. Hetzelfde verhaal voor een gebruiker (principal). Een principal heeft ook identities.

up | down

Claim

Bij een identity horen één of meerdere claims. Zo kunnen we de rijbewijscategorie als een claim zien dat bij een motorvoertuig-bestuurder-identiteit hoort. Een persoon kan bijvoorbeeld een rijbewijscategorie-A-claim hebben voor het mogen besturen van “zware motoren”, maar het hoeft niet bij die ene claim te blijven. Iemand kan naast een rijbewijscategorie-A-claim ook een rijbewijscategorie-B-claim bezitten indien de persoon niet alleen “zware motoren” mag besturen, maar ook auto’s.

up | down

Web Applicatie

In deze post zullen we het één en ander toelichten aan de hand van een Blazor Web Assembly hosted solution. In Visual Studio 2019 gebruiken we wederom de Blazor Web Assembly Hosted template en het voorbeeld betreft de website van een fictieve sportschool.

De website bestaat uit webpagina’s met content die voor iedereen te zien is en content die pas zichtbaar wordt na aanmelding op de website. Verder word je automatisch uitgelogd als gedurende 30 seconden of langer op de website geen activiteit meer wordt geregistreerd.

De gegevens van de ingeschrevene worden op het startscherm getoond nadat succesvol is ingelogd. De Gym is voor alle leden toegankelijk, maar een extra abonnement is nodig voor het mogen volgen van de groepslessen.

Het wel of niet hebben van een (geldig) abonnement komt tot uiting op de groeplessen webpagina waarbij het abonnement beschouwd kan worden als een claim dat je recht geeft op het mogen volgen van groepslessen.

We hebben voor dit alles gebruik gemaakt van Cookie authentication en we zullen het één en ander in detail bekijken.

up | down

Server

Het “optuigen” van de server project en de shared project is het eerste wat we gaan doen om te komen tot de hierboven bescheven website.

up | down

Modeldefinitie

We beginnen met de definitie van een klasse Lid voor de personen die sporten bij onze sportschool. De modeldefinitie maken we aan in de shared project daar de definitie door zowel de client als de server project gebruikt gaat worden.

namespace Cookie.Shared.Models
{
 public partial class Lid
 {
   public int ID { get; set; }
   public string Achternaam { get; set; }
   public string Voornaam { get; set; }
   
   [Required(ErrorMessage = "Geef je e-mailadres op.")]
   public string Email { get; set; }

   [Required(ErrorMessage = "Geef je wachtwoord op.")]
   public string Password { get; set; }
   
   public string Abonnement { get; set; }
   public bool Expired { get; set; }
 }
}

up | down

Identity provider

Je kunt je aanmelden op de website en je moet voor het aanmelden een e-mailadres en een wachtwoord opgeven. Het e-mailadres en het wachtwoord worden door de server gebruikt voor de authenticatie van de gebruiker en een “identity provider” is nodig om het één en ander geverifieerd te krijgen.

Een identity provider is meestal een “iets” waar de gebruikersgegevens zijn opgeslagen. De (web) applicatie zal je identiteit als gebruiker van de (web) applicatie controleren aan de hand van wat bekend is over jou bij haar identity provider. We houden het in dit voorbeeld simpel en de “identity provider” is in dit geval een List<T> in het intern geheugen.

We slaan van de wachtwoorden alleen de “hash” op. Wat het wachtwoord is van de gebruiker? Dat gaat ons niks aan. Het enige wat voor ons van belang is, is dat het door de gebruiker ingegeven wachtwoord moet leiden tot een hash dat gelijk moet zijn aan de hash zoals die bij ons bekend is.

De gebruiker mag zelf zijn / haar wachtwoord resetten indien de gebruiker zijn / haar wachtwoord is vergeten. Meestal wordt daarvoor een e-mail gestuurd naar het mailadres van de gebruiker met een tijdelijk wachtwoord. De gebruiker mag dan zelf een nieuw wachtwoord ingeven waarbij wij de hash krijgen van het nieuwe wachtwoord. De nieuwe hash gaan we weer registreren bij onze identity provider.

Het resetten van wachtwoorden en het versturen van daaraan gerelateerde e-mails valt buiten de scope van deze post en we gaan dan ook niet in op dit onderwerp.

De List<T> ziet er als volgt uit waarbij de List uit drie leden bestaat en één lid (Peter Veermans) een verlopen abonnement heeft:

public class MemoryModel : IModel
{
   private readonly List<Lid> _Leden = new List<Lid>
   {
     // ƒMaakHash(Geheim1) = ede000b74d6dc6a69bfe76f05c8bc72c
     // ƒMaakHash(Geheim2) = 77b71b03bfb703ddd7425ca46ce03e56
     // ƒMaakHash(Geheim3) = 5098d86f323ccfcef2b17dfffa505ba7
     new Lid()
     {
       ID =1,
       Achternaam="Jansen",
       Voornaam="Sandra",
       Email="sjansen@mrasoft.nl",
       Password="ede000b74d6dc6a69bfe76f05c8bc72c",
       Abonnement="Aerobics",
       Expired= false
      },
      new Lid() 
      {
       ID =2,
       Achternaam="Veermans",
       Voornaam="Peter",
       Email="pveermans@mrasoft.nl",
       Password="77b71b03bfb703ddd7425ca46ce03e56",
       Abonnement="Spinning",
       Expired= true
      },
      new Lid() 
     {
      ID =3,
      Achternaam="Mulder",
      Voornaam="Olga",
      Email="omulder@mrasoft.nl",
      Password="5098d86f323ccfcef2b17dfffa505ba7",
      Abonnement="Yoga",
      Expired= false},
     };

We gebruiken voor de identity provider een IModel-interface en de IModel-interface kent in dit voorbeeld een MemoryModel-implementatie waarbij een List<T> in het intern geheugen de datasource is.

We kunnen daarnaast ook een DBModel-implementatie bouwen waarbij een tabel in een SQL Server database de datasource is. We gaan in deze posts niet in op de DBModel-implementatie maar je kunt er meer over lezen in deze post.

up | down

Host

De host wordt geactiveerd bij het opstarten van een applicatie en het zet de dingen klaar die nodig zijn voor het doen draaien van de applicatie.

up | down

ConfigureServices

Bij de server project geven we in bestand Startup.cs bij methode ConfigureServices aan dat we gebruik maken van Cookie authentication als zijnde de te gebruiken Authentication Scheme.

Bij de .LoginPath-option geef je op dat controller action method ongeautoriseerd aangeroepen moet worden indien het aanmelden niet lukt.

public void ConfigureServices(IServiceCollection services)
{
   ...
   // Which Authentication Scheme for 
   // maintaining the state of the user 
   // Cookie Authentication is chosen
   services.AddAuthentication(options =>
   {
     options.DefaultScheme = 
     CookieAuthenticationDefaults.AuthenticationScheme;
   }
   ).AddCookie(options => 
    { options.LoginPath = 
      "/api/Sportschool/ongeautoriseerd";
    });

    // Registreer de MemoryModel-implementatie 
    // van interface IModel
    // Voor de Memory model wil je niet dat de List 
    // elke keer weer wordt geïnitialiseerd 
    // naar de beginsituatie 
    // bij elke HTTP-request, 
    // dus daarom een SingleTon:
    services.AddSingleton<IModel, MemoryModel>();
   ...
}

up | down

Configure

Verder moet je bij de server project in bestand Startup.cs bij methode Configure opgeven dat Authentication en Authorization Middeware wordt gebruikt:

public void Configure(
IApplicationBuilder app,  IWebHostEnvironment env)
{
  ...
  // Authentication en 
  // Authorization MiddleWare
  app.UseAuthentication();
  app.UseAuthorization();
  ...
}

up | down

SportschoolController

De SportschoolController is de eerste web API server controller die we binnen de server project definiëren. De web API server controller bevat de controller action methods aanmelden, afmelden en ongeautoriseerd. Controller action methods die direct worden “getriggerd” door acties van de gebruiker.

up | down

aanmelden

We gaan uitgebreid op deze controller action method in omdat belangrijke dingen worden ingesteld bij deze methode. Dingen die van belang zijn voor de verdere werking van de web applicatie.

Controller action method aanmelden wordt aangeroepen vanuit het aanmeldscherm (MeldAan.razor). De controller action method ontvangt dan een object dat bestaat uit een e-mailadres en een wachtwoord.

[HttpPost("aanmelden")]
public async Task<ActionResult<Lid>> Aanmelden(Lid lid)
{
 try
 {
  // MaakHash password
  lid.Password = Utility.MaakHash(lid.Password);

  Lid aangemeldLid = 
  await _model.HaalopLid(lid.Email, lid.Password);

  // Evalueren dat correct is ingelogd
  if(aangemeldLid != null)
  {
   //------------------------------
   // using System.Security.Claims;
   //------------------------------
   //create claims
   var claimID = 
   new Claim
   (ClaimTypes.NameIdentifier, 
   Convert.ToString(aangemeldLid.ID));

   var claimNaam = 
   new Claim(ClaimTypes.Name, 
   aangemeldLid.Voornaam + " " + 
   aangemeldLid.Achternaam);

   //create claimsIdentity
   var claimsIdentity = 
   new ClaimsIdentity(
   new[] { claimID,  claimNaam }, "lidSportschool");

   //create claimsPrincipal
   var claimsPrincipal = 
   new ClaimsPrincipal(claimsIdentity);

   // AuthenticationProperties
   AuthenticationProperties authenticationProperties = 
   new AuthenticationProperties()
   {
      IsPersistent = false,
      ExpiresUtc = DateTime.Now.AddSeconds(30),
      RedirectUri = "/"
   };

    //------------------------------------------
    //using Microsoft.AspNetCore.Authentication;
    //------------------------------------------
    //Sign In User
    await HttpContext.SignInAsync(
    claimsPrincipal, authenticationProperties);
  }

  // Retourneer
  // Bij een succevolle aanmelding is:
  // a) het object gevuld
  // b) een ClaimsPrincipal aangemaakt met een claim
  // c) een sign in gedaan met
  //    await HttpContext.SignInAsync(claimsPrincipal);
  // Een leeg object wordt geretourneerd 
  // indien het aanmelden niet is gelukt
  return await Task.FromResult(aangemeldLid);
 }
 catch
 {
  // Leeg object retourneren als om één of andere reden
  // bij het ophalen van de gegevens toch wat fout gaat
  return await Task.FromResult(new Lid());
 }
}

Hash
We hebben geen interesse in het wachtwoord. We zijn alleen maar geïnteresseerd in de hash van het wachtwoord en we bepalen in regel 7 de hash van het wachtwoord dat we hebben ontvangen.

Authenticatie
In regel 10 verifiëren we de ontvangen gegevens. Het e-mailadres en de hash van het wachtwoord moet een ‘match’ geven met wat aanwezig is bij de identity provider. Als er geen ‘match’ is dan duidt dat op een onjuist ingegeven e-mailadres en/of een onjuist ingegeven wachtwoord. Een leeg object wordt dan geretourneerd en Razor-component MeldAan.razor kan daaruit concluderen dat het aanmelden is mislukt.

Security context
De security context kan beschouwd worden als een soort badge met toegangsinformatie. Bij (web) applicaties komt de security context tot uiting in een ClaimsPrincipal-object. Het object bestaat dan uit een identity met een aantal claims. Zo hebben we in dit voorbeeld een claimID welke we uiteindelijk zullen terugvinden in de ClaimsPrincipal-object.

Claim
Een aantal controller action methods in de web applicatie eisen een aangemelde gebruiker en de controller action methods zullen het pas doen zodra aan deze voorwaarde is voldaan. De aanmeldgegevens worden in regel 10 geverifieerd en een claimID wordt gegenereerd bij het correct zijn van de aanmeldgegevens.

De claimID kan dan gebruikt worden als bewijsstuk voor het aangemeld zijn van de gebruiker. De controller action methods accepteren de claimID als een geldig bewijsstuk waardoor een gebruiker zich maar één keer hoeft te identificeren (middels zijn e-mailadres en wachtwoord).

Identiteit
De claims horen bij een bepaalde identiteit en de gebruiker heeft in dit geval de identiteit van een sportief mens die ingeschreven staat bij een sportschool. Een identiteit kan meerdere claims bevatten en alle claims worden in dit voorbeeld in een array gezet (array claimsIdentity).

We zien bij regel 32 dat twee claims in de array zijn gezet (de claimID en een claimNaam). claimNaam wordt in deze applicatie alleen maar gebruikt voor het doen aangeven als wie je bent aangemeld. claimNaam geeft verder geen bijzondere rechten voor het doen opstarten van iets.

ClaimsPrincipal
De claimsIdentity en alle bijbehorende claims worden uiteindelijk bij elkaar geharkt en in regel 36 in een ClaimsPrincipal-object gezet. Methode HttpContext.SignInAsync() wordt vervolgens aangeroepen in regel 51 en 52 waarbij de ClaimsPrincipal-object mee wordt gegeven. De methode zorgt ervoor dat een cookie wordt gecreëerd welke aan de client wordt gegeven.

Een cookie en claims zijn aanwezig na het aanmelden waaronder een claimID. De cookie en de claimID kunnen vanaf dat moment door de client gebruikt worden voor het doen opvragen van gegevens door controller action methods. We zien in de Chrome Browser bij de developer tools (<F12> – Application) de cookie welke de client heeft ontvangen:


Stateless – Cookie
De server zal bij het inloggen bij Cookie authentication een cookie genereren zodra geverifieerd is dat de inloggegevens correct zijn. De client krijgt de cookie en de client moet de ontvangen cookie weer meesturen bij elk verzoek naar de server. De cookie en de inhoud van de cookie zijn met name van belang voor de controller action methods die eisen dat de gebruiker is aangemeld. De cookie en de inhoud daarvan zijn voor die controllers een bewijsstuk dat de gebruiker is aangemeld en ze zullen op grond daarvan hun medewerking verlenen.

We hadden het in deze post gehad over REST en dat de relatie tussen de client en de server bij REST Stateless is. Cookie authentication valt binnen de Stateless REST architectuurstijl want de client krijgt weliswaar een cookie van de server, maar de server gaat zich niet bezighouden met de opslag van de cookie. De client mag de cookie zelf opslaan in de cache van zijn browser en de cookie moet weer elke keer meegestuurd worden bij elk verzoek naar de server.

Cookie – ExpiresUTC
In de regels 39 t/m 45 stellen we het één en ander in m.b.t. de cookie voor de client. In dit voorbeeld geven we bij eigenschap ExpiresUTC aan dat de cookie niet meer geldig is als gedurende 30 seconden of langer geen activiteit meer wordt geregistreerd op de website. De “kieskeurige” controller action methods (zij die eisen dat de gebruiker aangemeld is) zullen verzoeken die van de client komen niet meer uitvoeren indien de cookie is verlopen of niet meer aanwezig is.

Cookie – IsPersistent
De cookie wordt bij het afsluiten van de web applicatie en/of het afsluiten van de browser automatisch verwijderd uit de cache van de browser. Je kunt bij eigenschap IsPersistent opgeven of je dat wel of niet wil. In dit voorbeeld willen we dat de cookie automatisch wordt verwijderd want we hebben voor de eigenschap de waarde false opgegeven. De waarde true is met name handig als je bij het opnieuw opstarten van de web applicatie en/of de browser weer automatisch ingelogd wil zijn (maar dan moet de cookie ook nog geldig zijn).

up | down

afmelden

Controller action method afmelden wordt getriggerd vanuit een afmeld-button in MainLayout.razor.

In controller action method aanmelden zagen we bij de regels 5152 dat een methode .SignInAsync() werd aangeroepen.

[HttpGet("afmelden")]
 public async Task<ActionResult<bool>> Afmelden()
 {
    try
    {
      ...
      await HttpContext.SignOutAsync();
      return Ok(true);
    }
    catch (Exception e)
    {
      ...
    }
}

In controller action method afmelden wordt bij regel 7 een methode .SignOutAsync() aangeroepen wat tot gevolg heeft dat de cookie ook zal verdwijnen:

De client wordt door de controllers op de server vanaf dat moment beschouwd als zijnde niet meer aangemeld.

up | down

ongeautoriseerd

We zagen een verwijzing naar controller action method ongeautoriseerd bij de host van de server project. Controller action method ongeautoriseerd wordt getriggered indien het aanmelden niet lukt. Bijvoorbeeld omdat bij het inloggen een verkeerd wachtwoord is ingegeven.

        [HttpGet]
        [Route("ongeautoriseerd")]
        public ActionResult Ongeautoriseerd()
        {
            return Unauthorized();
        }

De controller action method retourneert via Helper Methode Unauthorized() een HTTP Unauthorized Status Code.

In onze voorbeeldapplicatie wordt bij elke (client) Razor-component controller action method gegevenslid aangeroepen. De controller action method zal een Unauthorized HTTP Status Code geven indien de gebruiker niet of niet meer is aangemeld.

[HttpGet]
[Route("gegevenslid/{ID}")]
public async Task<ActionResult<Lid>> gegevenslid(int ID)
{
   Lid lid = await _model.HaalopLid(ID);
   return Ok(lid);
}

De afhandeling van die HTTP Status Code leggen we bij de Razor-componenten in de client project. Bij een HTTP Unauthorized Status Code wordt het startscherm opgestart met een parameter true. De true-parameter zorgt ervoor dat bij het opstarten van dat startscherm de browser wordt gerefreshed zodat de webpagina’s weer content laten zien die iedereen mag zien en de controller action methods weten dat de client een anonieme, niet ingelogde gebruiker heeft.

@code {
    private int ID;
    ...

    protected override async Task OnInitializedAsync()
    {
      ...

       // Er is een claimID in authenticationState.
       var claimID = 
       authenticatieStatus.User
       .FindFirst
       (c => c.Type == ClaimTypes.NameIdentifier);

       ID = Convert.ToInt32(claimID?.Value);

       // We gebruiken de claimID voor het doen 
       // ophalen van de gegevens. De claimID 
       // mogen we gebruiken omdat we die hebben 
       // gekregen na een succesvolle aanmelding
       var opgehaald = 
       await httpClient.GetAsync
       ("/api/Profiel/gegevenslid" + "/" + ID);

      // De Http Status Code zal Unauthorized zijn als 
      //de cookie is verlopen of niet aanwezig is
      if (opgehaald.StatusCode == 
          HttpStatusCode.Unauthorized)
       {
          navigationManager.NavigateTo("/", true);
       }
      ...
    }

up | down

ProfielController

ProfielController is de tweede controller die we definiëren binnen de server project. De controller action methods in de ProfielController zijn bedoeld voor de verschillende (client) Razor-componenten zodat zij van een ingelogde gebruiker de gegevens kunnen opvragen.

Een claimID wordt binnen de applicatie gebruikt voor het doen opvragen van de gegevens. Een claimID is aanwezig zodra een gebruiker succesvol is ingelogd. We hebben verder een [Authorize]-attribuut toegevoegd aan de ProfielController zodat diens controller action methods alleen maar uitgevoerd mogen worden voor clients met een ingelogde gebruiker.

Sommige controller action methods kunnen aangeroepen worden door middel van de url van de controller action method die in de adresbalk van de browser “geplakt” wordt:

Met de [Authorize]-attribuut wordt dat scenario enigszins afgedekt want een [Authorize]-attribuut zal ervoor zorgen dat alleen maar zinnige output wordt getoond bij een ingelogde gebruiker waarbij achterdeurtjes zoals het plakken van urls in de browser ook mee worden genomen bij de “afdichting”:

We kunnen gebruik maken van een [Authorize]-attribuut omdat we de Authorization Middleware hebben geactiveerd bij de host van de server project.

De controller action method kan uiteraard verder “afgedicht” worden met o.a. autorisatie “Roles” want er zijn met alleen maar een [Authorize]-attribuut verder geen restricties meer zodra je bent ingelogd. Je kunt dan voor iedere ID gegevens opvragen en het is de vraag of dat wel gewenst is. Uitgebreide autorisatie is een onderwerp dat buiten de scope van deze post valt en in deze post gaan we daar dan ook niet op in.

up | down

Client

De server project hebben we inmiddels “opgetuigd” en we gaan ons nu bezighouden met de client project.

up | down

Components.Authorization

We hebben de Microsoft.AspNetCore.Components.Authorization (nuGet) package nodig indien we authenticatie en autorisatie willen doorvoeren in ons client project en we beginnen met het installeren van de package:

up | down

AuthenticationStateProvider

De gebruiker is ingelogd en we willen bijvoorbeeld weten of het abonnement van de gebruiker geldig is. De info krijgen we met een Authentication State Provider en we moeten daarvoor methode GetAuthenticationStateAsync() van klasse AuthenticationStateProvider implementeren. Voor de implementatie creëren we klasse AuthenticatieStatus welke erft van base class AuthenticationStateProvider:

public class 
AuthenticatieStatus :  AuthenticationStateProvider
{
  ...

  public async override 
  Task<AuthenticationState> GetAuthenticationStateAsync()
  {
    ...
  }

}

We geven de onderstaande implementatie aan methode GetAuthenticationStateAsync. De methode retourneert uiteindelijk een AuthenticationState met info over de ingelogde gebruiker. De AuthenticationState bestaat in dit voorbeeld uit drie claims waaronder de claimExpired (regel 3033) welke staat voor het wel of niet geldig zijn van een abonnement.

We zullen bij de groepslessen webpagina zien dat Groepslessen.razor gaat ‘vissen’ naar die claimExpired en dat is geen probleem want de claim is aanwezig in de AuthenticationState van de ingelogde gebruiker. De Razor-component kan op grond van de claim besluiten dat niks getoond gaat worden op de webpagina omdat het abonnement van ingelogde gebruiker ongeldig is.

public async override 
Task<AuthenticationState> GetAuthenticationStateAsync()
{
  try
  {
     // Het object kan gevuld worden 
     // als succesvol is ingelogd
     Lid huidiglid = 
     await _httpClient.GetFromJsonAsync<Lid>
     ("/api/Profiel/huidiglid");

     if (huidiglid != null && huidiglid.ID != 0)
     {
        //creëer claims uit het geretourneerde object

        // claimID
        var claimID = 
        new Claim
        (ClaimTypes.NameIdentifier, 
         Convert.ToString(huidiglid.ID));

        // claimNaam
        var claimNaam = 
        new Claim
        (ClaimTypes.Name, 
         huidiglid.Voornaam + " " + 
         huidiglid.Achternaam);

        // claimExpired
        var claimExpired = 
        new Claim
        (ClaimTypes.Expired, 
         Convert.ToString(huidiglid.Expired));

        //claimsIdentity
        var claimsIdentity = 
        new ClaimsIdentity
        (new[] { claimID, claimNaam, claimExpired }, 
        "lidSportschool");

        //claimsPrincipal
        var claimsPrincipal = 
        new  ClaimsPrincipal(claimsIdentity);

        // retourneer de claimsPrincipal
        return new AuthenticationState(claimsPrincipal);
     }
     else
        // het lid kan niet gevonden worden
        // retourneer een lege ClaimsPrincipal
        return new AuthenticationState
        (new ClaimsPrincipal(new ClaimsIdentity()));
     }
  catch
  {
        // er is wat fout gegaan, 
        // retourneer een lege ClaimsPrincipal
        return new AuthenticationState
        (new ClaimsPrincipal(new ClaimsIdentity()));
   }

De Authentication State Provider haalt gegevens op (regel 810) en dat gaat zomaar? In onze applicatie gaat niks vanzelf en het ophalen van de gegevens gaat goed omdat er sprake is van een ingelogde gebruiker. Een claimID is daardoor aanwezig en controller action method huidiglid gebruikt die claimID voor het doen ophalen van de gegevens van die ingelogde gebruiker.

Is de gebruiker niet ingelogd? Controller action method huidiglid zal in dat geval een leeg object retourneren en de AuthenticationState zal daardoor ook leeg zijn.

up | down

App.razor

We hebben een geïmplementeerde Authentication State Provider die over een ingelogde gebruiker van alles weet. De informatie moet wel bij de (client) Razor-componenten komen en dat bewerkstelligen we door het laten doen opnemen van een <CascadingAuthenticationState> en een <AuthorizeRouteView> in bestand App.razor.

<CascadingAuthenticationState>
    <Router AppAssembly ...>
        <Found ...>
            <AuthorizeRouteView ... />
        </Found>
        <NotFound>
            ...
        </NotFound>
    </Router>
</CascadingAuthenticationState>

up | down

AuthorizeView

We zijn bij de (client) Razor-componenten aangekomen. Onze applicatie heeft een menubalk en de menubalk is (in ons geval) een MainLayout.razor (Client) Razor-component. We zien dit als we niet ingelogd zijn:

En we zien dit als we als we wel ingelogd zijn:

We krijgen bovenstaand voor elkaar door gebruik te maken van de <AuthorizeView>-component. De <AuthorizeView>-component maakt ook gebruik van de info van de Authentication State Provider en het kan daardoor zien of iemand ingelogd is of niet.

<div class="main">

  <AuthorizeView>

     <Authorized>
        Je bent ingelogd als 
        @context.User.Identity.Name.
        <button class="btn btn-danger" 
           @onclick="Afmelden">Afmelden
        </button>
     </Authorized>

     <NotAuthorized>
        Je bent niet aangemeld.
        <button class="btn btn-primary" 
           @onclick="Aanmelden">Aanmelden
        </button>
     </NotAuthorized>

  </AuthorizeView>
 
  <div>
    @Body
  </div>

</div>

Is iemand ingelogd? Dan wordt op de webpagina datgene getoond wat tussen <Authorized>-tags staat. Is iemand niet ingelogd? Dan wordt op de webpagina datgene getoond wat tussen <NotAuthorized>-tags staat.

We kunnen ook gebruik maken van de context-object. Zo hadden we deze claim in de AuthenticationState gezet:

new Claim 
(ClaimTypes.Name, 
huidiglid.Voornaam + " " +
huidiglid.Achternaam);

en we krijgen de inhoud van de claim via de context-object als volgt op het scherm:

@context.User.Identity.Name

waarmee wordt getoond onder welke naam je bent ingelogd.

up | down

Cascading Parameter

We hebben laten zien hoe we in de (client) Razor-component gebruik kunnen maken van de <AuthorizeView>-component en de context-object. De info van de Authentication State Provider kunnen we ook via code ophalen. We voegen daarvoor een [CascadingParameter]-attribuut toe aan de desbetreffende (Client) Razor-component zodat de informatie over de ingelogde gebruiker voor handen is.

@code {

 [CascadingParameter]
  public Task<AuthenticationState> 
  authenticationState { get; set; }

  protected override async Task OnInitializedAsync()
  {
    ...
  }

}

up | down

Profiel.razor

Profiel.razor is het eerste scherm wat getoond gaat worden bij het opstarten van de applicatie. We gebruiken in de (client) Razor-component de <AuthorizeView> en aanvankelijk zal alles getoond worden wat tussen de <NotAuthorized>-tags staat omdat de gebruiker op dat moment nog niet ingelogd is.

<h1>Hallo wereld...</h1>
Dit is je nieuwe Blazor app, welkom!
We illusteren het één en ander met 
het voorbeeld van een sportschool.<br />

<AuthorizeView>

 <Authorized>
    Goedendag @lid.Voornaam. 
    Je hebt jezelf aangemeld en je ID is @ID.<br />
    Hieronder staan je gegevens. 
    De webpagina's van je nieuwe app kun je nu bekijken.
    ...
    @if (Expired)
    {
      Let op, je abonnement is niet meer geldig...
    }

    Je claims:<br />
    <ul>
      @foreach (var claim in ClaimsDieIemandHeeft)
      {
        <li>@claim.Type:&nbsp;@claim.Value</li>
      }
    </ul>
 </Authorized>

 <NotAuthorized>
    Ps: je bent niet (meer) aangemeld...
 </NotAuthorized>

</AuthorizeView>

Allerlei informatie over de ingelogde gebruiker is dankzij de Authentication State Provider voor handen. We hadden gezien dat we de context-object kunnen gebruiken om het één en ander getoond te doen krijgen. De AuthenticationState kunnen we ook met code uitlezen omdat we een [CascadingParameter] hebben toegevoegd aan de (client) Razor-component.

@code 
{
   ...
   private List<Claim> ClaimsDieIemandHeeft;
   ...

   [CascadingParameter]
   public Task<AuthenticationState> 
   authenticationState { get; set; }

   protected override async Task OnInitializedAsync()
   {
     var authenticatieStatus = await authenticationState;
     if (authenticatieStatus.User.Identity.IsAuthenticated)
     {
       ...
       // De claims ophalen die iemand heeft
       ClaimsDieIemandHeeft = 
       authenticatieStatus.User.Claims.ToList();
       ...
     }
   }

}

En we vullen met code variabele ClaimsDieIemandHeeft. De variabele lezen we uit in de user interface-gedeelte van de (client) Razor-component (regel 2124) waardoor we van de ingelogde gebruiker de claims zien.

up | down

Groepslessen.razor

We hadden gezien dat de AuthenticationState uit drie claims bestaat waaronder een claimExpired welke staat voor het wel of niet geldig zijn van een abonnement. We passen een vorm van autorisatie toe omdat het aangemeld wezen (authenticatie) niet volstaat. Je moet een bepaalde claim hebben om content te mogen zien. De claim is in dit geval een geldig abonnement, maar het kan ook een bepaalde ‘Role’ of wat anders zijn. We vullen met code variabele Expired:

@code
{
 private bool Expired;

 [CascadingParameter]
 public Task<AuthenticationState>
 authenticationState { get; set; }

 protected override async Task OnInitializedAsync()
 {
   var authenticatieStatus = 
   await authenticationState;
   if (authenticatieStatus.User.Identity.IsAuthenticated)
   {
     ...
     // Er is een claimExpired in de AuthenticationState
     var claimExpired = 
     authenticatieStatus.User.FindFirst
     (c => c.Type == ClaimTypes.Expired);
     Expired = Convert.ToBoolean(claimExpired?.Value);
   }
 }
}

In de user interface-gedeelte van de (client) Razor-component gaan we dingen tonen (of juist niet). Wat getoond wordt is afhankelijk van het geldig wezen van het abonnement van de ingelogde gebruiker. We maken daarbij gebruik van variabele Expired die op zijn beurt weer via de claimExpired gevuld wordt.

<AuthorizeView>

  <NotAuthorized>
     Voor deze webpagina is aanmelden verplicht.
  </NotAuthorized>

  <Authorized>
     @if (!Expired)
     {
        Je hebt een geldig abonnement. 
        Maak gebruik van je abonnement en 
        geef je hier op voor onze groepslessen.
     }
     else
     {
        Je hebt <b>geen</b> abonnement of 
        je abonnement is niet meer geldig.
        Je kan onbeperkt met de groepslessen meedoen 
        als je een geldig abonnement hebt.
     }
   </Authorized>

</AuthorizeView>

up | down

Slot

In deze post hebben we het één en ander toegelicht aan de hand van een fictieve sportschool die een website heeft. We hebben daarvoor een Blazor Web Assembly hosted solution gemaakt met Visual Studio 2019 waarbij we alles vanaf ‘scratch’ opbouwen en we geen gebruik maken van wat Microsoft out-of-the-box meelevert aan authenticatie- en autorisatie-tools (code, tabeldefinities, Blazor-pagina’s etc.). 

Een aantal termen en concepten hebben we eerst de revue laten doen passeren alvorens in te gaan op de details van de Blazor Web Assembly hosted solution. De gehanteerde jargon komt bij de bouw van de applicatie elke keer terug en dan is het voor de begripsvorming over wat je aan het doen bent wel handig om te weten waar die termen voor staan.

De eerste stappen voor de bouw van de webapplicatie beginnen met het “optuigen” van de server project en de shared project . We gebruiken in het project Cookie Authentication en dat geven we o.a. op in bestand Startup.cs bij methode ConfigureServices. De server project bevat uiteindelijk een aantal controller action methods voor de client. Met name de controller action method voor het inloggen is belangrijk omdat bij deze methode dingen ingesteld worden m.b.t. de cookies. Ook de verificatie van de inloggegevens vindt plaats bij de desbetreffende controller action method.

M.b.t. de bouw van de webapplicatie, de laatste stappen eindigen bij de client project. Een  Microsoft.AspNetCore.Components.Authorization package moet als eerste geïnstalleerd worden voor het doen toepassen van authenticatie en autorisatie in de client project. De Razor (client) componenten maken gebruik van de diensten van een Authentication State Provider waarmee men weet wat de “authenticatie status” van de gebruiker is. Wat de Razor (client) component dan uiteindelijk aan content gaat tonen, dat is afhankelijk van de AuthenticationState van de gebruiker waarbij het ook om een anonieme, nog niet ingelogde gebruiker kan gaan.

Cookie authentication is een techniek dat al jaren wordt toegepast binnen web applicaties. Andere authenticatietechnieken zoals JWT Web tokens zijn ook voor handen voor het doen doorvoeren van autorisatie en authenticatie in je web applicatie. Binnen de developer community zijn talloze meningen over welke authenticatievorm het beste is en in welke situatie een authenticatietechniek het beste tot haar recht komt.

‘Common practice’ is om tokens in situaties te gebruiken waarin een web applicatie over meerdere servers is opgeschaald. Daarbij staan de ‘server project’ en de web API server controllers op verschillende servers. Voor Cookie authentication moeten dan extra dingen gedaan worden omdat in zo’n situatie sprake is van meerdere domeinen en cookies alleen maar bedoeld zijn om gelezen te mogen worden in omgevingen in eenzelfde domein. Men zou dan kunnen kiezen voor een constructie dat Cookie authentication gebruikt wordt voor de Blazor Web Assembly applicatie en Token authentication voor de web API Server controllers. Meer over JWT Web tokens in deze post.

Hopelijk ben je met deze (uitgebreide) posting weer wat wijzer geworden en ik hoop je weer terug te zien in één van mijn volgende blog posts. Wil je weten wat ik nog meer over Blazor heb geschreven? Hit the Blazor button…

up

Laat een reactie achter

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