In questa serie di articoli verrà realizzata un’applicazione per la gestione degli allenamenti, che prevede 3 livelli di accesso:

  • Anonimo: per la visualizzare le pagine pubbliche
  • Atleta: per il cliente che usufruisce dei servizi erogati
  • Admin: per la gestione amministrativa del sito

Per regolamentare le tipologie di accesso sopra descritte è necessario, oltre alla registrazione dell’utente, anche un sistema di autorizzazioni. Utilizzeremo quindi:

  • Blazor WebAssembly per il frontend
  • ASP.NET Core per le API ed autenticazione/autorizzazione
  • ASP.NET Identity per la gestione degli utenti
  • Database SQL Server
  • Entity Framework come ORM
  • JWT Token

Prima di iniziare, vorrei spendere due parole sull’implementazione di autenticazione / autorizzazione presente nelle ultime release di Blazor WebAssembly. Come è possibile leggere nella documentazione, è presente il supporto ad Open ID Connect, tant’è che quando viene creato il progetto con Visual Studio, c’è già tutto il necessario per gestire gli utenti. Non ho utilizzato tale implementazione perché le richieste di Login / Registrazione avvengono tramite le pagine di default di Identity Server, mentre io preferisco avere queste pagine all’interno del front-end Blazor.

Pagina Registrazione di ASP.NET Identity

Sicuramene possiamo andare a personalizzare le pagine ma per adesso ho preferito non farlo e seguo fiducioso questa richiesta su GitHub.

Creazione del progetto

Siamo pronti ad iniziare. Apriamo Visual Studio e creiamo un nuovo progetto App Blazor:

Visual Studio Crea Nuovo Progetto

Dopo avergli dato un nome ed impostato una cartella dove salvare il progetto, clicchiamo su “Crea” e scegliamo Blazor WebAssembly App e spuntiamo “ASP.NET Core hosted”:

Blazor WebAssembly template

Il template di creazione del progetto non ha, ovviamente, tutti i pacchetti necessari alla nostra app, andiamo quindi ad installarl cliccando con il tasto destro sul progetto “Server” e selezionando “Gestisci pacchetti NuGet…”:

Finestra aggiunta pacchetti Nuget

Nella schermata che si aprirà, clicchiamo su “Sfoglia” ed installiamo questi pacchetti:

  • Microsoft.AspNetCore.Authentication.JwtBearer
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore
  • Microsoft.AspNetCore.Identity.UI
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

Installazione pacchetto Nuget

A questo punto abbiamo tutto il necessario.

Configurazione del database

Come anticipato inizialmente, il backend è basato su SQL Server e verrà utilizzato Entity Framework per l’acceso ai dati. E’ necessario quindi impostare la stringa di connessione e per farlo andiamo ad editare il file appsettings.json che si trova sulla root del progetto server:

File appsettings.json

Al suo contenuto iniziale, adiamo ad aggiungere la stringa di connessione:

Configurazione stringa connessione in appsettings.json

Il valore di DefaultConnection è così formato: "Data Source=NOME_DEL_SERVER;Initial Catalog=NOME_DEL_DATABASE;Persist Security Info=True;Connection Timeout=0;User ID=USERNAME;Password=PASSWORD"

Il mio approccio sarà di tipo Code First, quindi partendo dal modello mi affiderò ai tool di Entity Framework per creare la struttura del database. Per prima cosa è necessario creare il DBContext: andiamo a creare una cartella chiamata “Data” dentro il progetto “Server” ed al suo interno creiamo la classe “ApplicationDbContext.cs”:

File ApplicationDbContext

Il contenuto di tale classe è molto semplice perché sarà necessario solo estendere la classe IdentityDbContext, creare il costruttore e configurare il metodo OnModelCreating, che per adesso richiama le funzionalità del parent:

Contenuto di ApplicationDbContext

