// ============================================================================= // AuthenticationFailuresTests.cs // Sprint: SPRINT_0352_0001_0001_security_testing_framework // Task: SEC-0352-005 // OWASP A07:2021 - Identification and Authentication Failures // ============================================================================= using FluentAssertions; using StellaOps.Security.Tests.Infrastructure; namespace StellaOps.Security.Tests.A07_AuthenticationFailures; /// /// Tests for OWASP A07:2021 - Identification and Authentication Failures. /// Ensures proper authentication practices in Authority and related modules. /// [Trait("Category", "Security")] [Trait("OWASP", "A07")] public sealed class AuthenticationFailuresTests : SecurityTestBase { [Fact(DisplayName = "A07-001: Brute force should be rate-limited")] public async Task BruteForce_ShouldBeRateLimited() { // Arrange var attempts = 0; var blocked = false; // Act - simulate rapid authentication attempts for (int i = 0; i < 15; i++) { var result = await SimulateAuthAttempt("user@test.com", "wrong-password"); attempts++; if (result.IsBlocked) { blocked = true; break; } } // Assert blocked.Should().BeTrue("Rate limiting should block after multiple failed attempts"); attempts.Should().BeLessThanOrEqualTo(10, "Should block before 10 attempts"); } [Fact(DisplayName = "A07-002: Weak passwords should be rejected")] public void WeakPasswords_ShouldBeRejected() { // Arrange var weakPasswords = new[] { "password", "123456", "password123", "qwerty", "admin", "letmein", "welcome", "abc123" }; // Act & Assert foreach (var password in weakPasswords) { var result = ValidatePasswordStrength(password); result.IsStrong.Should().BeFalse($"Weak password '{password}' should be rejected"); } } [Fact(DisplayName = "A07-003: Strong passwords should be accepted")] public void StrongPasswords_ShouldBeAccepted() { // Arrange var strongPasswords = new[] { "C0mpl3x!P@ssw0rd#2024", "Str0ng$ecur3P@ss!", "MyV3ryL0ng&SecurePassword!", "!@#$5678Abcdefgh" }; // Act & Assert foreach (var password in strongPasswords) { var result = ValidatePasswordStrength(password); result.IsStrong.Should().BeTrue($"Strong password should be accepted"); } } [Fact(DisplayName = "A07-004: Session tokens should expire")] public void SessionTokens_ShouldExpire() { // Arrange var maxSessionDuration = TimeSpan.FromHours(24); var token = CreateSessionToken(issuedAt: DateTimeOffset.UtcNow.AddHours(-25)); // Act var isValid = ValidateSessionToken(token); // Assert isValid.Should().BeFalse("Expired session tokens should be rejected"); } [Fact(DisplayName = "A07-005: Session tokens should be revocable")] public void SessionTokens_ShouldBeRevocable() { // Arrange var token = CreateSessionToken(issuedAt: DateTimeOffset.UtcNow); ValidateSessionToken(token).Should().BeTrue("Fresh token should be valid"); // Act RevokeSessionToken(token); // Assert ValidateSessionToken(token).Should().BeFalse("Revoked token should be rejected"); } [Fact(DisplayName = "A07-006: Failed logins should not reveal user existence")] public void FailedLogins_ShouldNotRevealUserExistence() { // Arrange & Act var existingUserError = SimulateAuthAttempt("existing@test.com", "wrong").Result.ErrorMessage; var nonExistentUserError = SimulateAuthAttempt("nonexistent@test.com", "wrong").Result.ErrorMessage; // Assert - error messages should be identical existingUserError.Should().Be(nonExistentUserError, "Error messages should not reveal whether user exists"); } [Fact(DisplayName = "A07-007: MFA should be supported")] public void Mfa_ShouldBeSupported() { // Arrange var mfaMethods = GetSupportedMfaMethods(); // Assert mfaMethods.Should().NotBeEmpty("MFA should be supported"); mfaMethods.Should().Contain("TOTP", "TOTP should be a supported MFA method"); } [Fact(DisplayName = "A07-008: Account lockout should be implemented")] public async Task AccountLockout_ShouldBeImplemented() { // Arrange var userId = "test-lockout@test.com"; // Act - trigger lockout for (int i = 0; i < 10; i++) { await SimulateAuthAttempt(userId, "wrong-password"); } var accountStatus = GetAccountStatus(userId); // Assert accountStatus.IsLocked.Should().BeTrue("Account should be locked after multiple failures"); accountStatus.LockoutDuration.Should().BeGreaterThan(TimeSpan.Zero); } [Fact(DisplayName = "A07-009: Password reset tokens should be single-use")] public void PasswordResetTokens_ShouldBeSingleUse() { // Arrange var resetToken = GeneratePasswordResetToken("user@test.com"); // Act - use token once var firstUse = UsePasswordResetToken(resetToken, "NewP@ssw0rd!"); var secondUse = UsePasswordResetToken(resetToken, "AnotherP@ss!"); // Assert firstUse.Should().BeTrue("First use of reset token should succeed"); secondUse.Should().BeFalse("Second use of reset token should fail"); } [Fact(DisplayName = "A07-010: Default credentials should be changed")] public void DefaultCredentials_ShouldBeChanged() { // Arrange var defaultCredentials = new[] { ("admin", "admin"), ("root", "root"), ("admin", "password"), ("administrator", "administrator") }; // Act & Assert foreach (var (username, password) in defaultCredentials) { var result = SimulateAuthAttempt(username, password).Result; result.IsSuccess.Should().BeFalse($"Default credential {username}/{password} should not work"); } } // Helper methods private static async Task SimulateAuthAttempt(string username, string password) { await Task.Delay(1); // Simulate async operation // Simulate rate limiting after 5 attempts var attempts = GetAttemptCount(username); if (attempts >= 5) { return new AuthResult(false, true, "Authentication failed"); } IncrementAttemptCount(username); return new AuthResult(false, false, "Authentication failed"); } private static int GetAttemptCount(string username) { // Simulated - would use actual rate limiter return _attemptCounts.GetValueOrDefault(username, 0); } private static void IncrementAttemptCount(string username) { _attemptCounts[username] = _attemptCounts.GetValueOrDefault(username, 0) + 1; } private static readonly Dictionary _attemptCounts = new(); private static PasswordValidationResult ValidatePasswordStrength(string password) { var hasUpperCase = password.Any(char.IsUpper); var hasLowerCase = password.Any(char.IsLower); var hasDigit = password.Any(char.IsDigit); var hasSpecial = password.Any(c => !char.IsLetterOrDigit(c)); var isLongEnough = password.Length >= 12; var isStrong = hasUpperCase && hasLowerCase && hasDigit && hasSpecial && isLongEnough; return new PasswordValidationResult(isStrong); } private static string CreateSessionToken(DateTimeOffset issuedAt) { return $"session_{issuedAt.ToUnixTimeSeconds()}_{Guid.NewGuid()}"; } private static readonly HashSet _revokedTokens = new(); private static bool ValidateSessionToken(string token) { if (_revokedTokens.Contains(token)) return false; // Extract issued time var parts = token.Split('_'); if (parts.Length < 2 || !long.TryParse(parts[1], out var issuedUnix)) return false; var issuedAt = DateTimeOffset.FromUnixTimeSeconds(issuedUnix); var age = DateTimeOffset.UtcNow - issuedAt; return age < TimeSpan.FromHours(24); } private static void RevokeSessionToken(string token) { _revokedTokens.Add(token); } private static string[] GetSupportedMfaMethods() { return new[] { "TOTP", "WebAuthn", "SMS", "Email" }; } private static AccountStatus GetAccountStatus(string userId) { var attempts = _attemptCounts.GetValueOrDefault(userId, 0); return new AccountStatus(attempts >= 10, TimeSpan.FromMinutes(15)); } private static readonly HashSet _usedResetTokens = new(); private static string GeneratePasswordResetToken(string email) { return $"reset_{email}_{Guid.NewGuid()}"; } private static bool UsePasswordResetToken(string token, string newPassword) { if (_usedResetTokens.Contains(token)) return false; _usedResetTokens.Add(token); return true; } private record AuthResult(bool IsSuccess, bool IsBlocked, string ErrorMessage); private record PasswordValidationResult(bool IsStrong); private record AccountStatus(bool IsLocked, TimeSpan LockoutDuration); }