testa
 
 
 
Internet ASP.NET MVC - Rendere sicure le applicazioni ASP.NET MVC con ASP.NET Identity
cornice di chiusura
 
Articolo orginale su CodeGuru
ASP.NET Identity è un nuovo sistema di autenticazione che ha lo scopo di sostituire l'attuale sistema di membership di ASP.NET.
ASP.NET Identity è una libreria basata su OWIN. I Progetti Template di Visual Studio permettono di utilizzare ASP.NET Identity per rendere sicure le applicazioni web create. Quando viene creato un nuovo progetto MVC utilizzare l'autenticazione "Individual User Accounts" che sta ad indicare che le informazioni degli utenti vengono archiviate nel database dell'Applicazione (e quindi l'utente non deve utilizzare sistemi di login esterni).

Se si crea un progetto MVC con queste impostazioni (impostazioni di default), si può notare che il template aggiunge un Account Controller e le rispettive ed associate Views per la registrazione dei nuovi utenti e della loro autenticazione. Si possono inoltre notare referenze ad alcuni assiemi OWIN che vengono aggiunti al progetto nella classe iniziale.

In questa guida verrà mostrato come implementare ASP.NET Identity in un progetto MVC vuoto partendo da zero.

Prima di partire con lo sviluppo di una web application di esempio è importante prendere familiarità con le parti di ASP.NET Identity; conoscendo queste parti, sarà possibile utilizzarle correttamente nelle proprie applicazioni.

Ci sono 6 parti importanti del sistema ASP.NET Identity su cui si concentrano i local user:
  • USER
  • ROLE
  • USER MANAGER
  • ROLE MANAGER
  • AUTHENTICATION MANAGER
  • ENTITY FRAMEWORK DbCONTEXT
Un USER rappresenta un utente del sistema. I dettagli basilari dell'autenticazione come UserID e Password, assieme alle informazioni del profilo che creano un utente di tipo USER. ASP.NET Identity viene rilasciato con la classe IdentityUser che racchiude le informazioni basilari di autenticazione.
Se si necessita di avere altre informazioni di proflo, si può creare una classe personalizzata che eredita dalla classe IdentityUser. Questa classe è analoga alla classe MembershipUser del sitema ASP.NET basato sui membri.

Un ROLE rappresenta un ruolo dell'utente. Come minimo un ruolo ha un nome per mezzo del quale è riconosciuto dal sistema. La classe IdentityRole di ASP.NET Identity fornisce questo ruolo base. Se si vogliono aggiungere altri elementi al ruolo (ad esempio una descrizione) si può creare una classe personalizzata che eredita dalla classe IdentityRole.

Un USER MANAGER è una classe che permette di gestire gli utenti. Creare, cancellare user accounts, modificare la password, aggiungere/rimuovere utenti da un ruolo e numerosi altri task possono essere eseguiti con un UserManager. ASP.NET Identity viene fornito con la classe UserManager che può essere utilizzata per questo scopo. Questa classe è simile all'oggetto intrinseco Membership del sistema ASP.NET Membership.

Un ROLE MANAGER è una classe che permette di gestire i ruoli. Creare un nuovo ruolo, cancellare un ruolo, verificare se un ruolo già esiste nel sistema e molti altri task possono essere compiuti mediante questa classe. Questa classe è simile all'oggetto intrinseco Roles del sistema ASP.NET Membership.

Tutte le classi menzionate in precedenza interagiscono rispettivamente con utenti e ruoli. Queste classi, da sole, non effettuano alcuna autenticazione. Autenticare un utente - login e logout - è una responsabilità dell'AuthenticationManager. L'autenticazione di utenti locali si basa sull'utilizzo di cookies, come nella Autenticazione di Form. ASP.NET Identity fornisce l'intefaccia IAuthenticationManager che rappresenta l'Autentication Manager. L'Autentication Manager è simile alla classe di ASP.NET dei FormAuthentication.

Un aspetto molto importante di ASP.NET Identity è che lo schema delle tabelle del database non sono fissate rigidamente (come nel sistema ASP.NET Membership); ma mediante l'utilizzo dell'approccio Entity Framework Code First, si possono apportare modifiche agli oggeti User e Role. Questo significa che per ogni proprietà dell'User, viene creata una specifica colonna nel database.

Come si può vedere dall'immagine, tutte le tabelle che iniziano per "AspNet" sono state create da ASP.NET Identity. Notare che la tabella AspNetUsers, che raccoglie le informazioni dell'utente, contiene anche informazioni riguardanti il profilo come FullName, BirthDate e Bio in colonne separate.
Di default viene creato un database separato nella cartella App_Data contenente tutte le tabelle descritte in precedenza. Comunque è possibile utilizzare un database personalizzato per archiviare queste informazioni. Se si decide di percorrere questa strada, le tabelle verranno create nel database definito. Per poter utilizzare questi dati, si può creare un DbContext personalizzato che erediti da IdentityDbContext.
Utilizzo di ASP.NET Identity

  • Ottenere i pacchetti NuGet necessari
  • Creare un DbContext personalizzato, User e Role
  • Aggiungere una classe OWIN Startup
  • Creare una classe View Model
  • Creare un AccountController
  • Creare la sezione di registrazione (Register, Login, ChangePassword e PasswordProfile) e le relative View
  • Creare un HomeController
  • Creare la View Index
  • Creare i Roles nel System
Ottenere i pacchetti NuGet necessari

Il primo passaggio è quello di ottenere i pacchetti necessari. Dalla cartella delle Referenze, tasto DX del mouse - Manage NuGet Packages, cercare ed installare:
  • "ASP.NET Identity"
  • "Microsoft.AspNet.Identity.EntityFramework" Questo serve per le IdentityUser e IdentityRole
Creare un DbContext personalizzato, User e Role

Il prossimo passaggio consiste nel creare tre classi POCO che rappresentano un DbContext, un User e un Role personalizzato:

public class MyIdentityDbContext : IdentityDbContext<MyIdentityUser>
{
   public MyIdentityDbContext() : base("connectionstring")
   {
   }
}

public class MyIdentityUser : IdentityUser
{
   public string FullName { get; set; }
   public DateTime BirthDate { get; set; }
   public string Bio { get; set; }
}

public class MyIdentityRole : IdentityRole
{
   public MyIdentityRole() { }

   public MyIdentityRole(string roleName, string description) : base(roleName)
   {
      this.Description = description;    }
   public string Description { get; set; }
}

La classe MyIdentityDbContext eredita dalla classe base IdentityDbContext e specifica un tipo generico di MyIdentityUser. In questo modo il sistema è in grado di determinare lo schema da applicare alle tabelle del database. Da notare che il costruttore della classe MyIdentityDbContext passa una stringa di connessione - connectionstring - alla classe base: questa connectionstring viene definita nel file web.config

<connectionStrings>
   <add name="connectionstring" connectionstring="data source=.;initial catalog=Northwind;integrated security=true" providerName="System.Data.SqlClient" />
</connectionStrings>

Da notare che la connessione mostrata specifica come database il classico Northwind, quindi tutte le tabelle necessarie ad ASP.NET Identity verranno create dentro questo DataBase.

La classe MyIdentityUser eredita dalla classe base IdentityUser ed aggiunge tre proprietà: FullName, BirthDate e Bio. Queste proprietà vanno a completare le informazioni sul profilo dell'utente. La classe base IdentityUser fornisce numerose proprietà come UserName, Email e PasswordHash.

La classe MyIdentityRole eredita dalla classe base IdentityRole e la estende con la proprietà Description.

Aggiungere la classe OWIN Startup

Ora è necessario aggiungere una classe OWIN nella cartella App_Start utilizzando il template OWIN Startup Class.
Modificare il nome della classe in Startup.cs e scrivere il seguente codice:

using Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.AspNet.Identity;

[assembly: OwinStartup(typeof(AspNetIdentityDemo.App_Start.Startup))]

namespace AspNetIdentityDemo.App_Start
{
   public class Startup    {
      public void Configuration(IAppBuilder app)       {
         CookieAutheticationOptions options = new CookieAutheticationOptions();          options.AuthenticationType = DefaultAuthenticationType.ApplicationCookies;          options.LoginPath = new PathString("/account/login");          app.UseCookieAuthentication(options);       }
   }
}

Assicurarsi che l'attributo relativo all'Assembly sia posto prima del namespace perché specifica la classe OWIN di Startup. La classe di Startup è formata da un metodo Configuration() che riceve un parametro di tipo IAppBuilder e questo metodo configura gli schemi di autenticazione (in questo caso l'autenticazione basata sull'utilizzo di cookie e la pagina di login dell'applicazione).

Creazione di una classe View Model

Ora si deve creare le classi necessarie per il controller Account.
Tutto ciò di cui abbiamo bisogno sono 4 classi: Register, Login, ChangePassword e ChangeProfile. Queste classi sono oggetti POCO con alcune data annotation:

public class Register
{
   [Required]    public string UserName { get; set; }
   [Required]    public string Password { get; set; }
   [Required]    [Compare("Password", ErrorMessage="Le due password non corrispondono"]    public string ConfirmPassword { get; set; }
   [Required]    [EmailAddress]    public string Email { get; set; }
   public string FullName { get; set; }
   public string BirthDate { get; set; }
   public string Bio { get; set; }
}

public class Login
{
   [Required]    [Display(Name="User Name")]    public string UserName { get; set; }
   [Required]    [DataType(DataType.Password)]    [Display(Name="Password")]    public string Password { get; set; }
   [Display(Name="Remember me?")]    public string RememberMe { get; set; }
}

public class ChangePassword
{
   [Required]    public string OldPassword { get; set; }
   [Required]    [StringLength(40, MinimumLength=6, ErrorMessage="Password deve essere compresa tra 6 e 40 caratteri")]    [Display(Name="Nuova Password")]    public string NewPassword { get; set; }
   [Compare("NewPassword", ErrorMessage="Le due password non corrispondono"]    public string ConfirmNewPassword { get; set; }
}

public class ChangeProfile
{
   public string FullName { get; set; }
   public string BirthDate { get; set; }
   public string Bio { get; set; }
}

Creare queste classi ed inserirle nella cartella Models.

Creazione Account Controller

È in questo controller che si sviluppa tutta la magia della sicurezza!
L'Account controller contiene 9 Action Methods e un Costruttore. Di seguito riporto i nomi e gli scopi di tutti questi metodi:

Metodo Descrizione
AccountController() Costruttore. Inizializza UserManager e RoleManager
Register() ActionMethod. Ritorna la vista Register
Register(Register model) Questa versione del metodo Register() viene chiamata quando l'utente invia la sottoscrizione e viene avviata la procedura di creazione dello user
Login() ActionMethod. Ritorna la vista Login
Login(Login model) Questa versione del metodo Login() viene chiamata quando l'utente invia la sottoscrizione e viene avviata la procedura di autenticazione dello user
ChangePassword() ActionMethod. Ritorna la vista di ChangePassword
ChangePassword(ChangePassword model) Questa versione del metodo ChangePassword() viene chiamata quando l'utente invia il Cambio di Password e viene avviata la procedura di modifica della password dello user
ChangeProfile() ActionMethod. Ritorna la vista di ChangeProfile
ChangeProfile(ChangeProfile model) Questa versione del metodo ChangeProfile() viene chiamata quando l'utente invia il Cambio di Profilo e viene avviata la procedura di modifica del profilo dello user
Logout() Il metodo di Logout viene invocato quando un utente loggato preme sul pulsante Logout (da qualunauq View possa essere presente) e viene avviata la procedura di rimozione del cookie di autenticazione.

Ora andiamo ad implementare questi metodi uno-per-uno.
Aggiungere la classe AccountController nella cartella Controllers e scrivere il costruttore:

private UserManager<MyIdentityUser> userManager;
private RoleManager<MyIdentityRole> roleManager;

public AccountController()
{
   MyIdentityDbContext db = new MyIdentityDbContext();
   UserStore<MyIdentityUser> userStore = new UserStore<MyIdentityUser>(db);
   userManager = new UserManager<MyIdentityUser>(userStore);
   RoleStore<MyIdentityRole> roleStore = new RoleStore<MyIdentityRole>(db);
   roleManager = new RoleManager<MyIdentityRole>(roleStore);
}

Il codice precedente dichiara due variabili all'interno della classe AccountController - una di tipo UserManager ed una di tipo RoleManager. Mentre vengono dichiarate queste variabili, vengono specificati i tipi generici di MyIdentityUser e MyIdentityRole.

Il costruttore della classe AccountController crea un'istanza del DbContext personalizzato e lo passa al costruttore delle classi UserStore e RoleStore: queste istanze vengono passate successivamente al costruttore delle classi UserManager e RoleManager. Le classi UserStore e RoleStore fondamentalmente si occupano di archiviare e recuperare i dati dal database.

public ActionResult Register()
{
   return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Register(Register model)
{
   if (ModelState.IsValid)
   {
      MyIdentityUser user = new MyIdentityUser();
      user.UserName = model.UserName;
      user.Email = model.Email;
      user.FullName = model.FullName;
      user.BirthDate = model.BirthDate;
      user.Bio = model.Bio;
      IdentityResult result = userManager.Create(user, model.Password);

      if (result.Succeeded)
      {
         userManager.AddToRole(user.Id, "Administrator");
         return RedirectToAction("Login","Account");
      }
      else
      {
         ModelState.AddModelError("UserName", "Error while creating the user!");
      }
   }

   return View(model);
}

La versione POST del metodo Register() crea un'istanza della classe MyIdentityUser e setta le sue proprietà alle corrispondenti proprietà della classe Modello Register. Dopodiché viene creato un nuovo User Account chiamando il metodo Create() dall'oggetto UserManager.
Se IdentityResult.Succeded ritorna TRUE, indica che è stato correttamente creato un User Account, in questo modo il nuovo utente appena creato viene assegnato il ruolo di Amministratore (in una situazione più reale, si dovrà separare la gestione dei ruoli dell'utente in una pagina diversa, per assegnare un nuovo ruolo all'utente, qui per maggiore semplicità, viene assegnato un ruolo nel medesimo istante in cui viene creato). L'utente viene quindi reindirizzato alla pagina di Login.

Se viene sollevato qualche errore durante la creazione dell'User, un messaggio di errore viene aggiunto al Dictionary ModelState e la View Register viene nuovamente mostrata con il messaggio di errore.

public ActionResult Login(string returnUrl)
{
   ViewBag.ReturnUrl = returnUrl;
   return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(Login model, string returnUrl)
{
   if (ModelState.IsValid)
   {
      MyIdentityUser user = userManager.Find(model.UserName, model.Password);
      if (user != null)
      {
         IAuthenticationManager authMgr = HttpContext.GetOwinContext().Authentication;
         authMgr.SignOut(DefaultAuthenticationType.ExternalCookie);
         ClaimsIdentity identity = userMgr.CreateIdentity(user, DefaultAuthenticationType.ApplicationCookie);
         AuthenticationProperties props = new AuthenticationProperties();
         props.IsPersistent = model.RememberMe;
         authMgr.SignIn(props, identity);

         if (Url.IsLocalUrl(returnUrl))
            return Redirect(returnUrl);
         else
            return RedirectToAction("Index", "Home");
      }
      else
         ModelState.AddModelError("", "UserName o Password non validi");
   }

   return View(model);
}

Se un utente tenta di effettuare l'accesso senza loggarsi, viene automaticamente re-indirizzato alla pagina di Login. Nel fare questa operazione, il sistema passa come parametro l'url della pagina richiesta come parametro stringa. Questo parametro viene recuperato dal metodo Login(). Il primo metodo Login() semplicemente passa il parametro returnUrl alla View perché possa essere reinviato al termine dell'operazione di Login.

Il secondo metodo Login() effettua la validazione di UserName e Password se sono corretti: questa procedura è fatta effettuando la ricerca di un utente con specifico UserName e Password utilizzando l'oggetto UserManager. Se viene trovato un utente con i parametri corretti, la procedura riceve un AuthenticationManager chiamando HttpContext.GetOwinContext().Authentication ed archivia in una variabile di tipo IAuthenticationManager. L'utente si può sloggare chiamando il metodo SignOut().
Infine l'oggetto ClaimsIdentity viene creato chiamando il metodo CreateIdentity() dall'UserManager. Il metodo SignIn() accetta questo ClaimsIdentity ed effettua le operazioni di autenticazione. L'oggetto AuthenticationProperties specifica la proprietà IsPersistent per indicare se i cookie di autenticazione devono essere persistenti(true) o no(false).

[Authorize]
public ActionResult ChangePassword()
{
   return View();
}

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult ChangePassword(ChangePassword model)
{
   if (ModelState.IsValid)
   {
      MyIdentityUser user = userManager.FindByName(HttpContext.User.Identity.Name);
      IdentityResult result = userManager.ChangePassword(user.Id, model.OldPassword, model.NewPassword);
      if (result.Succeded)
      {
         IAuthenticationManager authMgr = HttpContext.GetOwinContext().Authentication;
         authMgr.SignOut();
         return RedirectToAction("Login", "Account");
      }
      else
         ModelState.AddModelError("", "Errore nel cambio di Password");
   }

   return View(model);
}

Il secondo metodo ChangePassword() riceve lo UserName corrente mediante HttpContext.User.Identity.Name; grazie a questo valore, il metodo FindByName() di UserManager ritorna il MyIdentityUser associato. Infine il metodo ChangePassword() di UserManager passando l'Id dell'utente, la vecchia password e la nuova password. Se l'operazione viene eseguita con successo, l'utente viene sloggato ed il sistema lo reindirizza alla pagina di Login.

[Authorize]
public ActionResult ChangeProfile()
{
   MyIdentityUser user = userManager.FindByName(HttpContext.User.Identity.Name);
   ChangeProfile model = new ChangeProfile();
   model.FullName = user.FullName;
   model.BirthDate = user.BirthDate;
   model.Bio = user.Bio;
   return View(model);
}

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult ChangeProfile()
{
   if (ModelState.IsValid)
   {
      MyIdentityUser user = userManager.FindByName(HttpContext.User.Identity.Name);
      user.FullName = model.FullName;
      user.BirthDate = model.BirthDate;
      user.Bio = model.Bio;
      IdentityResult result = userManager.Update(user);
      if (result.Succeded)
         ViewBag.Message = "Profilo aggiornato correttamente"
      else
         ModelState.AddModelError("", "Errore nel salvataggio del profilo");
   {
   return View(model);
}

Il primo metodo ChangeProfile() recupera MyIdentityUser mediante il metodo FindByName() di UserManager. I dettagli del profilo dell'utente corrente servono per riempire i campi del Model ChangeProfile e passati alla View ChangeProfile.
Il secondo metodo ChangeProfile() riceve le informazioni modificate e mediante il metodo Update() aggiorna il profilo recuperato mediante il metodo FindByName() di UserManager.

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult LogOut()
{
   IAuthenticationManager authMgr = HttpContext.GetOwinContext().Autentication;;
   authMgr.SignOut();
   return RedirectToAction("Login", "Account");
}

Creare le View Resgiter, Login, ChangePassword e ChangeProfile

Ora si devono creare le 4 View:
  • Register.cshtml
  • Login.cshtml
  • ChangePassword.cshtml
  • ChangeProfile.cshtml
per i corrispondenti metodi dell'AccountController.
Creare il Controller HomeController

Il Controller HomeController è responsabile della sicurezza dei metodi. Aggiungere alla cartella Controller e modificare l'ActionMethod Index() come segue:

[Authorize]
public ActionResult Index()
{
   MyIdentityDbContext db = new MyIdentityDbContext();

   UserStore<MyIdentityUser> userStore = new UserStore<MyIdentityUser>(db);
   UserManager<MyIdentityUser> userManager = new UserManager<MyIdentityUser>(userStore);

   MyIdentityUser user = userManager.FindByName(HttpContext.User.Identity.Name);

   NorthwindEntities northwinDb = new NorthwindEntities();

   List<Customer> model = null;

   if(userManager.IsInRole(user.Id, "Administrator"))
      model = nothwindDb.Customers.ToList();

   if (userManager.IsInRole(user.Id, "Operator"))
      model = nothwindDb.Customers.Where(c => c.Country == "USA").ToList();

   ViewBag.FullName = user.FullName;
   return View(model); }

L'ActionMethod Index() è decorato con l'attributo [Authorize] perché lo si vuole rendere sicuro. All'interno di questo metodo viene creato un MyIdentityDbContext, un UserStore e un UserManager; viene chiamato FindByName() di UserManager per ottenere l'attuale utente loggato. Viene verificato il ruolo mediante il metodo IsInRole() di UserManager. Infine viene rimandato alla View Index e il FullName dell'utente viene inserito nella ViewBag.

Creare la View Index

Creare la View Index che mostri il messaggio all'utente utilizzando la proprietà della ViewBag. Inoltre viene mostrata una tabella contenente l'elenco dei Customers.
Creare i Ruoli nel sistema

Abbiamo visto utilizzare i ruoli Amministratore e Operatore, ma non li abbiamo ancora definiti. Questo viene fatto all'interno dell'evento Application_Start di Global.asax

MyIdentityDbContext db = new MyIdentityDbContext();
RoleStore<MyIdentityRole> roleStore = new RoleStore<MyIdentityUser>(db);
RoleManager<MyIdentityRole> roleManager = new RoleManager<MyIdentityUser>(roleStore);

if (!roleManager.RoleExists("Administrator"))
{
   MyIdentityRole newRole = new MyIdentityRole("Administrator"e;, "Amministratori possono aggiungere, modificare e cancellare dati");
   roleManager.Create(newRole);
}

if (!roleManager.RoleExists("Operator"))
{
   MyIdentityRole newRole = new MyIdentityRole("Operator"e;, "Gli operatori possono solo aggiungere e modificare i dati");
   roleManager.Create(newRole);
}

 
 
 
 
I23 di Boccaletti Emanuele
 
I23 di Boccaletti Emanuele
41121 Modena (MO)
Tel. +39 347 1302420
emanuele@i23.eu
Skype: emanuele.boccaletti
 
 
Copyright © I23 di Boccaletti Emanuele - emanuele@i23.eu