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
Come EF 8 ha ottimizzato le query che usano il metodo Contains
Modificare i metadati nell'head dell'HTML di una Blazor Web App
Gestione degli stili CSS con le regole @layer
Ordinare randomicamente una lista in C#
Come migrare da una form non tipizzata a una form tipizzata in Angular
Supporto ai tipi DateOnly e TimeOnly in Entity Framework Core
Effettuare il binding di date in Blazor
Definire stili a livello di libreria in Angular
Eseguire query manipolando le liste contenute in un oggetto mappato verso una colonna JSON
Gestire il colore CSS con HWB
Creare gruppi di client per Event Grid MQTT
Evitare la script injection nelle GitHub Actions