public async Task <IActionResult> IndexPost([EmailAddress, MaxLength(100)] string email_address, [FromBody] IndexPostRequestBody body) { if (!ModelState.IsValid) { return(BadRequest(ModelState)); } // Make sure the alias doesn't already exist. if (await ef.Aliases.AnyAsync(a => a.EmailAddress == email_address)) { return(StatusCode(StatusCodes.Status409Conflict)); } // Create or identify a being. Being being; if (body.password != null) { // A new being should be created. if (body.otherEmailAddress != null) { // Both 'password' and 'otherEmailAddress' cannot be provided. return(BadRequest()); } ef.Beings.Add(being = new Being { SaltedHashedPassword = Sha512Util.SaltAndHashNewPassword(body.password) }); await ef.SaveChangesAsync(); // To populate BeingID } else { // The new alias should be linked to an existing being. if (body.otherEmailAddress == null) { // One of 'password' and 'otherEmailAddress' must be provided. return(BadRequest()); } being = (await ef.Aliases .Include(a => a.Being) .FirstOrDefaultAsync(a => a.EmailAddress == body.otherEmailAddress)) ?.Being; if (being == null) { log.LogWarning($"Invalid attempt to link '{email_address}' to non-existent alias '{body.otherEmailAddress}'"); return(NotFound()); } } // Create the alias. ef.Aliases.Add(new Alias { EmailAddress = email_address, BeingID = being.BeingID }); await ef.SaveChangesAsync(); return(NoContent()); }
public async Task Alias_IndexPatchMatchingOld_NoContentWithChangedPassword() { using (IdentityWsDbContext ef = CreateEf()) { Being being = new Being { SaltedHashedPassword = Sha512Util.SaltAndHashNewPassword("password1") }; ef.Aliases.Add(new Alias { EmailAddress = "*****@*****.**", Being = being }); await ef.SaveChangesAsync(); AliasesController patient = new AliasesController(ef, dummyLog, now, dummyRunner); AliasesController.IndexPatchRequestBody body = new AliasesController.IndexPatchRequestBody { oldPassword = "******", password = "******" }; IActionResult result = await patient.IndexPatch("*****@*****.**", body); result.Should().BeOfType <NoContentResult>($"'{nameof(body.oldPassword)}' matched"); Sha512Util.TestPassword("p@ssword1", being.SaltedHashedPassword).Should().BeTrue("the password should have been changed"); } }
public async Task Alias_IndexPatchExpiredReset_NotAuthorized() { using (IdentityWsDbContext ef = CreateEf()) { ef.Aliases.Add(new Alias { EmailAddress = "*****@*****.**", Being = new Being { PasswordResetToken = "abracadabra", PasswordResetTokenValidUntil = now.UtcNow, SaltedHashedPassword = Sha512Util.SaltAndHashNewPassword("password1") } }); await ef.SaveChangesAsync(); AliasesController patient = new AliasesController(ef, dummyLog, now, dummyRunner); AliasesController.IndexPatchRequestBody body = new AliasesController.IndexPatchRequestBody { resetToken = "abracadabra", password = "******" }; IActionResult result = await patient.IndexPatch("*****@*****.**", body); result.Should().BeOfType <UnauthorizedResult>($"the reset token is expired"); } }
public async Task Alias_IndexPatchOldSameAsNew_Conflict() { using (IdentityWsDbContext ef = CreateEf()) { ef.Aliases.Add(new Alias { EmailAddress = "*****@*****.**", Being = new Being { SaltedHashedPassword = Sha512Util.SaltAndHashNewPassword("password1") } }); await ef.SaveChangesAsync(); AliasesController patient = new AliasesController(ef, dummyLog, now, dummyRunner); AliasesController.IndexPatchRequestBody body = new AliasesController.IndexPatchRequestBody { oldPassword = "******", password = "******" }; IActionResult result = await patient.IndexPatch("*****@*****.**", body); result.Should().BeOfType <StatusCodeResult>() .Which.StatusCode.Should().Be(409, $"'{nameof(body.password)}' must differ from '{nameof(body.oldPassword)}'"); } }
public async Task WrongClient_Login_NotFound() { const string PASSWORD = "******", CLIENT = "testing"; using (IdentityWsDbContext ef = CreateEf()) { Alias alias; ef.Aliases.Add(alias = new Alias { EmailAddress = "*****@*****.**", Being = new Being { SaltedHashedPassword = Sha512Util.SaltAndHashNewPassword(PASSWORD), Clients = new HashSet <BeingClient> { new BeingClient { ClientName = CLIENT } } } }); await ef.SaveChangesAsync(); ClientsController patient = new ClientsController(ef, dummyLog, now, null); IActionResult result = await patient.Login("*****@*****.**", "wrong", new ClientsController.LoginRequestBody { password = PASSWORD }); result.Should().BeOfType <NotFoundResult>("the client does not match"); } }
public async Task Alias_IndexPatchMismatchingOld_NotAuthorized() { using (IdentityWsDbContext ef = CreateEf()) { ef.Aliases.Add(new Alias { EmailAddress = "*****@*****.**", Being = new Being { SaltedHashedPassword = Sha512Util.SaltAndHashNewPassword("password1") } }); await ef.SaveChangesAsync(); AliasesController patient = new AliasesController(ef, dummyLog, now, dummyRunner); AliasesController.IndexPatchRequestBody body = new AliasesController.IndexPatchRequestBody { oldPassword = "******", password = "******" }; IActionResult result = await patient.IndexPatch("*****@*****.**", body); result.Should().BeOfType <UnauthorizedResult>($"'{nameof(body.oldPassword)}' does not match"); } }
public void Password_SaltAndHashNewPasswordTwice_ReturnsDifferentHashes() { const string PASSWORD = "******"; Sha512Util.SaltAndHashNewPassword(PASSWORD).Should().NotBe(Sha512Util.SaltAndHashNewPassword(PASSWORD), "a random salt should be used for each invocation"); }
public async Task RightPassword_Login_NoContentAndDbTrue() { const string PASSWORD = "******", CLIENT = "testing"; using (IdentityWsDbContext ef = CreateEf()) { Alias alias; ef.Aliases.Add(alias = new Alias { EmailAddress = "*****@*****.**", Being = new Being { SaltedHashedPassword = Sha512Util.SaltAndHashNewPassword(PASSWORD), Clients = new HashSet <BeingClient> { new BeingClient { ClientName = CLIENT } } }, LoginAttempts = new List <LoginAttempt> { new LoginAttempt { DateCreated = now.UtcNow.AddMinutes(-7) }, new LoginAttempt { DateCreated = now.UtcNow.AddMinutes(-6) }, new LoginAttempt { DateCreated = now.UtcNow.AddMinutes(-5), // The account is not locked because this success breaks the sequence. Success = true }, new LoginAttempt { DateCreated = now.UtcNow.AddMinutes(-4) } } }); await ef.SaveChangesAsync(); ClientsController patient = new ClientsController(ef, dummyLog, now, config); IActionResult result = await patient.Login("*****@*****.**", CLIENT, new ClientsController.LoginRequestBody { password = PASSWORD }); result.Should().BeOfType <NoContentResult>("the password matches"); (await ef.LoginAttempts.CountAsync()).Should().Be(5, "a new record should be added"); (await ef.LoginAttempts.LastAsync()).Should().Match <LoginAttempt>(a => a.Success && a.ClientName == CLIENT, "such a client was supplied"); } }
public async Task <IActionResult> Login([EmailAddress, MaxLength(100)] string email_address, [Required, MaxLength(20)] string client, [FromBody] LoginRequestBody body) { if (!ModelState.IsValid) { return(BadRequest(ModelState)); } // Get the entities. Alias alias = await ef.Aliases .Include(a => a.Being).ThenInclude(b => b.Clients) .FirstOrDefaultAsync(a => a.EmailAddress == email_address); Being being = alias?.Being; if (being == null || !being.Clients.Any(c => c.ClientName == client)) { return(NotFound()); } // Check the number of consecutive failures. DateTime period_start = now.UtcNow.AddMinutes(-1 * config.GetValue <double>("LockoutPeriodMins")); int consecutive_failures = await ef.LoginAttempts .Where(a => a.Alias.BeingID == being.BeingID && a.DateCreated >= period_start && !a.Success && !ef.LoginAttempts.Any(a2 => a2.Alias.BeingID == being.BeingID && a2.LoginAttemptID > a.LoginAttemptID && a2.Success)) .CountAsync(); if (consecutive_failures >= config.GetValue <int>("MaxFailedLoginsBeforeLockout")) { return(StatusCode(StatusCodes.Status503ServiceUnavailable)); } // Check the password. bool password_ok = Sha512Util.TestPassword(body.password, being.SaltedHashedPassword); // Log the attempt. ef.LoginAttempts.Add(new LoginAttempt { AliasID = alias.AliasID, Success = password_ok, ClientName = client }); await ef.SaveChangesAsync(); return(password_ok ? (IActionResult)NoContent() : Unauthorized()); }
public async Task WrongPassword_Login_UnauthorizedAndDbFalse() { const string PASSWORD = "******", CLIENT = "testing"; using (IdentityWsDbContext ef = CreateEf()) { Alias alias; ef.Aliases.Add(alias = new Alias { EmailAddress = "*****@*****.**", Being = new Being { SaltedHashedPassword = Sha512Util.SaltAndHashNewPassword(PASSWORD), Clients = new HashSet <BeingClient> { new BeingClient { ClientName = CLIENT } } }, LoginAttempts = new List <LoginAttempt> { new LoginAttempt { // This is too old to count. DateCreated = now.UtcNow.AddMinutes(-15).AddMilliseconds(-100) }, new LoginAttempt { DateCreated = now.UtcNow.AddMinutes(-5) } } }); await ef.SaveChangesAsync(); ClientsController patient = new ClientsController(ef, dummyLog, now, config); IActionResult result = await patient.Login("*****@*****.**", CLIENT, new ClientsController.LoginRequestBody { password = "******" }); LoginAttempt attempt = await ef.LoginAttempts.FirstAsync(a => a.AliasID == alias.AliasID); result.Should().BeOfType <UnauthorizedResult>("the password doesn't match"); (await ef.LoginAttempts.CountAsync()).Should().Be(3, "a new record should be added"); (await ef.LoginAttempts.LastAsync()).Should().Match <LoginAttempt>(a => !a.Success && a.ClientName == CLIENT, "such a client was supplied"); } }
public async Task RightPasswordLockedOut_Login_ServiceUnavailable() { const string PASSWORD = "******", CLIENT = "testing"; using (IdentityWsDbContext ef = CreateEf()) { Alias alias; ef.Aliases.Add(alias = new Alias { EmailAddress = "*****@*****.**", Being = new Being { SaltedHashedPassword = Sha512Util.SaltAndHashNewPassword(PASSWORD), Clients = new HashSet <BeingClient> { new BeingClient { ClientName = CLIENT } } }, LoginAttempts = new List <LoginAttempt> { new LoginAttempt { DateCreated = now.UtcNow.AddMinutes(-15) }, new LoginAttempt { DateCreated = now.UtcNow.AddMinutes(-4) } } }); await ef.SaveChangesAsync(); ClientsController patient = new ClientsController(ef, dummyLog, now, config); IActionResult result = await patient.Login("*****@*****.**", CLIENT, new ClientsController.LoginRequestBody { password = PASSWORD }); result.Should().BeOfType <StatusCodeResult>() .Which.StatusCode.Should().Be(StatusCodes.Status503ServiceUnavailable, "the being is locked"); (await ef.LoginAttempts.CountAsync()).Should().Be(2, "no new record should be added"); } }
public async Task Alias_IndexPatchNoOldPasswordOrResetToken_BadRequest() { using (IdentityWsDbContext ef = CreateEf()) { ef.Aliases.Add(new Alias { EmailAddress = "*****@*****.**", Being = new Being { SaltedHashedPassword = Sha512Util.SaltAndHashNewPassword("password1") } }); await ef.SaveChangesAsync(); AliasesController patient = new AliasesController(ef, dummyLog, now, dummyRunner); AliasesController.IndexPatchRequestBody body = new AliasesController.IndexPatchRequestBody { password = "******" }; IActionResult result = await patient.IndexPatch("*****@*****.**", body); result.Should().BeOfType <BadRequestResult>($"neither {nameof(body.resetToken)} nor {nameof(body.oldPassword)} was provided"); } }
public async Task <IActionResult> IndexPatch([EmailAddress, MaxLength(100)] string email_address, [FromBody] IndexPatchRequestBody body) { if (!ModelState.IsValid) { return(BadRequest(ModelState)); } // Get the being. Being being = (await ef.Aliases .Include(a => a.Being) .FirstOrDefaultAsync(a => a.EmailAddress == email_address)) ?.Being; if (being == null) { return(NotFound()); } if (body.resetToken != null) { // Authenticate via reset token. if (body.oldPassword != null) { // Only one of resetToken or oldPassword may be supplied. return(BadRequest()); } if (!being.PasswordResetTokenValidUntil.HasValue || being.PasswordResetTokenValidUntil <= now.UtcNow || being.PasswordResetToken != body.resetToken) { return(Unauthorized()); } if (Sha512Util.TestPassword(body.password, being.SaltedHashedPassword)) { // Cannot change password to itself. return(StatusCode(StatusCodes.Status409Conflict)); } // The token is used up. being.PasswordResetToken = null; being.PasswordResetTokenValidUntil = null; } else { // Authenticate via old password. if (body.oldPassword == null) { // One of resetToken or oldPassword must be supplied. return(BadRequest()); } if (!Sha512Util.TestPassword(body.oldPassword, being.SaltedHashedPassword)) { return(Unauthorized()); } if (body.oldPassword == body.password) { // Cannot change password to itself. return(StatusCode(StatusCodes.Status409Conflict)); } } // Change the password. being.SaltedHashedPassword = Sha512Util.SaltAndHashNewPassword(body.password); await ef.SaveChangesAsync(); return(NoContent()); }
public void PasswordAndDifferentSalt_Crypt_ReturnsDifferentHash() { Sha512Util.Crypt("Hello world!", "$6$saltsdiffs").Should() .NotEndWith("svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1", "the salt has been changed"); }
public void PasswordAndSalt_Crypt_ReturnsExpectedHash() { Sha512Util.Crypt("Hello world!", "$6$saltstring").Should() .Be("$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1", "that's what the PHP test says"); }
public void MismatchingPasswordAndHash_TestPassword_ReturnsFalse() { Sha512Util.TestPassword("Hello worldx!", "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1") .Should().BeFalse("the hash does not match the password"); }