[FIX] Uge session of debugging and code coverage.
This commit is contained in:
+50
-47
@@ -7,7 +7,7 @@ class Auth {
|
||||
private string $usersFile;
|
||||
private string $jwtSecret;
|
||||
|
||||
public function __construct(string $usersFile = null) {
|
||||
public function __construct(?string $usersFile = null) {
|
||||
$this->usersFile = $usersFile ?? __DIR__ . '/../config/users.json';
|
||||
|
||||
// Load JWT secret from environment variable
|
||||
@@ -19,25 +19,34 @@ class Auth {
|
||||
|
||||
public function login(string $username, string $password): array {
|
||||
$users = $this->loadUsers();
|
||||
$user = null;
|
||||
|
||||
foreach ($users['users'] as $user) {
|
||||
if ($user['username'] === $username) {
|
||||
if (password_verify($password, $user['password'])) {
|
||||
$token = $this->generateToken($user);
|
||||
return [
|
||||
'success' => true,
|
||||
'token' => $token,
|
||||
'user' => [
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role']
|
||||
]
|
||||
];
|
||||
}
|
||||
return ['success' => false, 'error' => 'Invalid password'];
|
||||
foreach ($users['users'] as $candidate) {
|
||||
if (hash_equals($candidate['username'], $username)) {
|
||||
$user = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => false, 'error' => 'User not found'];
|
||||
// Always run a password check to avoid a timing oracle on user existence
|
||||
if ($user === null) {
|
||||
password_verify($password, '$2y$10$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG');
|
||||
return ['success' => false, 'error' => 'Invalid credentials'];
|
||||
}
|
||||
|
||||
if (!password_verify($password, $user['password'])) {
|
||||
return ['success' => false, 'error' => 'Invalid credentials'];
|
||||
}
|
||||
|
||||
$token = $this->generateToken($user);
|
||||
return [
|
||||
'success' => true,
|
||||
'token' => $token,
|
||||
'user' => [
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function verifyToken(string $token): ?array {
|
||||
@@ -49,8 +58,14 @@ class Auth {
|
||||
|
||||
list($header, $payload, $signature) = $parts;
|
||||
|
||||
// Verify signature
|
||||
$expectedSignature = base64_encode(
|
||||
// Verify algorithm header before trusting anything
|
||||
$headerData = json_decode(self::base64UrlDecode($header), true);
|
||||
if (!is_array($headerData) || ($headerData['alg'] ?? '') !== self::JWT_ALGO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify signature (constant-time)
|
||||
$expectedSignature = self::base64UrlEncode(
|
||||
hash_hmac('sha256', "$header.$payload", $this->jwtSecret, true)
|
||||
);
|
||||
|
||||
@@ -59,54 +74,46 @@ class Auth {
|
||||
}
|
||||
|
||||
// Decode payload
|
||||
$payloadData = json_decode(base64_decode($payload), true);
|
||||
$payloadData = json_decode(self::base64UrlDecode($payload), true);
|
||||
|
||||
// Check expiry
|
||||
if (isset($payloadData['exp']) && $payloadData['exp'] < time()) {
|
||||
if (!is_array($payloadData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Expiry is mandatory
|
||||
if (!isset($payloadData['exp']) || !is_numeric($payloadData['exp']) || (int) $payloadData['exp'] < time()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $payloadData;
|
||||
}
|
||||
|
||||
public function requireAuth(string $token): array {
|
||||
$payload = $this->verifyToken($token);
|
||||
|
||||
if ($payload === null) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
private static function base64UrlEncode(string $data): string {
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
public function requireAdmin(string $token): array {
|
||||
$payload = $this->requireAuth($token);
|
||||
|
||||
if ($payload['role'] !== 'admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Forbidden']);
|
||||
exit;
|
||||
private static function base64UrlDecode(string $data): string {
|
||||
$remainder = strlen($data) % 4;
|
||||
if ($remainder !== 0) {
|
||||
$data .= str_repeat('=', 4 - $remainder);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
return base64_decode(strtr($data, '-_', '+/'));
|
||||
}
|
||||
|
||||
private function generateToken(array $user): string {
|
||||
$header = base64_encode(json_encode([
|
||||
$header = self::base64UrlEncode(json_encode([
|
||||
'alg' => self::JWT_ALGO,
|
||||
'typ' => 'JWT'
|
||||
]));
|
||||
|
||||
$payload = base64_encode(json_encode([
|
||||
$payload = self::base64UrlEncode(json_encode([
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
'iat' => time(),
|
||||
'exp' => time() + self::JWT_EXPIRY
|
||||
]));
|
||||
|
||||
$signature = base64_encode(
|
||||
$signature = self::base64UrlEncode(
|
||||
hash_hmac('sha256', "$header.$payload", $this->jwtSecret, true)
|
||||
);
|
||||
|
||||
@@ -121,8 +128,4 @@ class Auth {
|
||||
$content = file_get_contents($this->usersFile);
|
||||
return json_decode($content, true) ?? ['users' => []];
|
||||
}
|
||||
|
||||
public function hashPassword(string $password): string {
|
||||
return password_hash($password, PASSWORD_BCRYPT);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user