first commit

This commit is contained in:
Claudecio Martins
2026-06-16 10:04:10 -03:00
commit a951944997
4463 changed files with 419677 additions and 0 deletions
@@ -0,0 +1,217 @@
<?php
namespace WorkbloomERP\Module\v0\Auth\Middlewares;
use WorkbloomERP\Utils\JwtUtil;
use KrothiumAPI\Utils\HttpUtil;
use WorkbloomERP\Utils\CacheUtil;
use WorkbloomERP\Utils\CypherUtil;
use WorkbloomERP\Exceptions\AppException;
use WorkbloomERP\Module\v0\Auth\Factories\ServiceFactory;
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;
class AuthMiddleware {
/**
* Valida se o token JWT da requisição atual corresponde a um estado de "Login Inicial".
*
* Este método atua como um guardião de segurança (Guard) para rotas que exigem
* ações obrigatórias do usuário logo após o primeiro acesso ao sistema, como a
* troca obrigatória de senha ou a configuração de MFA (Autenticação de Dois Fatores).
*
* ---
* ## Fluxo de Execução
* 1. **Extração:** Obtém o token JWT do cabeçalho `Authorization: Bearer`.
* 2. **Validação:** Verifica a integridade e assinatura do token através do `JwtUtil`.
* 3. **Verificação de Claim:** Decodifica o token e confirma se a claim `initial_login` está presente e definida como `true`.
* 4. **Retorno:**
* - Retorna `true` se o token for válido e corresponder ao estado de login inicial.
* - Retorna um `array` estruturado contendo detalhes do erro caso a validação falhe.
*
* ---
* ## Observações Técnicas
* - O método utiliza variáveis de ambiente (`$_ENV`) para configurar o `JwtUtil`, garantindo que as configurações de `secretKey`, `algorithm` e `ttl` estejam sincronizadas com a política de segurança da aplicação.
* - Este método captura internamente instâncias de `AppException`, tratando o fluxo de erro de forma silenciosa para o chamador, retornando um status de erro estruturado em vez de propagar a exceção.
*
* @return true|array{status: string, response_code: int, message: string}
* Retorna `true` em caso de sucesso. Retorna um array com o erro caso o token seja inválido, ausente ou não possua a claim necessária.
*/
public static function isInitialLogin(): true|array {
try {
// Valida o token JWT presente no header Authorization (Bearer Token)
$bearerToken = HttpUtil::getBearerToken();
if (!$bearerToken) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
$jwtUtil = new JwtUtil(
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
);
$decodedToken = $jwtUtil->validate(token: $bearerToken);
if (!$decodedToken) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
$decodedToken = json_decode(json: json_encode($decodedToken, flags: JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
$userData = $decodedToken['user_data'] ?? null;
if (!$userData) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
if (!isset($userData['initial_login']) || $userData['initial_login'] !== true) {
throw new AppException(message: 'Token de acesso não corresponde a um login inicial.', code: 403);
}
return true;
} catch(AppException $e) {
return [
'status' => 'error',
'response_code' => $e->getCode() ?? 500,
'message' => $e->getMessage()
];
}
}
/**
* Middleware de autenticação para validar sessões e tokens JWT em requisições protegidas.
*
* Este método atua como um "Gatekeeper" (Guard). Ele implementa uma estratégia híbrida
* de validação (Cache-First, seguida de DB e JWT) para equilibrar performance com
* segurança. Ele verifica a existência do token, a validade da assinatura JWT,
* a revogação da sessão no banco de dados e a integridade do dispositivo (IP/User-Agent).
*
* ---
* ## Fluxo de Execução
* 1. **Extração:** Obtém o token do header `Authorization`. Retorna erro 401 se ausente.
* 2. **Cache-First Check:** Verifica no Redis (`login:main:{token}`). Se encontrado, autoriza imediatamente (retorna `null`), evitando carga no banco de dados.
* 3. **Validação de Persistência:** Consulta o `UsuarioSessionRepo` (via hash do token). Valida revogação (`revoked_at`) e integridade.
* 4. **Validação JWT:** Decodifica e verifica a assinatura/expiração (`exp`) do JWT.
* 5. **Validação de Integridade (Guard):** Invoca `validateSessionData` para garantir que o IP e User-Agent atuais coincidem com a sessão original.
* 6. **Caching:** Se tudo estiver correto, armazena o payload no cache para acelerar futuras requisições e retorna `null` (acesso concedido).
*
* ---
* ## Arquitetura de Validação
*
*
* ---
* ## Observações Técnicas
* - **Interface de Retorno:**
* - `null`: A autenticação foi bem-sucedida; o fluxo de execução pode continuar.
* - `array`: A autenticação falhou; o array contém a resposta de erro estruturada para o cliente.
* - **Segurança:** Utiliza `CypherUtil` para garantir que tokens sensíveis não sejam comparados em texto puro contra o banco de dados.
*
* @return array|null Retorna `null` se o usuário estiver autenticado e autorizado. Retorna um `array` com `response_code`, `message` e `output` em caso de erro.
* @see JwtUtil::validate() Para detalhes sobre a validação da assinatura criptográfica.
* @see self::validateSessionData() Para a verificação de segurança de IP e User-Agent.
*/
public static function handle(): ?array {
try {
// Valida o token JWT presente no header Authorization (Bearer Token)
$bearerToken = HttpUtil::getBearerToken();
if (!$bearerToken) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
$jwtUtil = new JwtUtil(
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
);
// Valida o token JWT e decodifica seu payload para extrair os dados do usuário e as informações de sessão.
$decodedToken = $jwtUtil->validate(token: $bearerToken);
if (!$decodedToken) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
// Verifica se o token corresponde a um login inicial, bloqueando o acesso a rotas protegidas caso seja verdadeiro.
$decodedToken = json_decode(json: json_encode($decodedToken, flags: JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
if ($decodedToken['user_data']['initial_login'] == true) {
throw new AppException(message: 'Acesso negado. O token corresponde a um login inicial e não pode ser usado para acessar esta rota.', code: 403);
}
// Verifica se a sessão principal já está em cache para evitar consultas desnecessárias ao banco de dados e validações repetitivas.
// Se a sessão estiver em cache, considera o token como válido e retorna null para permitir o acesso à rota protegida.
if (CacheUtil::get(key: "login:main:{$bearerToken}")) {
return null;
}
$usuarioSessionRepo = ServiceFactory::makeUsuarioSessionRepo();
// Busca a sessão do usuário no banco de dados utilizando o hash do token para garantir que o token é válido e não foi revogado.
$usuarioSessionModel = $usuarioSessionRepo->findByIdentifier(identifier: 'token_hash', value: CypherUtil::hash(data: $bearerToken));
if (!$usuarioSessionModel || !CypherUtil::verify(data: $bearerToken, hash: $usuarioSessionModel->getTokenHash()) || $usuarioSessionModel->getRevokedAt()) {
throw new AppException(message: 'Sessão inválida ou expirada. Faça login novamente.', code: 401);
}
// Valida o token JWT e decodifica seu payload para extrair os dados do usuário e as informações de sessão.
// Se o token for inválido, expirado ou tiver sua assinatura comprometida, uma exceção é lançada.
$decodedToken = $jwtUtil->validate(token: $bearerToken);
if (!$decodedToken) {
throw new AppException(message: 'Token de acesso inválido ou ausente.', code: 401);
}
$decodedToken = json_decode(json: json_encode($decodedToken, JSON_THROW_ON_ERROR), associative: true, flags: JSON_THROW_ON_ERROR);
$remainingTime = $decodedToken['exp'] - time();
if ($remainingTime <= 0) {
throw new AppException(message: 'Sessão expirada. Faça login novamente.', code: 401);
}
self::validateSessionData(sessionModel: $usuarioSessionModel, tokenData: $decodedToken);
CacheUtil::set(key: "login:main:{$bearerToken}", ttl: $remainingTime, value: $decodedToken);
return null;
} catch (AppException $e) {
return [
'status' => 'error',
'response_code' => $e->getCode() ?: 500,
'message' => $e->getMessage(),
'output' => $e->getDetails() ? ['errors' => $e->getDetails()] : null,
];
}
}
/**
* Valida a integridade e autenticidade da sessão do usuário comparando metadados.
*
* Este método atua como uma barreira de segurança (Guard) que garante que a sessão
* atual está sendo utilizada pelo mesmo dispositivo e rede que a iniciou. Ao comparar
* o IP e o User-Agent da requisição atual com os dados persistidos no banco de dados
* (`UsuarioSessionModel`) e os metadados contidos no token JWT (`$tokenData`),
* protegemos o sistema contra ataques de sequestro de sessão (session hijacking).
*
* ---
* ## Fluxo de Execução
* 1. **Captura:** Extrai o IP (`REMOTE_ADDR`) e o User-Agent (`HTTP_USER_AGENT`) da variável superglobal `$_SERVER`.
* 2. **Comparação (Match):** Utiliza uma expressão `match` para validar quatro pontos de convergência:
* - IP da Sessão (DB) vs. IP Atual.
* - User-Agent da Sessão (DB) vs. User-Agent Atual.
* - IP registrado no Payload do Token vs. IP Atual.
* - User-Agent registrado no Payload do Token vs. User-Agent Atual.
* 3. **Bloqueio:** Caso qualquer divergência seja detectada, a sessão é considerada inválida e uma `AppException` (401 Unauthorized) é disparada.
*
* ---
* ## Observações Técnicas
* - O uso de `match` (PHP 8+) torna o código altamente legível e performático para a verificação booleana múltipla.
* - Este método é fundamental em cenários onde a segurança de acesso exige que o usuário não troque de rede ou navegador enquanto a sessão estiver ativa.
* - Caso a requisição venha através de um proxy, certifique-se de que a configuração de captura de IP (`REMOTE_ADDR`) esteja ajustada corretamente no ambiente.
*
* @param UsuarioSessionModel $sessionModel O modelo contendo os dados da sessão persistidos no banco.
* @param array $tokenData O payload decodificado do token JWT contendo os metadados da sessão.
* @return void
* @throws AppException Caso haja qualquer incompatibilidade nos dados (IP ou User-Agent), invalidando a sessão.
*/
private static function validateSessionData(UsuarioSessionModel $sessionModel, array $tokenData): void {
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
$isInvalidSession = match(true) {
$sessionModel->getIpAddress() !== $ipAddress,
$sessionModel->getUserAgent() !== $userAgent,
$tokenData['session_data']['ip_address'] !== $ipAddress,
$tokenData['session_data']['user_agent'] !== $userAgent => true,
default => false
};
if ($isInvalidSession) {
throw new AppException(message: 'Sessão inválida ou expirada. Faça login novamente.', code: 401);
}
}
}