Add project files
This commit is contained in:
+268
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
/**
|
||||
* JWT Authentication Helper
|
||||
* Provides JWT token generation and validation for API authentication
|
||||
*/
|
||||
|
||||
use Firebase\JWT\JWT as FirebaseJWT;
|
||||
use Firebase\JWT\Key;
|
||||
|
||||
class JWT {
|
||||
private static ?string $secretKey = null;
|
||||
|
||||
/**
|
||||
* Get or generate JWT secret key
|
||||
*/
|
||||
private static function getSecretKey(): string {
|
||||
if (self::$secretKey !== null) {
|
||||
return self::$secretKey;
|
||||
}
|
||||
|
||||
// Try to get from environment
|
||||
$envKey = getenv('JWT_SECRET');
|
||||
if ($envKey && strlen($envKey) >= 32) {
|
||||
self::$secretKey = $envKey;
|
||||
return self::$secretKey;
|
||||
}
|
||||
|
||||
// Try to get from database settings
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('SELECT value FROM settings WHERE key = ?');
|
||||
$stmt->execute(['jwt_secret']);
|
||||
$result = $stmt->fetch();
|
||||
|
||||
if ($result && !empty($result['value'])) {
|
||||
self::$secretKey = $result['value'];
|
||||
return self::$secretKey;
|
||||
}
|
||||
|
||||
// Generate new secret key and save it
|
||||
$newKey = bin2hex(random_bytes(32));
|
||||
|
||||
$stmt = $pdo->prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = ?');
|
||||
$stmt->execute(['jwt_secret', $newKey, $newKey]);
|
||||
|
||||
self::$secretKey = $newKey;
|
||||
return self::$secretKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JWT token for user
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @param int $expiresIn Token lifetime in seconds (default: 30 days)
|
||||
* @return string JWT token
|
||||
*/
|
||||
public static function generate(int $userId, int $expiresIn = 2592000): string {
|
||||
$issuedAt = time();
|
||||
$expire = $issuedAt + $expiresIn;
|
||||
|
||||
$payload = [
|
||||
'iss' => 'amnezia-panel', // Issuer
|
||||
'aud' => 'amnezia-api', // Audience
|
||||
'iat' => $issuedAt, // Issued at
|
||||
'exp' => $expire, // Expiration
|
||||
'sub' => $userId, // Subject (user ID)
|
||||
'jti' => bin2hex(random_bytes(16)) // JWT ID (unique token identifier)
|
||||
];
|
||||
|
||||
return FirebaseJWT::encode($payload, self::getSecretKey(), 'HS256');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and decode JWT token
|
||||
*
|
||||
* @param string $token JWT token
|
||||
* @return object|null Decoded token payload or null if invalid
|
||||
*/
|
||||
public static function decode(string $token): ?object {
|
||||
try {
|
||||
$decoded = FirebaseJWT::decode($token, new Key(self::getSecretKey(), 'HS256'));
|
||||
|
||||
// Verify issuer and audience
|
||||
if ($decoded->iss !== 'amnezia-panel' || $decoded->aud !== 'amnezia-api') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
} catch (Exception $e) {
|
||||
error_log('JWT decode error: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user ID from JWT token
|
||||
*
|
||||
* @param string $token JWT token
|
||||
* @return int|null User ID or null if invalid
|
||||
*/
|
||||
public static function getUserId(string $token): ?int {
|
||||
$decoded = self::decode($token);
|
||||
|
||||
if ($decoded === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)$decoded->sub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify JWT token and get user data
|
||||
*
|
||||
* @param string $token JWT token
|
||||
* @return array|null User data or null if invalid
|
||||
*/
|
||||
public static function verify(string $token): ?array {
|
||||
$userId = self::getUserId($token);
|
||||
|
||||
if ($userId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('SELECT id, name, email, role FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
return $user ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract token from Authorization header
|
||||
*
|
||||
* @return string|null Token or null if not found
|
||||
*/
|
||||
public static function getTokenFromHeader(): ?string {
|
||||
// Try getallheaders() first (Apache/FPM)
|
||||
if (function_exists('getallheaders')) {
|
||||
$headers = getallheaders();
|
||||
} else {
|
||||
// Fallback for other environments (nginx, CLI)
|
||||
$headers = [];
|
||||
foreach ($_SERVER as $key => $value) {
|
||||
if (strpos($key, 'HTTP_') === 0) {
|
||||
$header = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5)))));
|
||||
$headers[$header] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check Authorization header
|
||||
if (isset($headers['Authorization'])) {
|
||||
$auth = $headers['Authorization'];
|
||||
|
||||
// Bearer token format: "Bearer {token}"
|
||||
if (preg_match('/Bearer\s+(.+)/', $auth, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Check X-API-Token header (alternative)
|
||||
if (isset($headers['X-Api-Token'])) {
|
||||
return $headers['X-Api-Token'];
|
||||
}
|
||||
|
||||
// Also check direct $_SERVER access for Authorization
|
||||
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
|
||||
$auth = $_SERVER['HTTP_AUTHORIZATION'];
|
||||
if (preg_match('/Bearer\s+(.+)/', $auth, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware: Require JWT authentication for API endpoints
|
||||
*
|
||||
* @return array|null User data if authenticated, sends 401 response and returns null if not
|
||||
*/
|
||||
public static function requireAuth(): ?array {
|
||||
$token = self::getTokenFromHeader();
|
||||
|
||||
if ($token === null) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Missing authentication token']);
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = self::verify($token);
|
||||
|
||||
if ($user === null) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Invalid or expired token']);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create API token for user (saves to database)
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @param string|null $name Token name/description
|
||||
* @param int $expiresIn Token lifetime in seconds (default: 30 days)
|
||||
* @return array Token data (id, token, expires_at)
|
||||
*/
|
||||
public static function createApiToken(int $userId, ?string $name = null, int $expiresIn = 2592000): array {
|
||||
$token = self::generate($userId, $expiresIn);
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + $expiresIn);
|
||||
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO api_tokens (user_id, token, name, expires_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
$userId,
|
||||
$token,
|
||||
$name ?? 'API Token',
|
||||
$expiresAt
|
||||
]);
|
||||
|
||||
return [
|
||||
'id' => (int)$pdo->lastInsertId(),
|
||||
'token' => $token,
|
||||
'name' => $name ?? 'API Token',
|
||||
'expires_at' => $expiresAt
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke API token
|
||||
*
|
||||
* @param int $tokenId Token ID
|
||||
* @param int $userId User ID (for ownership verification)
|
||||
* @return bool Success
|
||||
*/
|
||||
public static function revokeApiToken(int $tokenId, int $userId): bool {
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('DELETE FROM api_tokens WHERE id = ? AND user_id = ?');
|
||||
return $stmt->execute([$tokenId, $userId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all API tokens for user
|
||||
*
|
||||
* @param int $userId User ID
|
||||
* @return array List of tokens
|
||||
*/
|
||||
public static function getUserTokens(int $userId): array {
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT id, name, LEFT(token, 20) as token_preview, created_at, expires_at
|
||||
FROM api_tokens
|
||||
WHERE user_id = ? AND (expires_at IS NULL OR expires_at > NOW())
|
||||
ORDER BY created_at DESC
|
||||
');
|
||||
$stmt->execute([$userId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user