Scadenza della password in ASP.NET Identity

di Marco De Sanctis, in ASP.NET Identity,

Sottoporre periodicamente a scadenza una password è una delle esigenze più comuni, soprattutto in ambito enterprise. Si tratta di una funzionalità che non è disponibile direttamente in ASP.NET Identity, ma che, vista la versatilità di questo framework, è comunque possibile implementare.

Gestione dell'infrastruttura di storage
Come prima cosa, dobbiamo predisporre l'infrastruttura di storage e, nella pratica, aggiungere un campo al profilo utente, all'interno del quale memorizzeremo la data di ultimo cambio password:

public class ApplicationUser : IdentityUser
{
  // altro codice...

  public DateTime PasswordChangeDateUtc { get; set; }
}

Alla creazione utente, dobbiamo memorizzare la data corrente. Pertanto dobbiamo effettuare l'override del metodo CreateAsync su ApplicationUserManager:

public override async Task<IdentityResult> CreateAsync(ApplicationUser user)
{
  user.PasswordChangeDateUtc = DateTime.UtcNow;
  return await base.CreateAsync(user);
}

In maniera del tutto analoga, dobbiamo aggiornare questo dato anche in caso di cambio password o di reset:

public override async Task<IdentityResult> ChangePasswordAsync(
  string userId, string currentPassword, string newPassword)
{
  var result = await base.ChangePasswordAsync(
    userId, currentPassword, newPassword);

  if (result.Succeeded)
  {
    await this.Store.StorePasswordChangedAsync(userId);
  }

  return result;
}

public override async Task<IdentityResult> ResetPasswordAsync(
  string userId, string token, string newPassword)
{
  var result = await base.ResetPasswordAsync(userId, token, newPassword);

  if (result.Succeeded)
  {
    await this.Store.StorePasswordChangedAsync(userId);
  }

  return result;
}

Il metodo StorePasswordChangedAsync che abbiamo usato nel codice precedente, è un extension method di IUserStore e non fa altro che aggiornare il timestamp con la data odierna:

internal static async Task StorePasswordChangedAsync(
    this IUserStore<ApplicationUser, string> store, string userId)
{
  if (string.IsNullOrWhiteSpace(userId))
    throw new ArgumentNullException("userId");

  var user = await store.FindByIdAsync(userId);

  if (user == null)
    return;

  user.PasswordChangeDateUtc = DateTime.UtcNow;

  await store.UpdateAsync(user);
}

Verifica in fase di login
A questo punto, possiamo spostare la nostra attenzione alla fase di login, e gestire questo campo nell'ApplicationSignInManager. In particolare, è molto comodo intanto implementare un metodo per verificare l'avvenuta scadenza della password:

public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
{
  public async Task<bool> CheckPasswordExpiredAsync(
    string userName, string password)
  {
    var user = await this.UserManager.FindByNameAsync(userName);

    if (user == null)
      return false;

    if (!(await this.UserManager.CheckPasswordAsync(user, password)))
      return false;

    return (DateTime.UtcNow - user.PasswordChangeDateUtc) > 
      TimeSpan.FromDays(30);
  }

  // ... altro codice qui ...
]

La nostra implementazione è molto semplice, e come prima cosa recupera i dati dell'utente, verificando che la password sia corretta. Solo in questo caso possiamo controllare se quest'ultima non sia scaduta, nel nostro caso confrontandola con un timeout fisso di 30 giorni. Ovviamente in un caso reale questo sarà un dato configurabile, così che possiamo cambiarlo a piacimento.

Questo metodo può essere utilizzato per gestire la validazione dei dati di login, all'interno del metodo PasswordSignInAsync:

public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
{
  // ... altro codice qui ...

  public async override Task<SignInStatus> PasswordSignInAsync(
    string userName, string password, bool isPersistent, bool shouldLockout)
  {
    if (await this.CheckPasswordExpiredAsync(userName, password))
    {
      return SignInStatus.LockedOut;
    }

    return await base.PasswordSignInAsync(
      userName, password, isPersistent, shouldLockout);
  }
}

Un aspetto importante da sottolineare è che la verifica della scadenza venga effettuata sempre, prima ancora di richiamare l'implementazione base di PasswordSignInAsync. La ragione di questo approccio risiede nel fatto che questo metodo, in caso di password corretta, imposta già il cookie di Login, autenticando di fatto l'utente, mentre noi non vogliamo che questa operazione avvenga nel caso in cui la password sia scaduta.

In questa situazione, allora, ci limitiamo a restituire un SignInStatus di LockedOut, che poi dobbiamo gestire all'intero di AccountController, in fase di Login.

public class AccountController : Controller
{
  public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
  {
    var result = await SignInManager.PasswordSignInAsync(
      model.Email, model.Password, model.RememberMe, shouldLockout: false);
    
    switch (result)
    {
      case SignInStatus.Success:
        return RedirectToLocal(returnUrl);
      case SignInStatus.LockedOut:
        if (await this.SignInManager
            .CheckPasswordExpiredAsync(model.Email, model.Password))
        {
          var user = await this.UserManager.FindByNameAsync(model.Email);
          var resetCode = await this.UserManager
            .GeneratePasswordResetTokenAsync(user.Id);

          return this.RedirectToAction("ResetPassword", 
            "Account", new { userId = user.Id, code = resetCode });
        }

        return View("Lockout");
      case SignInStatus.RequiresVerification:
        return RedirectToAction("SendCode", 
          new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
      case SignInStatus.Failure:
      default:
        ModelState.AddModelError("", "Invalid login attempt.");
        return View(model);
    }
  }
}

Questo metodo è assolutamente analogo a quello del template standard di Visual Studio, ma aggiunge della logica alla gestione del risultato di LockedOut. Nel caso infatti la password sia scaduta, viene generato un ResetToken e invocata la funzionalità di ResetPassword. Come possiamo notare, il tutto avviene senza che si effettui alcuna Login, costringendo di fatto l'utente a cambiare la password scaduta per poter operare sul nostro sito.

Conclusioni
Al di là di una serie di funzionalità che erano comunque assenti nel vecchio MembershipProvider, la caratteristica più importante di ASP.NET Identity è la sua espandibilità. In questo script, per esempio, ci siamo posti l'obiettivo di gestire un requisito che non è previsto nel framework di sicurezza, come la scadenza della password utente.

L'implementazione ha richiesto alcune modifiche sia in fase di storage che sul flusso di login, ma siamo comunque riusciti a inserire la logica nella pipeline di ASP.NET Identity e a sfruttare gran parte dei blocchi già esistenti, come la verifica della credenziali e il reset della password.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi