264 lines
6.0 KiB
PHP
264 lines
6.0 KiB
PHP
<?php
|
|
// phpcs:ignoreFile
|
|
/**
|
|
* JWT Token Helpers
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Firebase\JWT\JWT;
|
|
use Firebase\JWT\Key;
|
|
use Firebase\JWT\ExpiredException;
|
|
|
|
/**
|
|
* Get the JWT secret key from environment or fallback.
|
|
*/
|
|
function jwt_get_secret(): string
|
|
{
|
|
$secret = getenv('JWT_SECRET');
|
|
|
|
if ($secret === false || $secret === '') {
|
|
// Fallback for development - MUST be set in production
|
|
$secret = 'lasuca-dev-secret-change-in-production-' . md5(__DIR__);
|
|
}
|
|
|
|
return $secret;
|
|
}
|
|
|
|
/**
|
|
* Generate an access token for a user.
|
|
*/
|
|
function jwt_create_access_token(array $user): string
|
|
{
|
|
$issuedAt = time();
|
|
$expiresAt = $issuedAt + (15 * 60); // 15 minutes
|
|
|
|
$payload = [
|
|
'iss' => 'lasuca-api',
|
|
'iat' => $issuedAt,
|
|
'exp' => $expiresAt,
|
|
'sub' => $user['username'],
|
|
'type' => 'access',
|
|
'user' => [
|
|
'username' => $user['username'],
|
|
'growerid' => $user['growerid'] ?? null,
|
|
'growername' => $user['growername'] ?? null,
|
|
'email' => $user['email'] ?? null,
|
|
],
|
|
];
|
|
|
|
return JWT::encode($payload, jwt_get_secret(), 'HS256');
|
|
}
|
|
|
|
/**
|
|
* Generate a refresh token for a user.
|
|
*/
|
|
function jwt_create_refresh_token(array $user): array
|
|
{
|
|
$issuedAt = time();
|
|
$expiresAt = $issuedAt + (30 * 24 * 60 * 60); // 30 days
|
|
$tokenId = bin2hex(random_bytes(32));
|
|
|
|
$payload = [
|
|
'iss' => 'lasuca-api',
|
|
'iat' => $issuedAt,
|
|
'exp' => $expiresAt,
|
|
'sub' => $user['username'],
|
|
'type' => 'refresh',
|
|
'jti' => $tokenId,
|
|
];
|
|
|
|
$token = JWT::encode($payload, jwt_get_secret(), 'HS256');
|
|
|
|
return [
|
|
'token' => $token,
|
|
'token_id' => $tokenId,
|
|
'expires_at' => date('Y-m-d H:i:s', $expiresAt),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Validate and decode a JWT token.
|
|
*/
|
|
function jwt_decode_token(string $token): ?array
|
|
{
|
|
try {
|
|
$decoded = JWT::decode($token, new Key(jwt_get_secret(), 'HS256'));
|
|
return (array) $decoded;
|
|
} catch (ExpiredException $e) {
|
|
return null;
|
|
} catch (\Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract Bearer token from Authorization header.
|
|
*/
|
|
function jwt_get_bearer_token(): ?string
|
|
{
|
|
$headers = null;
|
|
|
|
if (isset($_SERVER['Authorization'])) {
|
|
$headers = trim($_SERVER['Authorization']);
|
|
} elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) {
|
|
$headers = trim($_SERVER['HTTP_AUTHORIZATION']);
|
|
} elseif (function_exists('apache_request_headers')) {
|
|
$requestHeaders = apache_request_headers();
|
|
$requestHeaders = array_combine(
|
|
array_map('ucwords', array_keys($requestHeaders)),
|
|
array_values($requestHeaders)
|
|
);
|
|
if (isset($requestHeaders['Authorization'])) {
|
|
$headers = trim($requestHeaders['Authorization']);
|
|
}
|
|
}
|
|
|
|
if ($headers === null) {
|
|
return null;
|
|
}
|
|
|
|
if (preg_match('/Bearer\s+(\S+)/i', $headers, $matches)) {
|
|
return $matches[1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Require valid access token and return user data.
|
|
*/
|
|
function jwt_require_auth(): array
|
|
{
|
|
$token = jwt_get_bearer_token();
|
|
|
|
if ($token === null) {
|
|
api_error('Authorization required', 401);
|
|
}
|
|
|
|
$payload = jwt_decode_token($token);
|
|
|
|
if ($payload === null) {
|
|
api_error('Invalid or expired token', 401);
|
|
}
|
|
|
|
if (!isset($payload['type']) || $payload['type'] !== 'access') {
|
|
api_error('Invalid token type', 401);
|
|
}
|
|
|
|
if (!isset($payload['user']) || !is_array((array) $payload['user'])) {
|
|
api_error('Invalid token payload', 401);
|
|
}
|
|
|
|
return (array) $payload['user'];
|
|
}
|
|
|
|
/**
|
|
* Store refresh token in database.
|
|
*/
|
|
function jwt_store_refresh_token(string $username, string $tokenId, string $expiresAt): bool
|
|
{
|
|
global $conn;
|
|
|
|
$sql = "INSERT INTO refresh_tokens (username, token_id, expires_at, created_at)
|
|
VALUES (?, ?, ?, NOW())";
|
|
|
|
$stmt = $conn->prepare($sql);
|
|
|
|
if ($stmt === false) {
|
|
return false;
|
|
}
|
|
|
|
$stmt->bind_param('sss', $username, $tokenId, $expiresAt);
|
|
$success = $stmt->execute();
|
|
$stmt->close();
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* Validate refresh token exists and is not revoked.
|
|
*/
|
|
function jwt_validate_refresh_token(string $username, string $tokenId): bool
|
|
{
|
|
global $conn;
|
|
|
|
$sql = "SELECT id FROM refresh_tokens
|
|
WHERE username = ?
|
|
AND token_id = ?
|
|
AND expires_at > NOW()
|
|
AND revoked_at IS NULL
|
|
LIMIT 1";
|
|
|
|
$stmt = $conn->prepare($sql);
|
|
|
|
if ($stmt === false) {
|
|
return false;
|
|
}
|
|
|
|
$stmt->bind_param('ss', $username, $tokenId);
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
$exists = $result->num_rows > 0;
|
|
$stmt->close();
|
|
|
|
return $exists;
|
|
}
|
|
|
|
/**
|
|
* Revoke a specific refresh token.
|
|
*/
|
|
function jwt_revoke_refresh_token(string $tokenId): bool
|
|
{
|
|
global $conn;
|
|
|
|
$sql = "UPDATE refresh_tokens SET revoked_at = NOW() WHERE token_id = ?";
|
|
|
|
$stmt = $conn->prepare($sql);
|
|
|
|
if ($stmt === false) {
|
|
return false;
|
|
}
|
|
|
|
$stmt->bind_param('s', $tokenId);
|
|
$success = $stmt->execute();
|
|
$stmt->close();
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* Revoke all refresh tokens for a user.
|
|
*/
|
|
function jwt_revoke_all_user_tokens(string $username): bool
|
|
{
|
|
global $conn;
|
|
|
|
$sql = "UPDATE refresh_tokens SET revoked_at = NOW() WHERE username = ? AND revoked_at IS NULL";
|
|
|
|
$stmt = $conn->prepare($sql);
|
|
|
|
if ($stmt === false) {
|
|
return false;
|
|
}
|
|
|
|
$stmt->bind_param('s', $username);
|
|
$success = $stmt->execute();
|
|
$stmt->close();
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* Clean up expired tokens (call periodically).
|
|
*/
|
|
function jwt_cleanup_expired_tokens(): int
|
|
{
|
|
global $conn;
|
|
|
|
$sql = "DELETE FROM refresh_tokens WHERE expires_at < NOW()";
|
|
$result = $conn->query($sql);
|
|
|
|
return $result ? $conn->affected_rows : 0;
|
|
}
|