Blazor WebAssembly, Teil 3: Authentifizierung und Autorisierung

Die MiracleList-Webanwendung soll im dritten Teil des Tutorials nun um einen Anmeldedialog zur Authentifizierung erweitert werden.

vorlesen Druckansicht 1 Kommentar lesen
Lesezeit: 11 Min.
Von
  • Dr. Holger Schwichtenberg
Inhaltsverzeichnis

Bereits beim Anlegen eines Blazor-WebAssembly-Projekts in Visual Studio oder an der Kommandozeile mit dotnet new gibt es Optionen, eine Authentifizierung zu konfigurieren. Mit der Option ASP.NET Core Hosted in Verbindung mit Individual Accounts | Store user accounts in-app erhält man eine Mischung aus einer Blazor-WebAssembly-Anwendung mit einer in ASP.NET Core Razor (als serverseitige Lösung) realisierten Benutzeranmeldung und -verwaltung. Alternativ dazu kann man sich mit einem OIDC-Provider (Open ID Connect) verbinden.

Blazor-Tutorial

Der dritte des Tutorials zeigt, wie man ein beliebiges WebAPI-Backend zur Authentifizierung verwenden kann, auch wenn dieses nicht OIDC-kompatibel ist. Das MiracleList-Backend realisiert einen eigenen Authentifizierungsmechanismus auf Basis von Benutzernamen und Kennwort zur Anmeldung, der dann für die folgenden WebAPI-Aufrufe ein Authentifizierungs-Token (als Zeichenkette) liefert. Im ersten Teil des Tutorials wurde die Klasse AuthenticationManager implementiert, die diesen Mechanismus in der Methode Login() kapselt. Bisher wurde sie in der Komponente Index.razor aufgerufen. Das soll nun auf eine eigenständige Anmeldeseite verlagert werden.

Videos by heise

Die Abbildung 1 visualisiert die im MiracleList-Blazor-Frontend zu erstellende Anmeldeseite mit Eingabe für Benutzername und Kennwort sowie eine Schaltfläche. Listing 1 zeigt die zugehörige Razor-Template-Seite Login.razor und Listing 2 die passende Code-Behind-Datei.

Aus der Razor Component "Login.razor" erzeugte Anmeldeseite (Abb. 1)
using�Microsoft.AspNetCore.Components;
using�Microsoft.AspNetCore.Components.Authorization;
using�System.Threading.Tasks;
 
namespace�Web
{
�public�class�LoginModel�:�ComponentBase
ďż˝{
��[Inject]�public�NavigationManager�NavigationManager�{�get;�set;�}
��[Inject]�AuthenticationStateProvider�asp�{�get;�set;�}�=�null;
 
��#region�Properties�f�r�Datenbinding
��public�string�Username�{�get;�set;�}
��public�string�Password�{�get;�set;�}
��public�string�Message�{�get;�set;�}
��#endregion
 
��protected�override�async�System.Threading.Tasks.Task�OnInitializedAsync()
��{
���//�Reaktion�auf�diese�URL
���if�(this.NavigationManager.Uri.ToLower().Contains("/logout"))
���{
����await�((AuthenticationManager)asp).Logout();
���}
��}
 
��///�<summary>
��///�Reaktion�auf�Benutzeraktion
��///�</summary>
��protected�async�Task�Login()
��{
���Message�=�"Logging�in...";
���bool�ok�=�await�(asp�as�AuthenticationManager).Login(Username,�Password);
���if�(ok)�this.NavigationManager.NavigateTo("/main");
���else�Message�=�"Login�Error!";
��}
�}�//�end�class�Login
}

Listing 1: Razor-Template fĂĽr die Anmeldeseite (Login.razor)

@pageďż˝"/"
@pageďż˝"/Logout"
@inherits�LoginModel;
 
<div�class="panel�panel-primary">
�<div�class="panel-heading">
��<h2>Benutzeranmeldung</h2>
ďż˝</div>
�<div�class="panel-body">
��<div>MiracleList�ist�eine�Beispielanwendung�f�r�eine�Single-Page-Webapplication�(SPA)�mit�ASP.NET�Core�Blazor�Webassembly�(@System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription)�zur�Aufgabenverwaltung.</div>
��<div>Autor:�Dr.�Holger�Schwichtenberg,�<a�href="http://www.IT-Visions.de">www.IT-Visions.de</a>,�2018-@DateTime.Now.Year</div>
��<br�/>
��<div>
���Zur�Benutzeranmeldung�geben�Sie�die�Kombination�aus�Ihrer�E-Mail-Adresse�und�einem�beliebigen�Kennwort�ein.�Wenn�es�f�r�diese�E-Mail-Adresse�noch�kein�Benutzerkonto�gibt,�wird�automatisch�ein�neues�Benutzerkonto�angelegt�mit�einigen�Beispielaufgaben.
��</div>
��<br>
 
