JWT authentication in Blazor

  1. Inleiding
  2. Solution
  3. Creatie JWT Web Token
    1. Aanmelden
    2. Local Storage
    3. jwt.io
    4. Secret Key
    5. Controller action method aanmelden
  4. JWT Web Token gebruik
    1. Authenticationstate Provider – MeldAan.razor
    2. Controller action method huidiglid
    3. Razor-componenten – Gym.Razor
  5. Slot

Inleiding

In de post over Cookie Authentication zagen we hoe we met Cookie Authentication bepaalde delen van een website kunnen afschermen. We zagen ook hoe we met Cookie Authentication bepaalde controller action methods van web API server controllers kunnen afschermen voor personen die zich niet hebben aangemeld (anonymous users).

JWT Web Tokens (Jason Web Tokens) hebben omstreeks 2010 hun intrede gedaan en ze zijn inmiddels een internet standaard geworden (RFC 7519). Met JWT Web Tokens kan ook het één en ander bewerkstelligd worden m.b.t. de autorisatie en de authenticatie. Common practice is om JWT Web Tokens in situaties te gebruiken waarin een web applicatie over meerdere servers is opgeschaald en de ‘server project’ en de web API server controllers op verschillende servers staan.

We zullen het één en ander toelichten aan de hand van dezelfde fictieve sportschool die we aanhaalden in de post over Cookie Authentication. Het verdient dan ook de aanbeveling om eerst de post over Cookie Authentication door te nemen daar deze post een vervolg is op die post. De functionaliteit van de voorbeeldapplicatie is t.o.v. de Cookie Authentication post precies hetzelfde en het enige verschil is dat voor de autorisatie en authenticatie gebruik wordt gemaakt van JSON Web Tokens.

De voorbeeldcode vind je in deze Github repository. Het advies is om onderstaand door te nemen aan de hand van de voorbeeldcode die bij deze post hoort.

up | down

Solution

De voorbeeldapplicatie in deze post is een Blazor Web Assembly hosted solution waarvoor in Visual Studio 2019 de Blazor Web Assembly Hosted template is gebruikt.

We gebruiken de volgende componenten voor de toelichting hoe een JWT Web Token te creëren:

  • Razor-component MeldAan.razor (JWTWebToken.Client)
  • Controller action method aanmelden van Web API server controller SportschoolController (JWTWebToken.Server)
  • Klasse AuthenticatieVraag (JWTWebToken.Shared)
  • Klasse AuthenticatieAntwoord (JWTWebToken.Shared)
  • Configuratiebestand appsettings.json (JWTWebToken.Server)

We gebruiken de volgende componenten voor de toelichting hoe een JWT Web Token te gebruiken:

  • Authentication State Provider AuthenticatieStatus (JWTWebToken.Client)
  • Controller action method huidiglid van Web API server controller ProfielController (JWTWebToken.Server)
  • Razor-component Gym.razor (JWTWebToken.Client)
  • methode gegevenslid van Service ProfielService (JWTWebToken.Client)
  • Handler DelegatieHandler (JWTWebToken.Client)

up | down

JWT Web Token creatie

Hoe wordt een JWT Web token gecreëerd? In de voorbeeldcode doen we dat vanuit Razor-component MeldAan.razor.

Aanmelden

Controller action method aanmelden (regel 7-8) wordt aangeroepen na het invullen van een e-mailadres en een wachtwoord. De controller action method is onderdeel van web API server controller Sportschool.

public async Task Aanmelden()
{
  try
  {
    fout = string.Empty;
    var resultaat = 
       await httpClient.PostAsJsonAsync<AuthenticatieVraag>
       ($"/api/Sportschool/aanmelden", authenticatieVraag);

    if (resultaat.IsSuccessStatusCode)
    {
      AuthenticatieAntwoord authenticatieAntwoord = 
      await resultaat.Content
      .ReadFromJsonAsync<AuthenticatieAntwoord>();
      token = authenticatieAntwoord.Token;
      
      if (token != string.Empty)
      {
        // sla de token op
        await _localStorageService.SetItemAsync
        ("jwt_token", token);
        
        // gebruik de true-parameter om te refreshen en
        _navigationManager.NavigateTo("/", true);
      }
      else
      {
        fout = 
        "Het aanmelden is niet gelukt. 
        Probeer het opnieuw.";
      }
    }
  }
  catch
  {
    fout = 
    "Het aanmelden is niet gelukt. 
    Probeer het opnieuw.";
  }
}

Input voor controller action method aanmelden is een AuthenticatieVraag-object en een AuthenticatieAntwoord-object. De klassenmodules zijn terug te vinden in de Shared project:

public class AuthenticatieVraag
{
  [Required(ErrorMessage = "Geef je e-mailadres op.")]
   public string Email { get; set; }
        