Ovviamente sarà necessario importare i namespace richiesti. A questo punto possiamo configurare la nostra applicazione “Server” per utilizzare tutto ciò che abbiamo installato fino a questo punto. Per farlo è necessario aprire il file Startup.cs, che si trova sempre nel progetto Server, e modificare il metodo ConfigureServices. Nello specifico andiamo ad aggiungere:

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddDefaultIdentity<IdentityUser>(options => {
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireDigit = false;
    options.Password.RequiredLength = 8;
})
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = Configuration["Jwt:Issuer"],
            ValidAudience = Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecurityKey"]))
        };
    });

Analizziamo il codice che abbiamo scritto. Le prime 2 righe indicano qual è il DbContext da utilizzare e dove trovare la stringa di connessione del database. Chi ha un po’ di praticità con Entity Framework avrà già visto queste istruzioni.

Abbastanza esplicativa è anche la porzione di codice services.AddDefaultIdentity<IdentityUser>(): non facciamo altro che dire ad ASP.NET Identity quali caratteristiche devono avere la password e che store usare. In ambiente di test preferisco usare password che non siano necessariamente alfanumeriche e quindi tramite le direttive options mi semplifico la vita.

Con l’ultima parte, services.AddAuthentication(), configuriamo l’autenticazione tramite token di tipo JWT Bearer, prendendo la chiave, l’issuer e l’audience dal file “appsettings.json”:

Parametri configurazione token JWT

Inizializzazione del database

Arrivati a questo punto siamo pronti per creare le tabelle necessarie ad ASP.NET Identity. Per farlo apriamo la “Console di Gestione pacchetti” andando su Strumenti -> Gestione pacchetti Nuget:

Console Gestione pacchetti NuGet

Andiamo a creare la prima migrazione con il comando Add-Migration InitDBTrain.Ecco il risultato:

Migration Entity Framework

Verrà creato e visualizzato un file C# contenete tutte le modifiche che saranno apportate dal database. InitDBTrain è solo la descrizione della migrazione appena creata quindi potete personalizzarla come credete. Per attuare tali modifiche sarà necessario il comando Update-Database:

Aggiornamento del database con Entity Framework

Andando a verificare il database, vedremo le tabella create:

Aggiornamento del database con Entity Framework

Estendere il modello di ASP.NET Identity

Di default ASP.NET Identity non è predisposto a persistere alcune informazioni che possono tornare utili quando si tratta di archiviare dati utente come nome, cognome o data di nascita. Per ampliare il modello user di ASP.NET Identity è sufficiente estendere la sua classe base e per farlo andiamo a creare la classe “Athlete” perché come detto, l’applicazione gestirà i programmi di allenamento degli atleti registrati. Nella cartella Data del progetto Server creiamo la classe:

public class Athlete : IdentityUser
{
    [Required]
    [StringLength(50, MinimumLength = 3)]
    [PersonalData]
    public string FirstName { get; set; }

    [Required]
    [StringLength(50, MinimumLength = 3)]
    [PersonalData]
    public string LastName { get; set; }

    // Campo calcolato
    public string Name { get; set; }

    [Required]
    [DataType(DataType.Date)]
    [PersonalData]
    public DateTime Birthday { get; set; }

    [Required]
    [PersonalData]
    public char Sex { get; set; }
}

Il codice è molto esplicativo per chi conosce la Data Annotation e forse l’unica annotazione che merita una spiegazione è quella identificata da [PersonalData], con la quale andiamo a dire ad ASP.NET Identity che quel campo è personale e quindi va gestito secondo le specifiche richieste dalla normativa sulla privacy. Come probabilmente saprete, in Europa vige il GDPR che tra le sue regole impone di rendere disponibili all’utente tutti i suoi dati personali e il diritto all’oblio, dargli cioè la possibilità di eliminare tutti i dati salvati sul sistema. Gli automatismi di ASP.NET Identity ci permettono di rispettare entrambe le regole, il nostro compito è contrassegnare con questo attributo quali siano i dati personali.

