Nel corso di uno dei precedenti script (https://www.aspitalia.com/script/1184/Scadenza-Password-ASP.NET-Identity.aspx) abbiamo personalizzato ASP.NET Identity per sottoporre a scadenza periodica la password utente. Tipicamente, a questo requisito si accompagna quello di vietare il riutilizzo di una delle password precedenti. Vediamo come possiamo supportare anche questo scenario.
Identity model
Anche in questo caso, dobbiamo modificare le classi usate dallo store per memorizzare i dati degli utenti, introducendo uno storico delle password utilizzate:
public class ApplicationUser : IdentityUser { public virtual ICollection<PasswordHistoryEntry> PasswordHistory { get; set; } // altro codice qui } public class PasswordHistoryEntry { public int Id { get; set; } public ApplicationUser User { get; set; } public string Hash { get; set; } public DateTime ChangeDate { get; set; } }
Ovviamente, visto che non è corretto tenere traccia delle password in chiaro, l'oggetto PasswordHistoryEntry conterrà solo l'hash di quelle impostate dall'utente, unitamente alla data in cui è stata fatta la modifica, così che possiamo eventualmente supportare policy meno restrittive (es. non riutilizzare le ultime 5 password).
Modifiche su ApplicationUserManager
Ora che il nostro strato di storage è allineato al nuovo requisito, possiamo finalmente modificare la classe ApplicationUserManager, effettuando l'override dei metodi CreateAsync, ChangePasswordAsync e ResetPasswordAsync per introdurre la verifica sulla password inserita dall'utente:
public override async Task<IdentityResult> ChangePasswordAsync(string userId, string currentPassword, string newPassword) { if (await this.CheckPasswordAlreadyUsedAsync(userId, newPassword)) { return new IdentityResult( "Password già utilizzata in passato, sceglierne un'altra"); } var result = await base.ChangePasswordAsync(userId, currentPassword, newPassword); if (result.Succeeded) { await this.Store.StorePasswordChangedAsync(userId); } return result; } public override async Task<IdentityResult> CreateAsync(ApplicationUser user) { var result = await base.CreateAsync(user); if (result.Succeeded) { await this.Store.StorePasswordChangedAsync(user.Id); } return result; }
Il primo passo è quello di invocare il metodo CheckPasswordAlreadyUsedAsync (lo vedremo nel dettaglio tra un attimo), che effettua la verifica di corrispondenza con lo storico password dell'utente. Nel caso questa abbia successo, possiamo procedere alla modifica e, successivamente, ad aggiornare lo storico delle password con il metodo StorePasswordChangedAsync.
Lo stesso metodo viene invocato dal metodo CreateAsync, in maniera del tutto analoga, per memorizzare la prima password scelta dall'utente in fase di registrazione.
CheckPasswordAlreadyUsedAsync è molto semplice, visto che si limita a verificare se la password proposta rientri già nell'history dell'utente:
public async Task<bool> CheckPasswordAlreadyUsedAsync( string userId, string password) { var user = await this.Store.FindByIdAsync(userId); if (user == null) return false; return user.PasswordHistory .OrderByDescending(x => x.ChangeDate) .Take(5) .ToList() .Any(x => this.PasswordHasher.VerifyHashedPassword(x.Hash, password) != PasswordVerificationResult.Failed); }
Nell'esempio in alto, abbiamo preso in considerazione le ultime 5 password utilizzate, ma ovviamente possiamo modificare a piacimento questo vincolo, configurarlo o addirittura rimuoverlo, secondo le esigenze. Una nota importante riguarda il fatto che non controlliamo direttamente l'hash della password, ma sfruttiamo il metodo VerifyHashedPassword del password hasher, così siamo sicuri che stiamo controllandone l'uguaglianza in maniera coerente con l'algoritmo di hashing utilizzato.
L'ultimo passaggio riguarda la memorizzazione della password nello storico. Viene effettuata dall'extension method StorePasswordChangedAsync che abbiamo già introdotto nello script precedente:
internal static async Task StorePasswordChangedAsync( this IUserStore<ApplicationUser, string> store, string userId) { if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException("userId"); var passwordStore = (IUserPasswordStore<ApplicationUser, string>)store; var user = await store.FindByIdAsync(userId); if (user == null) return; var changeDate = DateTime.UtcNow; var passwordHash = await passwordStore.GetPasswordHashAsync(user); user.PasswordHistory.Add(new PasswordHistoryEntry() { User = user, Hash = passwordHash, ChangeDate = changeDate }); await store.UpdateAsync(user); }
Anche qui, la logica è molto semplice: viene intanto fatto un tentativo di recupero dell'utente, che in caso di mancato successo non provoca alcun errore per non svelare dettagli del nostro sistema di security. In seguito, non facciamo altro che inserire una nuova entry all'interno dell'history, contenente l'hash della password dell'utente, che recuperiamo tramite il metodo GetPasswordHashAsync. L'ultima riga esegue l'aggiornamento sullo storage, così che i nuovi dati vengano persistiti.
Commenti
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Short-circuiting della Pipeline in ASP.NET Core
Usare le collection expression per inizializzare una lista di oggetti in C#
Miglioramenti nell'accessibilità con Angular CDK
Gestire errori funzionali tramite exception in ASP.NET Core Web API
Eseguire attività basate su eventi con Azure Container Jobs
Load test di ASP.NET Core con k6
Generare file per il download da Blazor WebAssembly
Code scanning e advanced security con Azure DevOps
Registrare servizi multipli tramite chiavi in ASP.NET Core 8
Determinare lo stato di un pod in Kubernetes
Configurare policy CORS in Azure Container Apps
Implementare il throttling in ASP.NET Core