'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; }