��<div�class="row">
���<div�class="col-xs-12�form-group">
����<label�for="name">E-Mail-Adresse:</label>
����<input�name="name"�@bind="this.Username"�id="name"�type="text"�placeholder="Ihre�E-Mail-Adresse"�class="form-control">
���</div>
��</div>
 
��<div�class="row">
���<div�class="col-xs-12�form-group">
����<label�for="password">Kennwort</label>
����<input�name="password"�@bind="this.Password"�id="password"�placeholder="Ihr�Kennwort"�type="password"�class="form-control">
���</div>
��</div>
 
��<button�@onclick="Login"�class="btn�btn-primary"�type="submit"�id="Anmelden"�name="Anmelden">Anmelden</button>
��<span�id="errorMsg"�class="text-danger">@ErrorMsg</span>
ďż˝</div>
</div>
 
<text>MiracleList�Tutorial�v</text>@System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()

Listing 2: Code-Behind-Datei fĂĽr die Anmeldeseite (Login.razor.cs)

Im Programmcode sehen die Leserinnen und Leser erstmals in diesem Tutorial, wie man von einer URL auf eine andere umlenkt. Dazu lässt man sich eine Instanz der in Blazor vordefinierten Klasse NavigationManager

[Inject] public NavigationManager NavigationManager { get; set; }

injizieren und nutzt dann auf der Instanz die Methode NavigateTo() unter Angabe der relativen Ziel-URL:

this.NavigationManager.NavigateTo("/abc/def");

Zudem kann man mit dem NavigationManager auch die aktuelle URL abfragen. In Listing 1 sind für die Razor Component Login.razor direkt zwei verschiedene Routen mit @page-Direktiven definiert. Das ist gestattet in Blazor; es obliegt Softwareentwicklern, dem eine Bedeutung zu geben. Im Programmcode im Lebenszyklusereignis OnInitializedAsync() (Listing 2) wird daher unterschieden, dass beim Aufruf mit der relativen Adresse /logout eine Abmeldung stattfindet und dann wieder das Anmeldeformular erscheint. Das ist eine elegante Lösung, die erlaubt, dass man von überall her durch Aufruf der URL http://server/logout die Anmeldung ausführen kann.

Damit der überarbeitete Programmcode der Code-Behind-Datei Login.razor.cs wieder kompiliert, ist im AuthenticationManager eine Routine Logout() zu ergänzen, die die Operation /Logoff im Server ruft (s. Listing 3).

using�System.Threading.Tasks;
using�System;
using�System.Security.Claims;���//�NEU�in�Teil�3
using�Microsoft.AspNetCore.Components.Authorization;���//�NEU�in�Teil�3
using�MiracleListAPI;
 
