Suas ideias em realidade digital!
Em APIs com JWT, o ideal é que o access token tenha vida curta (ex: 10–15 minutos). Assim, se ele vazar, o estrago é limitado. Mas aí surge o problema: o usuário teria que logar toda hora.
A solução é usar um refresh token (vida longa, ex: 7 a 30 dias) para gerar novos access tokens sem pedir senha novamente.
Authorization: Bearer./api/auth/refreshPOST /api/auth/login → gera tokensPOST /api/auth/refresh → renova access (e rotaciona refresh)POST /api/auth/logout → revoga refreshGET /api/me → rota protegida com access tokenGuardar refresh token no banco permite revogar, rotacionar e controlar sessões. Exemplo de tabela simples:
CREATE TABLE refresh_tokens (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
token_hash VARCHAR(255) NOT NULL,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at DATETIME NULL,
user_agent VARCHAR(255) NULL,
ip VARCHAR(64) NULL,
INDEX (user_id),
INDEX (token_hash)
);
✅ Armazene hash do refresh token, não o token em texto puro.
Você pode manter o JWT manual (como no post anterior) ou usar biblioteca. Aqui vou usar o mesmo padrão: JWT no access + refresh aleatório (mais seguro).
<?php
function generateRefreshToken(): string {
return bin2hex(random_bytes(48)); // 96 chars
}
<?php
function saveRefreshToken(PDO $pdo, int $userId, string $refreshToken, int $days = 15): array {
$hash = password_hash($refreshToken, PASSWORD_BCRYPT);
$expiresAt = (new DateTimeImmutable())->modify("+{$days} days");
$stmt = $pdo->prepare("INSERT INTO refresh_tokens (user_id, token_hash, expires_at, user_agent, ip)
VALUES (:uid, :hash, :exp, :ua, :ip)");
$stmt->execute([
':uid' => $userId,
':hash' => $hash,
':exp' => $expiresAt->format('Y-m-d H:i:s'),
':ua' => ($_SERVER['HTTP_USER_AGENT'] ?? null),
':ip' => ($_SERVER['REMOTE_ADDR'] ?? null),
]);
return ['id' => (int)$pdo->lastInsertId(), 'expires_at' => $expiresAt];
}
🔐 Dica: se você quiser suportar múltiplos dispositivos, não sobrescreva tokens antigos — crie um por sessão.
<?php
// POST /api/auth/login
// 1) valida credenciais...
$userId = 1; // exemplo
$access = generateJWT([
'id' => $userId,
'role' => 'user'
], 'SEGREDO_SUPER_SECRETO', 900); // 15 min
$refresh = generateRefreshToken();
// salva refresh no banco
$pdo = getPDO(); // sua conexão
$stored = saveRefreshToken($pdo, $userId, $refresh, 15);
// grava cookie httpOnly
setcookie('refresh_token', $refresh, [
'expires' => $stored['expires_at']->getTimestamp(),
'path' => '/api/auth',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
echo json_encode(['access_token' => $access, 'expires_in' => 900]);
✅ Access token vai no front. ✅ Refresh token fica em cookie httpOnly (front não lê via JS).
A prática mais segura é rotacionar o refresh token: cada uso gera um novo e revoga o anterior. Isso reduz risco de replay se vazar.
<?php
// POST /api/auth/refresh
$refresh = $_COOKIE['refresh_token'] ?? '';
if (!$refresh) {
http_response_code(401);
echo json_encode(['error' => 'Refresh token ausente']);
exit;
}
$pdo = getPDO();
// 1) buscar tokens válidos não revogados e não expirados
$stmt = $pdo->prepare("SELECT id, user_id, token_hash, expires_at, revoked_at
FROM refresh_tokens
WHERE revoked_at IS NULL AND expires_at > NOW()
ORDER BY id DESC LIMIT 50");
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 2) comparar com password_verify (hash)
$found = null;
foreach ($rows as $r) {
if (password_verify($refresh, $r['token_hash'])) { $found = $r; break; }
}
if (!$found) {
http_response_code(401);
echo json_encode(['error' => 'Refresh inválido ou expirado']);
exit;
}
// 3) rotacionar: revoga o token atual
$pdo->prepare("UPDATE refresh_tokens SET revoked_at = NOW(), last_used_at = NOW() WHERE id = :id")
->execute([':id' => $found['id']]);
// 4) emite novo refresh + salva
$newRefresh = generateRefreshToken();
$stored = saveRefreshToken($pdo, (int)$found['user_id'], $newRefresh, 15);
// 5) emite novo access
$newAccess = generateJWT([
'id' => (int)$found['user_id'],
'role' => 'user'
], 'SEGREDO_SUPER_SECRETO', 900);
// 6) atualiza cookie
setcookie('refresh_token', $newRefresh, [
'expires' => $stored['expires_at']->getTimestamp(),
'path' => '/api/auth',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
echo json_encode(['access_token' => $newAccess, 'expires_in' => 900]);
💡 Em produção, em vez de varrer “50 tokens”, o ideal é incluir um identificador (ex: jti) ou token_id. Aqui mantive simples e didático.
<?php
// POST /api/auth/logout
$refresh = $_COOKIE['refresh_token'] ?? '';
if ($refresh) {
$pdo = getPDO();
// tenta encontrar e revogar o token
$stmt = $pdo->prepare("SELECT id, token_hash FROM refresh_tokens
WHERE revoked_at IS NULL AND expires_at > NOW()
ORDER BY id DESC LIMIT 100");
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $r) {
if (password_verify($refresh, $r['token_hash'])) {
$pdo->prepare("UPDATE refresh_tokens SET revoked_at = NOW() WHERE id = :id")
->execute([':id' => $r['id']]);
break;
}
}
}
// remove cookie
setcookie('refresh_token', '', [
'expires' => time() - 3600,
'path' => '/api/auth',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
echo json_encode(['ok' => true]);