Dopo questo breve excursus burocratico, torniamo al nostro codice. Creato il modello, andiamo ad aggiornare la classe ApplicationDbContext che diventa così:

public class ApplicationDbContext : IdentityDbContext<Athlete>
{
    public ApplicationDbContext(DbContextOptions options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity<Athlete>()
            .Property(p => p.Name)
            .HasComputedColumnSql("[LastName] + ' ' + [FirstName]");
    }
}

Nella prima riga abbiamo specificato il modello da usare, Athlete appunto, mentre nel metodo OnModelCreating abbiamo definito il campo calcolato Name. In questo modo il campo Name è semplicemente la concatenazione delle due proprietà LastName e FirstName. Per finire, andiamo ad aggiornare il file Startup.cs specificando che il modello è Athlete e non quello di default:

services.AddDefaultIdentity<Athlete>(options => {
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequireDigit = false;
    options.Password.RequiredLength = 8;
})

Aggiorniamo il database aggiungendo la migrazione con il comando Add-Migration AthleteModel e lanciando il comando Update-Database:

Aggiornamento del database con Entity Framework

Modelli per la registrazione

Dopo aver creato il contenitore dove persistere i dati è necessario creare i due modelli che si occuperanno rispettivamente della richiesta di registrazione e della relativa risposta, che andremo a creare nel progetto “Shared” perché saranno condivisi tra la parte frontend e backend. Per tenere ordinato il codice ho creato le cartelle Models > Account e al proprio interno ho inserito le 2 classi descritte: RegisterRequest e RegisterResponse:

La cartella Models > Account

Ecco il contenuto di RegisterRequest.cs:

public class RegisterRequest
{
    [Required]
    [StringLength(50)]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [StringLength(25)]
    public string FirstName { get; set; }

    [Required]
    [StringLength(25)]
    public string LastName { get; set; }

    [Required]
    public DateTime? Birthday { get; set; }

    [Required]
    public int? Sex { get; set; }

    [Required]
    [StringLength(16, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 8)]
    public string Password { get; set; }

    [Required]
    [Compare(nameof(Password), ErrorMessage = "Password mismatch")]
    public string ConfirmPassword { get; set; }
}

In questo modello ci sono tutti dati necessari alla registrazione e la sua particolarità è che sono tutte proprietà nullable. Alla richiesta di registrazione restituiremo il modello RegisterResponse, identificato nella classe il cui file è RegisterResponse.cs e il cui contenuto è questo:

public class RegisterResponse
{
    public bool IsSuccess { get; set; }
    public IEnumerable<string> Errors { get; set; }
}

Layout e Page