namespace�Web
{
ďż˝///ďż˝<summary>
�///�Authentifizierung�zum�Debugging
ďż˝///ďż˝</summary>
�public�class�AuthenticationManager�:�AuthenticationStateProvider�//�Vererbung�NEU�in�Teil�3
ďż˝{
��MiracleListAPI.MiracleListProxy�proxy�{�get;�set;�}
 
��public�AuthenticationManager(MiracleListAPI.MiracleListProxy�proxy)
��{
���this.proxy�=�proxy;
��}
 
��public�const�string�ClientID�=�"11111111-1111-1111-1111-111111111111";
��public�LoginInfo�CurrentLoginInfo�=�null;
 
��public�string�Token�{�get�{�return�CurrentLoginInfo?.Token;�}�}
 
��///�<summary>
��///�Login�to�be�called�by�Razor�Component�Login.razor
��///�</summary>
��public�async�Task<bool>�Login(string�username,�string�password)
��{
���bool�result�=�false;
���CurrentLoginInfo�=�null;
���var�l�=�new�LoginInfo()�{�Username�=�username,�Password�=�password,�ClientID�=�AuthenticationManager.ClientID�};
���try
���{
����CurrentLoginInfo�=�await�proxy.LoginAsync(l);
����if�(String.IsNullOrEmpty(CurrentLoginInfo.Token))
����{
�����Console.WriteLine("Anmeldung�NICHT�erfolgreich:�"�+�this.CurrentLoginInfo.Username);
����}
����else
����{
�����result�=�true;
�����Console.WriteLine("Anmeldung�erfolgreich:�"�+�this.CurrentLoginInfo.Username);
����}
���}
���catch�(Exception�ex)
���{
����Console.WriteLine("Anmeldefehler:�"�+�ex.Message);
���}
���Notify();
���return�result;
��}
 
��///�<summary>
��///�Logout�to�be�called�by�Razor�Component�Login.razor
��///�</summary>
��public�async�Task�Logout()
��{
���Console.WriteLine("Logout",�this.CurrentLoginInfo);
���if�(this.CurrentLoginInfo�==�null)�return;
���var�e�=�await�proxy.LogoffAsync(this.CurrentLoginInfo.Token);
���if�(e)
���{
����//�Remove�LoginInfo�in�RAM�for�clearing�authenticaton�state
����CurrentLoginInfo�=�null;
����Notify();
���}
���else
���{
����Console.WriteLine("Logout�Error!");
���}
��}
 
��///�<summary>
��///�NEU�in�Teil�3:�Notify�Blazor�infrastructure�about�new�Authentication�State
��///�</summary>
��private�void�Notify()
��{
���Console.WriteLine("Notify:�"�+�CurrentLoginInfo?.Username);
���this.NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
��}
 
��//�NEU�in�Teil�3
��public�override�async�Task<AuthenticationState>�GetAuthenticationStateAsync()
��{
���if�(this.CurrentLoginInfo�!=�null�&&�!String.IsNullOrEmpty(this.CurrentLoginInfo.Token)�&&�!String.IsNullOrEmpty(proxy.BaseUrl))
���{
����const�string�authType�=�"MiracleList�WebAPI�Authentication";
����var�identity�=�new�ClaimsIdentity(new[]
����{
����new�Claim("Backend",�proxy.BaseUrl),
����new�Claim(ClaimTypes.Sid,�this.CurrentLoginInfo.Token),�//�use�SID�claim�for�token
����new�Claim(ClaimTypes.Name,�this.CurrentLoginInfo.Username),
����},�authType);
 
����var�cp�=�new�ClaimsPrincipal(identity);
����var�state�=�new�AuthenticationState(cp);
����Console.WriteLine("GetAuthenticationStateAsync:�"�+�this.CurrentLoginInfo.Username);
����return�state;
���}
���else
���{
����Console.WriteLine("GetAuthenticationStateAsync:�no�user");
����var�state�=�new�AuthenticationState(new�ClaimsPrincipal(new�ClaimsIdentity()));
����return�state;
���}
��}
ďż˝}
}

Listing 3: Erweiterte Klasse AuthenticationManager

Zudem ist die Login()-Methode umzugestalten: Anstelle die statischen Anmeldedaten zu nutzen, benötigt diese nun zwei vom Aufrufer setzbare Parameter für Benutzername und Kennwort:

public async Task<bool> Login(string username, string password)
{
   var l = new LoginInfo() { Username = username, Password = password, ClientID = AuthenticationManager.ClientID };
...
}

Der Aufruf von Login() in Index.razor.cs muss gänzlich entfallen. Aus

  protected override async Task OnInitializedAsync()
  {
   // TODO: Muss sp�ter im Anmeldebildschirm erfolgen
   if (await am.Login())
   {
   await ShowCategorySet();
   }
  }

wird also

  protected override async Task OnInitializedAsync()
  {
   await ShowCategorySet();
  }

Damit kompiliert die Anwendung wieder, beim Start läuft sie aber sofort auf einen Laufzeitfehler: Die Fehlermeldung "System.InvalidOperationException: The following routes are ambiguous: '/' in 'Web.Pages.Login' '/' in 'Web.Pages.Index'" ist dabei aussagekräftig: Jede Route lässt sich nur an genau eine Komponente vergeben. Es wurden aber für den Start der Anwendung zwei Wurzelkomponenten festgelegt. Daher gilt es, in der Datei Index.razor die Route zu ändern von

@page "/"

in

@page "/main"

Nun werden Entwickler beim Anwendungsstart auf das Anmeldeformular gelenkt und nach erfolgreicher Anmeldung per NavigationManager mit this.NavigationManager.NavigateTo("/main") zur Hauptkomponente Index.razor. Aber leider verhindert noch nichts, dass Benutzer später direkt http://server/main aufrufen und die Anmeldung umgehen.