   [Required(ErrorMessage = "Geef je wachtwoord op.")]
   public string Password { get; set; }
}

public class AuthenticatieAntwoord
{
   public string Token { get; set; }
}

up | down

Local Storage

Controller action method aanmelden genereert een JWT Web Token en de JWT Web Token wordt als een string geretourneerd waarna Razor-component MeldAan.razor de token opslaat in de local storage van de desbetreffende client computer (regel 19 – 21) .

JWT Web Token authentication valt binnen de Stateless REST architectuurstijl want de client krijgt weliswaar een token van de server, maar de server gaat zich niet bezighouden met de opslag van de token. De client mag de token zelf opslaan in haar local storage. We zien na een succesvolle aanmelding de token dan ook terug als jwt_token in de local storage.

up | down

jwt.io

De token kunnen we nader bekijken met https://jwt.io/. In dit voorbeeld hebben we gebruik gemaakt van de HS256-Algorithm voor het doen genereren van de token.

We hebben op de https://jwt.io/-pagina de signature geverifieerd met de waarde van de geheime sleutel waarmee de JWT Web Token is gegenereerd. Dezelfde geheime sleutel wordt gebruikt voor de verificatie van de JWT Web Token.

up | down

Secret Key

Je moet een token meesturen bij elk verzoek naar de server voor die controller action methods waarvoor authenticatie en autorisatie verplicht zijn.

De token wordt geverifieerd met een geheime sleutel en de sleutel moet dan ook geheim blijven zodat de authenticiteit van de token gewaarborgd blijft. We bewaren in dit voorbeeld de waarde van de geheime sleutel in de appsettings.json-configuratiebestand op de server. De waarde van de geheime sleutel moet een lengte hebben van minimaal 32 karakters.

Er zijn uiteraard betere manieren voor het doen opslaan van de waarde van een geheime sleutel en op het internet is wel het één en ander terug te vinden over dat onderwerp. We doen het in ons voorbeeld gemakshalve op deze manier:

up | down

Controller action method aanmelden

Ten slotte de code van controller action method aanmelden welke onderdeel is van web API server controller Sportschool. De controller action method genereert een JWT Web Token en we zien dat de token claims bevat die door andere componenten gebruikt zullen worden. De gegenereerde tokens hebben in dit voorbeeld een geldigheidsduur van 30 seconden (regel 62).

[HttpPost("aanmelden")]
public async Task<ActionResult<AuthenticatieAntwoord>> 
           aanmelden(AuthenticatieVraag authenticatieVraag)
{
  string token = string.Empty;

  // MaakHash password
  authenticatieVraag.Password = 
  Utility.MaakHash(authenticatieVraag.Password);

  // Lid aangemeldLid = new Lid();
  Lid aangemeldLid = 
  await _model.HaalopLid
  (authenticatieVraag.Email, authenticatieVraag.Password);

  // Evalueren dat correct is ingelogd
  if (aangemeldLid != null)
  {
    // Token genereren
    token = GenereerJwtToken_hs256(aangemeldLid);
    Debug.WriteLine("GenereerJwtToken_hs256:");
    Debug.WriteLine(token);
    Debug.WriteLine("");
  }
  return await Task.FromResult
  (new AuthenticatieAntwoord() 
  { Token = token });
}

private string GenereerJwtToken_hs256(Lid lid)
{
  //getting the secret key
  string geheimeSleutel = 
  _configuration["JWTSettings:GeheimeSleutel"];
  var sleutel = 
  Encoding.ASCII.GetBytes(geheimeSleutel);

  //create claims
  var claimID = 
  new Claim(ClaimTypes.NameIdentifier, 
                    Convert.ToString(lid.ID));
  var claimNaam = 
  new Claim
  (ClaimTypes.Name, lid.Voornaam + " " + 
    lid.Achternaam);

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

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

  // AuthenticationProperties 
  // Expires = DateTime.UtcNow.AddDays(1),
  var tokenDescriptor = 
  new SecurityTokenDescriptor
  {
   Subject = claimsIdentity,
   Expires = 
   DateTime.UtcNow.AddSeconds(30),
   SigningCredentials = 
   new SigningCredentials
   (new SymmetricSecurityKey(sleutel), 
     SecurityAlgorithms.HmacSha256Signature)
  };

  //creating a token handler
  var jwtSecurityTokenHandler = 
  new JwtSecurityTokenHandler();
  var token = 
  jwtSecurityTokenHandler
  .CreateToken(tokenDescriptor);

  //returning the token back
  return jwtSecurityTokenHandler.WriteToken(token);
}

up | down

JWT Web Token gebruik

De JWT Web token wordt door de Authenticationstate Provider en door diverse Razor-componenten gebruikt. We zullen het één en ander toelichten met component MeldAan.razor welke de Authenticationstate Provider triggert. Verder zullen we Razor component Gym.razor aanhalen om het gebruik van een JWT Web Token nader toe te lichten