E’ giunto finalmente il momento di scrivere qualche riga di codice nel progetto “Client”, iniziando dal creare il layout che useremo sia per la registrazione che per la login. Viste le mie doti grafiche prossime allo zero oltre a Bootstrap, utilizzerò un comodo pacchetto per ora gratuito chiamato Radzen che offre una moltitudine di componenti per Blazor dall’aspetto professionale e comodi da usare. L’installazione è banale e vi rimando alla pagina specifica (https://www.radzen.com/) presente sul sito di Radzen per vedere come fare.

Creiamo nel progetto “Shared” un nuovo component Razor denominato AuthLayout.razor, il cui contenuto sarà semplicemente questo:

@inherits LayoutComponentBase
<div class="middle-box">
    @Body
</div>

Adesso è necessario creare la pagina di registrazione, per farlo aggiungiamo un componente Razor dentro Pages -> Auth denominato Register.razor, il cui contenuto è il seguente:

@page "/account/register"
@layout AuthLayout
@inject HttpClient Http
@using CompTrain.Shared.Models.Account;

<div class="row">
    <div class="col-12">
        <h2>Create new account</h2>
        <EditForm Model="model" OnValidSubmit="RegisterUser">
            <RadzenCard>
                <label>First Name</label>
                <RadzenTextBox @bind-Value="model.FirstName"></RadzenTextBox>

                <label>Last Name</label>
                <RadzenTextBox @bind-Value="model.LastName"></RadzenTextBox>

                <label>Birthday</label>
                <RadzenDatePicker @bind-Value="model.Birthday" DateFormat="dd/MM/yyyy" />

                <label>Sex</label>
                <RadzenRadioButtonList @bind-Value="model.Sex" TValue="int?">
                    <Items>
                        <RadzenRadioButtonListItem Text="Man" Value="1" TValue="int?" />
                        <RadzenRadioButtonListItem Text="Woman" Value="2" TValue="int?" />
                    </Items>
                </RadzenRadioButtonList>


                <label>Email</label>
                <RadzenTextBox @bind-Value="model.Email"></RadzenTextBox>

                <label>Password</label>
                <RadzenPassword @bind-Value="model.Password"></RadzenPassword>

                <label>Confirm Password</label>
                <RadzenPassword @bind-Value="model.ConfirmPassword"></RadzenPassword>

                <hr />
                <DataAnnotationsValidator />
                <ValidationSummary />


                <Alert Title="Attenzione" ErrorList="errorMessages" />


                <RadzenButton ButtonType="Radzen.ButtonType.Submit" Text="Register" ButtonStyle="Radzen.ButtonStyle.Primary" class="btn-block" mr-2></RadzenButton>

            </RadzenCard>
        </EditForm>
    </div>
</div>
@code {
    RegisterRequest model = new RegisterRequest();
    RegisterResponse registerResponse = null;
    IEnumerable<string> errorMessages = null;

    public async Task RegisterUser()
    {

        registerResponse = await Http.PostJsonAsync<RegisterResponse>("api/accounts/register", model);
        if (!registerResponse.IsSuccess)
        {
            errorMessages = registerResponse.Errors;
        }
    }

}

Da notare il binding con il modello RegisterRequest e l’utilizzo del componente denominato Alert, che andiamo subito a creare dentro Shared:

@if (IsVisible)
{
    <div class="alert alert-@CSSClass" role="alert">
        <h4 class="alert-heading">@Title</h4>
        <ul>
            @foreach (string error in ErrorList)
            {
                <li>@error</li>
            }
        </ul>
    </div>
}
@code {
    private bool IsVisible = false;

    [Parameter]
    public string Title { get; set; }

    [Parameter]
    public IEnumerable<string> ErrorList { get; set; }

    [Parameter]
    public string CSSClass { get; set; } = "warning";

    protected override void OnParametersSet()
    {
        IsVisible = !String.IsNullOrEmpty(Title) &&  (ErrorList != null && ErrorList.Count() > 0);
    }
}

In pratica questo component mostra a video l’alert di Bootstrap prendendo come parametri d’ingresso un titolo, una lista di errore e la classe CSS per dare colore e risalto all’avviso.

Prima di eseguire il progetto, un piccolo accorgimento grafico nel CSS. Preferisco creare un file CSS chiamato custom.css dove metto tutte le definizioni aggiunte da me per non intaccare i file CSS già presenti. Creiamo il file custom.css nella cartella wwwroot > css, e al suo interno inseriamo:

.middle-box {
    margin: auto;
    margin-top: 60px;
    width: 500px;
    padding: 30px;
    box-shadow: 5px 10px 18px #888888;
    border-radius: 20px;
}

Non dimenticate di aggiungere il link del CSS appena creato al file index.html presente dentro wwwroot (<link href="css/custom.css" rel="stylesheet" />).

Ci siamo, possiamo utilizzare il tasto play per lanciare nostra app! All’apertura del browser scriviamo nella barra degli indirizzi l’URL https://localhost:44322/account/register, che mostrerà il risultato del nostro lavoro:

La pagina di registrazione in Blazor

Nel prossimo articolo andremo a definire il controller che si occuperà della creazione dell’utente usando ASP.NET Identity, completando il processo di registrazione.