up | down

Authenticationstate Provider – MeldAan.razor

Het aanmelden vanuit Razor-component MeldAan.razor leidt tot de creatie van een JWT Web Token (regel 13 – 16) dat in de local storage wordt gezet (regel 20 – 22).

public async Task Aanmelden()
{
 try
 {
   fout = string.Empty;
   var resultaat = 
   await httpClient.PostAsJsonAsync<AuthenticatieVraag>
   ($"/api/Sportschool/aanmelden", authenticatieVraag);

   if (resultaat.IsSuccessStatusCode)
   {

     AuthenticatieAntwoord authenticatieAntwoord = 
     await resultaat.Content.ReadFromJsonAsync
     <AuthenticatieAntwoord>();
     token = authenticatieAntwoord.Token;

     if (token != string.Empty)
     {
       // sla de token op
       await _localStorageService.SetItemAsync
       ("jwt_token", token);

       // gebruik de true-parameter 
       // om te refreshen en
       _navigationManager.NavigateTo("/", true);

De authenticationstate provider wordt vervolgens getriggerd door MeldAan.razor waarbij de authenticationstate provider gebruik maakt van de eerder gegenereerde JWT Web Token (in methode .GetAuthenticationStateAsync() van klasse AuthenticatieStatus die erft van base class AuthenticationStateProvider).

public async override 
Task<AuthenticationState> GetAuthenticationStateAsync()
{
  try
  {
     // Het object kan gevuld worden 
     // als succesvol is ingelogd
     Lid lid = await HuidigLid();

     if (lid != null && lid.ID != 0)
     {
         //creëer claims uit het geretourneerde object
         ...
         return new AuthenticationState(claimsPrincipal);
     }
     else
          // het lid kan niet gevonden 
          // retourneer een lege ClaimsPrincipal
          return new AuthenticationState
          (new ClaimsPrincipal(new ClaimsIdentity()));
     }
  }
  catch ...
}

De authentication state provider roept op zijn beurt controller action method huidiglid aan van web API server controller ProfielController (regel 17 – 19). Voor de aanroep wordt de JWT Web Token uit de local storage gehaald en in de header gezet van de HTTP-verzoek naar controller action method huidiglid (regel 13 – 15).

public async Task<Lid> HuidigLid()
{
  //pulling the token from localStorage
  var token = await 
  _localStorageService.GetItemAsStringAsync("jwt_token");
  if (token == null) return null;

  // Eerste karakter verwijderen
  token = token.Remove(0, 1);
  // Laatste karakter verwijderen
  token = token.Remove(token.Length - 1, 1);

  // Zet de token in de header
  _httpClient.DefaultRequestHeaders
  .Add("Authorization","Bearer " + token);

  var response = 
  await _httpClient.GetAsync("/api/Profiel/huidiglid");
  response.EnsureSuccessStatusCode();

  var huidigLid = 
  await response.Content.ReadFromJsonAsync<Lid>();

   // returning the user if found
   if (huidigLid != null) 
   return 
   await Task.FromResult(huidigLid); else return null;
}

En we zien de token in de header van de HTTP-request naar controller action method huidiglid:

up | down

Controller action method huidiglid

In controller action method huidlid (deel uitmakend van Web API server controller ProfielController) wordt de binnengekregen JWT Web Token geverifieerd.

[Route("huidiglid")]
[HttpGet]
public async Task<ActionResult<Lid>> huidiglid()
{
  try
  {
    Lid huidigLid = new Lid();
    string geheimeSleutel = 
     _configuration["JWTSettings:GeheimeSleutel"];
    var sleutel = Encoding.ASCII.GetBytes(geheimeSleutel);

    var tokenValidationParameters = new TokenValidationParameters
    {
      ValidateIssuerSigningKey = true,
      IssuerSigningKey = new SymmetricSecurityKey(sleutel),
      ValidateIssuer = false,
      ValidateAudience = false
     };

     var tokenHandler = new JwtSecurityTokenHandler();
     SecurityToken securityToken;
     
     var jwt_token = Request
      .Headers[HeaderNames.Authorization].ToString()
      .Replace("Bearer ", "");
     
     // We simuleren een situatie dat met de token is geprutst
     // door de laatste karakter te verwijderen
     // jwt_token = jwt_token.Remove(jwt_token.Length - 1, 1);
     
     //validating the token
     var principle = 
     tokenHandler.ValidateToken
     (jwt_token, tokenValidationParameters, out securityToken);

     var jwtSecurityToken = (JwtSecurityToken)securityToken;
     if (jwtSecurityToken != null && 
         jwtSecurityToken.Header.Alg
         .Equals(SecurityAlgorithms.HmacSha256, 
         StringComparison.InvariantCultureIgnoreCase))
        {
           //returning the user if found
           var claimID = 
           principle.FindFirst(ClaimTypes.NameIdentifier)?.Value;
           huidigLid = 
           await _model.HaalopLid(Convert.ToInt32(claimID));
         }
         return Ok(huidigLid);
    }

    catch (Exception ex)
    {
       Debug.WriteLine("");
       Debug.WriteLine
       ("Error LidController Controller huidiglid()");
       Debug.WriteLine(ex.Message);
       Debug.WriteLine("");
       // throw;
       return Ok(new Lid());
     }
}

up | down

Razor componenten – Gym.razor

Methode OnInitializedAsync() wordt getriggerd bij het opstarten van een Razor component. Zo bevat de methode voor Razor component Gym.razor code voor het doen uitlezen van de claims uit de authentication state. Methode gegevenslid van service ProfielService wordt vervolgens aangeroepen (regel 21) waarbij methode .gegevenslid() de waarde meekrijgt van de claimID.

@code {
 private int ID;
 private Lid lid = new Lid();

 [CascadingParameter]
 public Task<AuthenticationState> 
 authenticationState { get; set; }
 
 protected override async Task OnInitializedAsync()
 {
  var authenticatieStatus = 
  await authenticationState;
  if (authenticatieStatus.User.Identity.IsAuthenticated)
  {
    // Er is een claimID in authenticationState.
    var claimID = authenticatieStatus.User.FindFirst
    (c => c.Type == ClaimTypes.NameIdentifier);

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

    lid = await ProfielService.gegevenslid(ID);
    if (lid == null || lid.ID == 0)
    { // Als het resultaat leeg is ...
      navigationManager.NavigateTo("/", true);
    } 
   }
 }
}

In service ProfielService wordt controller action method gegevenslid van web API server controller ProfielController aangeroepen (regel 10 – 12):

public class ProfielService : IProfielInterface
{
 ...
 public async Task<Lid> gegevenslid(int ID)
 {
  Lid lid = new Lid();

  try
  {
   var opgehaald = await 
   _httpClient
   .GetAsync("/api/Profiel/gegevenslid" +  "/" + ID);

   lid =  await opgehaald.Content.ReadFromJsonAsync<Lid>();
   return lid;
  }
  
  catch (Exception ex) ...
}

De aanroep wordt onderschept door methode .SendAsync() van klasse DelegatieHandler (die erft van base class DelegatingHandler), zijnde een handler die een JWT Web Token uit de local storage haalt en in de header zet van de HTTP-verzoek (regel 21 – 24):

protected override async Task<HttpResponseMessage> 
SendAsync 
(HttpRequestMessage httpRequestMessage, 
 CancellationToken cancellationToken)
{

 try
 {
  //de token uit de localStorage halen
  string token =  await
  _localStorageService.GetItemAsStringAsync("jwt_token");

  //token toevoegen aan de authorization header
  if (token != null)
  {
   // Eerste karakter verwijderen
   token = token.Remove(0, 1);
   // Laatste karakter verwijderen
   token = token.Remove(token.Length - 1, 1);

   // Toevoegen token
   httpRequestMessage
   .Headers.Authorization = 
   new AuthenticationHeaderValue("Bearer", token);
  }

  //sending the request
  return await 
  base
  .SendAsync(
  httpRequestMessage, 
  cancellationToken);
}
catch ...
}

En we zien de token in de header van de HTTP-request naar controller action method gegevenslid:

up | down

Slot

Deze post is een vervolg op de post over Cookie Authentication. De functionaliteit van de voorbeeldapplicatie is t.o.v. de Cookie Authentication post precies hetzelfde en het enige verschil is dat voor de autorisatie en authenticatie gebruik wordt gemaakt van JSON Web Tokens.

We begonnen de post met een schrijven hoe JSON Web Tokens gecreëerd worden. Daarbij kunnen we de token met https://jwt.io/ aan een nader onderzoek onderwerpen. Een geheime sleutel is van belang voor het creëren en het kunnen verifiëren van de JSON Web Token. De sleutel moet dan ook geheim blijven omdat de authenticiteit van de token anders niet gewaarborgd kan worden. In dit voorbeeld bewaren we de waarde van de geheime sleutel in de appsettings.json-configuratiebestand op de server.

We zijn ten slotte ingegaan op het gebruik van de JSON Web Tokens. Je moet de token meesturen bij elk verzoek naar de server voor die controller action methods waarvoor authenticatie en autorisatie verplicht zijn. Twee belangrijke ‘consumenten’ van JSON Web Tokens zijn de Authenticationstate Provider en de Razor-componenten en we hebben het één en ander toegelicht aan de hand van de MeldAan.razor-component (die de Authentication Provider triggert) en de Gym.razor-component.

Hopelijk ben je met deze 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

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