Segurança • 16 min de leitura

JWT na prática: autenticação real em PHP (sem Node) e em JavaScript puro

JWT na prática: autenticação real em PHP (sem Node) e em JavaScript puro

Antes de escrever qualquer código, vale ser honesto sobre algo que a maioria dos artigos ignora: JWT e OAuth2 não são a mesma coisa, e você provavelmente não precisa de OAuth2.

OAuth2 é um protocolo de delegação de autorização — serve para deixar um sistema acessar recursos de outro em nome do usuário. O "entrar com Google" do seu site é OAuth2. Mas se você está construindo o login da sua própria aplicação, o que você precisa é de autenticação baseada em JWT, que pode (ou não) usar o fluxo do OAuth2 internamente.

Este artigo cobre os dois cenários mais comuns no mercado brasileiro:

  • Cenário A: Backend PHP rodando em host compartilhado (sem Redis, sem Node, sem Docker)
  • Cenário B: Frontend JavaScript (React, Vue, Angular ou vanilla) consumindo qualquer API com JWT

O que o JWT é — e o que ele não é

Um JWT é um token assinado digitalmente. É composto por três partes separadas por pontos:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3MTQwMDE2MDB9.xK9...
     HEADER                        PAYLOAD                    SIGNATURE
  • Header: algoritmo de assinatura (HS256, RS256)
  • Payload: dados do usuário (sub, exp, roles) — não criptografado, apenas encoded em base64
  • Signature: garante que o token não foi adulterado

O que isso significa na prática:

  • O payload é legível por qualquer pessoa que tenha o token — nunca coloque senha, número de cartão ou dados sensíveis
  • A assinatura garante integridade, não confidencialidade
  • Um JWT válido não pode ser invalidado antes de expirar — a menos que você implemente um mecanismo extra

Cenário A: PHP em host compartilhado

Host compartilhado significa: sem Redis, sem processos persistentes, sem Node.js. Apenas PHP + MySQL (ou outro banco) + Apache/Nginx. É a realidade de milhares de projetos no Brasil.

A boa notícia: JWT funciona bem nesse ambiente. A compensação: revogação de tokens requer o banco de dados.

Instalação

composer require firebase/php-jwt

Se não tiver Composer no host, baixe o autoloader manualmente ou copie os arquivos da biblioteca. Em 2025, praticamente todos os hosts compartilhados respeitáveis suportam Composer via SSH.

Estrutura do projeto

api/
├── config/
│   └── jwt.php          # constantes e configuração
├── src/
│   ├── JwtHelper.php    # geração e verificação de tokens
│   └── AuthMiddleware.php # proteção de rotas
├── routes/
│   ├── login.php
│   ├── refresh.php
│   └── logout.php
└── .env                 # nunca commitar no Git

Configuração (.env e config/jwt.php)

# .env — nunca vá para o repositório
JWT_SECRET=gere_uma_chave_forte_com_no_minimo_32_caracteres_aqui
JWT_ACCESS_TTL=900       # 15 minutos em segundos
JWT_REFRESH_TTL=604800   # 7 dias em segundos
DB_HOST=localhost
DB_NAME=meu_banco
DB_USER=usuario
DB_PASS=senha
<?php
// config/jwt.php
// Carrega o .env de forma simples, sem biblioteca
$envFile = __DIR__ . '/../.env';
if (file_exists($envFile)) {
    foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
        if (strpos(trim($line), '#') === 0) continue;
        [$key, $value] = explode('=', $line, 2);
        $_ENV[trim($key)] = trim($value);
    }
}

define('JWT_SECRET',      $_ENV['JWT_SECRET']      ?? '');
define('JWT_ACCESS_TTL',  (int)($_ENV['JWT_ACCESS_TTL']  ?? 900));
define('JWT_REFRESH_TTL', (int)($_ENV['JWT_REFRESH_TTL'] ?? 604800));

if (empty(JWT_SECRET) || strlen(JWT_SECRET) < 32) {
    http_response_code(500);
    die(json_encode(['error' => 'Configuração de segurança inválida']));
}

JwtHelper.php — geração e verificação

<?php
// src/JwtHelper.php

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;

class JwtHelper
{
    /**
     * Gera um access token de vida curta (15 minutos por padrão).
     * Inclui apenas o necessário no payload — sem dados sensíveis.
     */
    public static function generateAccessToken(int $userId, string $role = 'user'): string
    {
        $now = time();

        $payload = [
            'iss' => $_SERVER['HTTP_HOST'] ?? 'api',  // quem emitiu
            'sub' => (string)$userId,                  // subject: ID do usuário
            'iat' => $now,                             // emitido em
            'exp' => $now + JWT_ACCESS_TTL,            // expira em
            'jti' => bin2hex(random_bytes(8)),         // ID único do token
            'role' => $role,
        ];

        return JWT::encode($payload, JWT_SECRET, 'HS256');
    }

    /**
     * Verifica e decodifica um access token.
     * Retorna o payload ou lança exceção.
     */
    public static function verifyAccessToken(string $token): object
    {
        // Exceções específicas para respostas diferentes ao cliente
        return JWT::decode($token, new Key(JWT_SECRET, 'HS256'));
    }

    /**
     * Extrai o token do header Authorization: Bearer <token>
     */
    public static function extractFromHeader(): ?string
    {
        $header = $_SERVER['HTTP_AUTHORIZATION']
            ?? apache_request_headers()['Authorization']
            ?? null;

        if (!$header || !str_starts_with($header, 'Bearer ')) {
            return null;
        }

        return substr($header, 7);
    }
}

Tabela de refresh tokens no banco

-- Rodando no MySQL do host compartilhado
CREATE TABLE refresh_tokens (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    token       VARCHAR(64) NOT NULL UNIQUE,   -- hash SHA-256 do token real
    user_id     INT NOT NULL,
    expires_at  DATETIME NOT NULL,
    revoked     TINYINT(1) NOT NULL DEFAULT 0,
    created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_token (token),
    INDEX idx_user  (user_id),
    INDEX idx_expires (expires_at)
);

Por que guardar o hash e não o token? Se o banco vazar, o atacante não terá os tokens reais — apenas hashes irreversíveis. O token original fica apenas no cookie do usuário.

routes/login.php

<?php
// routes/login.php

require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../config/jwt.php';
require_once __DIR__ . '/../src/JwtHelper.php';

header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://meusite.com');  // nunca use * em produção com credenciais
header('Access-Control-Allow-Credentials: true');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit(json_encode(['error' => 'Método não permitido']));
}

$body = json_decode(file_get_contents('php://input'), true);
$email = filter_var($body['email'] ?? '', FILTER_SANITIZE_EMAIL);
$senha = $body['senha'] ?? '';

if (!$email || !$senha) {
    http_response_code(400);
    exit(json_encode(['error' => 'Email e senha obrigatórios']));
}

// Conexão com PDO (mais seguro que mysqli)
try {
    $pdo = new PDO(
        "mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']};charset=utf8mb4",
        $_ENV['DB_USER'],
        $_ENV['DB_PASS'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
    );
} catch (PDOException $e) {
    http_response_code(500);
    exit(json_encode(['error' => 'Erro interno']));
}

// Busca o usuário pelo email
$stmt = $pdo->prepare('SELECT id, senha_hash, role FROM usuarios WHERE email = ? AND ativo = 1 LIMIT 1');
$stmt->execute([$email]);
$usuario = $stmt->fetch(PDO::FETCH_ASSOC);

// Verifica credenciais — tempo constante para evitar timing attack
if (!$usuario || !password_verify($senha, $usuario['senha_hash'])) {
    // Sempre retorna a mesma mensagem para não revelar se o email existe
    http_response_code(401);
    exit(json_encode(['error' => 'Credenciais inválidas']));
}

// Gera os tokens
$accessToken  = JwtHelper::generateAccessToken($usuario['id'], $usuario['role']);
$refreshToken = bin2hex(random_bytes(32));  // 64 chars, opaco
$tokenHash    = hash('sha256', $refreshToken);
$expiresAt    = date('Y-m-d H:i:s', time() + JWT_REFRESH_TTL);

// Salva o refresh token no banco
$stmt = $pdo->prepare(
    'INSERT INTO refresh_tokens (token, user_id, expires_at) VALUES (?, ?, ?)'
);
$stmt->execute([$tokenHash, $usuario['id'], $expiresAt]);

// Refresh token em cookie httpOnly (não acessível via JavaScript — protege de XSS)
setcookie('refresh_token', $refreshToken, [
    'expires'  => time() + JWT_REFRESH_TTL,
    'path'     => '/api/auth',         // restrito apenas às rotas de auth
    'secure'   => true,                // apenas HTTPS
    'httponly' => true,                // JavaScript não acessa
    'samesite' => 'Strict',
]);

// Access token no corpo da resposta (o cliente guarda em memória)
http_response_code(200);
echo json_encode([
    'access_token' => $accessToken,
    'expires_in'   => JWT_ACCESS_TTL,
    'token_type'   => 'Bearer',
]);

routes/refresh.php

<?php
// routes/refresh.php — renova o access token usando o refresh token do cookie

require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../config/jwt.php';
require_once __DIR__ . '/../src/JwtHelper.php';

header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://meusite.com');
header('Access-Control-Allow-Credentials: true');

$refreshToken = $_COOKIE['refresh_token'] ?? null;

if (!$refreshToken) {
    http_response_code(401);
    exit(json_encode(['error' => 'Refresh token ausente']));
}

$tokenHash = hash('sha256', $refreshToken);

try {
    $pdo = new PDO(
        "mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']};charset=utf8mb4",
        $_ENV['DB_USER'], $_ENV['DB_PASS'],
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
    );
} catch (PDOException $e) {
    http_response_code(500);
    exit(json_encode(['error' => 'Erro interno']));
}

// Busca o token e verifica validade
$stmt = $pdo->prepare(
    'SELECT rt.user_id, rt.expires_at, rt.revoked, u.role
     FROM refresh_tokens rt
     JOIN usuarios u ON u.id = rt.user_id
     WHERE rt.token = ? LIMIT 1'
);
$stmt->execute([$tokenHash]);
$record = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$record) {
    http_response_code(401);
    exit(json_encode(['error' => 'Refresh token inválido']));
}

if ($record['revoked']) {
    // Token já foi usado ou revogado — possível roubo de token
    // Boa prática: revogar todos os tokens do usuário por segurança
    $stmt = $pdo->prepare('UPDATE refresh_tokens SET revoked = 1 WHERE user_id = ?');
    $stmt->execute([$record['user_id']]);

    http_response_code(401);
    exit(json_encode(['error' => 'Token revogado. Faça login novamente.']));
}

if (new DateTime() > new DateTime($record['expires_at'])) {
    http_response_code(401);
    exit(json_encode(['error' => 'Refresh token expirado']));
}

// Revoga o token atual (rotação: cada refresh gera um novo token)
$stmt = $pdo->prepare('UPDATE refresh_tokens SET revoked = 1 WHERE token = ?');
$stmt->execute([$tokenHash]);

// Gera novo par
$novoAccessToken  = JwtHelper::generateAccessToken($record['user_id'], $record['role']);
$novoRefreshToken = bin2hex(random_bytes(32));
$novoHash         = hash('sha256', $novoRefreshToken);
$novaExpiracao    = date('Y-m-d H:i:s', time() + JWT_REFRESH_TTL);

$stmt = $pdo->prepare(
    'INSERT INTO refresh_tokens (token, user_id, expires_at) VALUES (?, ?, ?)'
);
$stmt->execute([$novoHash, $record['user_id'], $novaExpiracao]);

setcookie('refresh_token', $novoRefreshToken, [
    'expires'  => time() + JWT_REFRESH_TTL,
    'path'     => '/api/auth',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Strict',
]);

http_response_code(200);
echo json_encode([
    'access_token' => $novoAccessToken,
    'expires_in'   => JWT_ACCESS_TTL,
    'token_type'   => 'Bearer',
]);

src/AuthMiddleware.php — proteção de rotas

<?php
// src/AuthMiddleware.php
// Inclua no topo de qualquer rota protegida: require_once 'src/AuthMiddleware.php';

use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;

function requireAuth(): object
{
    $token = JwtHelper::extractFromHeader();

    if (!$token) {
        http_response_code(401);
        exit(json_encode(['error' => 'Token de acesso ausente']));
    }

    try {
        return JwtHelper::verifyAccessToken($token);
    } catch (ExpiredException $e) {
        http_response_code(401);
        exit(json_encode(['error' => 'Token expirado', 'code' => 'TOKEN_EXPIRED']));
    } catch (SignatureInvalidException $e) {
        http_response_code(401);
        exit(json_encode(['error' => 'Token inválido']));
    } catch (Exception $e) {
        http_response_code(401);
        exit(json_encode(['error' => 'Token inválido']));
    }
}

// Verificação de papel (role)
function requireRole(object $payload, string ...$roles): void
{
    if (!in_array($payload->role ?? '', $roles, true)) {
        http_response_code(403);
        exit(json_encode(['error' => 'Sem permissão para este recurso']));
    }
}
<?php
// Exemplo de rota protegida: api/usuarios.php

require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/config/jwt.php';
require_once __DIR__ . '/src/JwtHelper.php';
require_once __DIR__ . '/src/AuthMiddleware.php';

header('Content-Type: application/json');

// Verifica o token e retorna o payload decodificado
$payload = requireAuth();

// Restringe a rota apenas para admins
// requireRole($payload, 'admin');

// A partir daqui, $payload->sub é o ID do usuário autenticado
echo json_encode([
    'usuario_id' => $payload->sub,
    'role'       => $payload->role,
    'dados'      => ['...'],
]);

Limpeza periódica de tokens expirados

Hosts compartilhados não têm cron próprio facilmente. Use uma limpeza lazy: a cada N requisições, ou crie um endpoint interno disparado pelo seu deploy:

<?php
// src/cleanup.php — chame periodicamente (cron do painel do host, ou via deploy)
// Proteja com uma chave interna para não ser acessível publicamente

$chaveInterna = $_SERVER['HTTP_X_CLEANUP_KEY'] ?? '';
if ($chaveInterna !== ($_ENV['CLEANUP_KEY'] ?? '')) {
    http_response_code(403);
    exit;
}

$stmt = $pdo->prepare(
    'DELETE FROM refresh_tokens WHERE expires_at < NOW() OR (revoked = 1 AND created_at < DATE_SUB(NOW(), INTERVAL 30 DAY))'
);
$stmt->execute();
echo json_encode(['deleted' => $stmt->rowCount()]);

Cenário B: JavaScript puro (React, Vue, Angular, vanilla)

O frontend não é um servidor — ele não emite tokens, apenas os consome. Mas onde e como armazenar o token faz toda a diferença de segurança.

Onde guardar o access token

Recomendação prática: o access token fica em memória (variável JS), e o refresh token fica em cookie httpOnly (enviado automaticamente pelo browser, inacessível ao JavaScript). Ao recarregar a página, o access token some — mas o refresh token no cookie renova tudo automaticamente.

Módulo de autenticação (funciona em qualquer framework)

// src/auth/authService.js
// Framework-agnóstico: funciona com React, Vue, Angular ou vanilla JS

const API_BASE = 'https://meusite.com/api';

// Access token fica apenas em memória — não persiste entre recarregamentos,
// mas isso é intencional: o refresh token no cookie cuida da renovação.
let _accessToken = null;
let _refreshPromise = null; // evita múltiplas chamadas de refresh simultâneas

/**
 * Realiza login e armazena o access token em memória.
 * O refresh token é salvo automaticamente em cookie httpOnly pelo servidor.
 */
export async function login(email, senha) {
  const response = await fetch(`${API_BASE}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include', // necessário para enviar/receber cookies
    body: JSON.stringify({ email, senha }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error || 'Falha no login');
  }

  const data = await response.json();
  _accessToken = data.access_token;

  // Agenda renovação automática antes da expiração
  scheduleRefresh(data.expires_in);

  return data;
}

/**
 * Renova o access token usando o refresh token do cookie.
 * Chamada automática antes do token expirar, ou quando uma requisição
 * retorna 401 com code TOKEN_EXPIRED.
 *
 * _refreshPromise garante que chamadas simultâneas esperem pelo mesmo refresh,
 * em vez de disparar múltiplas requisições de renovação.
 */
export async function refreshAccessToken() {
  if (_refreshPromise) return _refreshPromise;

  _refreshPromise = fetch(`${API_BASE}/auth/refresh`, {
    method: 'POST',
    credentials: 'include', // envia o cookie refresh_token
  })
    .then(async (response) => {
      if (!response.ok) {
        _accessToken = null;
        throw new Error('Sessão expirada. Faça login novamente.');
      }
      const data = await response.json();
      _accessToken = data.access_token;
      scheduleRefresh(data.expires_in);
      return data.access_token;
    })
    .finally(() => {
      _refreshPromise = null;
    });

  return _refreshPromise;
}

/**
 * Faz logout: remove o token da memória e notifica o servidor
 * para revogar o refresh token no banco.
 */
export async function logout() {
  _accessToken = null;

  await fetch(`${API_BASE}/auth/logout`, {
    method: 'POST',
    credentials: 'include',
  }).catch(() => {}); // falha silenciosa — logout local já está feito
}

/**
 * Wrapper para fetch autenticado com renovação automática.
 * Use no lugar de fetch() em qualquer requisição autenticada.
 */
export async function authFetch(url, options = {}) {
  // Tenta renovar se não tiver token em memória (ex: após recarregar a página)
  if (!_accessToken) {
    await refreshAccessToken();
  }

  const makeRequest = async (token) =>
    fetch(url, {
      ...options,
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
        Authorization: `Bearer ${token}`,
      },
    });

  let response = await makeRequest(_accessToken);

  // Se expirou durante a requisição, renova e tenta uma vez mais
  if (response.status === 401) {
    const body = await response.clone().json().catch(() => ({}));
    if (body.code === 'TOKEN_EXPIRED') {
      const newToken = await refreshAccessToken();
      response = await makeRequest(newToken);
    }
  }

  return response;
}

// Agenda renovação automática 60 segundos antes de expirar
let _refreshTimer = null;
function scheduleRefresh(expiresInSeconds) {
  if (_refreshTimer) clearTimeout(_refreshTimer);
  const delay = Math.max((expiresInSeconds - 60) * 1000, 0);
  _refreshTimer = setTimeout(refreshAccessToken, delay);
}

// Verifica se há sessão ativa (para restaurar estado após reload)
export function isAuthenticated() {
  return _accessToken !== null;
}

Integração com React

// src/auth/AuthContext.jsx
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { login, logout, refreshAccessToken, isAuthenticated } from './authService';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser]       = useState(null);
  const [loading, setLoading] = useState(true); // verifica sessão ao carregar

  // Ao montar, tenta restaurar sessão via refresh token no cookie
  useEffect(() => {
    refreshAccessToken()
      .then(() => {
        // Token renovado — considera o usuário autenticado
        // Em produção, decodifique o payload para pegar dados do usuário
        setUser({ authenticated: true });
      })
      .catch(() => {
        setUser(null);
      })
      .finally(() => setLoading(false));
  }, []);

  const handleLogin = useCallback(async (email, senha) => {
    await login(email, senha);
    setUser({ authenticated: true });
  }, []);

  const handleLogout = useCallback(async () => {
    await logout();
    setUser(null);
  }, []);

  if (loading) return <div>Verificando sessão...</div>;

  return (
    <AuthContext.Provider value={{ user, login: handleLogin, logout: handleLogout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth deve estar dentro de AuthProvider');
  return ctx;
}
// Componente de login
import { useState } from 'react';
import { useAuth } from '../auth/AuthContext';

export function LoginForm() {
  const { login } = useAuth();
  const [erro, setErro]       = useState('');
  const [loading, setLoading] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    setErro('');

    const form = new FormData(e.target);
    try {
      await login(form.get('email'), form.get('senha'));
      // redirecione para a página principal
    } catch (err) {
      setErro(err.message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required placeholder="Email" />
      <input name="senha" type="password" required placeholder="Senha" />
      {erro && <p style={{ color: 'red' }}>{erro}</p>}
      <button type="submit" disabled={loading}>
        {loading ? 'Entrando...' : 'Entrar'}
      </button>
    </form>
  );
}

// Rota protegida
import { useAuth } from '../auth/AuthContext';
import { Navigate } from 'react-router-dom';

export function RotaProtegida({ children }) {
  const { user } = useAuth();
  return user ? children : <Navigate to="/login" replace />;
}

Integração com Vue 3

// src/auth/useAuth.js — Composable Vue 3
import { ref, readonly } from 'vue';
import { login, logout, refreshAccessToken } from './authService';

const user = ref(null);
const loading = ref(true);

// Inicializa tentando restaurar sessão
refreshAccessToken()
  .then(() => { user.value = { authenticated: true }; })
  .catch(() => { user.value = null; })
  .finally(() => { loading.value = false; });

export function useAuth() {
  async function handleLogin(email, senha) {
    await login(email, senha);
    user.value = { authenticated: true };
  }

  async function handleLogout() {
    await logout();
    user.value = null;
  }

  return {
    user: readonly(user),
    loading: readonly(loading),
    login: handleLogin,
    logout: handleLogout,
  };
}
// src/router/index.js — guarda de rota Vue Router
import { createRouter } from 'vue-router';
import { useAuth } from '../auth/useAuth';

router.beforeEach((to) => {
  const { user, loading } = useAuth();

  if (to.meta.requiresAuth && !user.value) {
    return { name: 'login', query: { redirect: to.fullPath } };
  }
});

Integração com Angular

// src/app/auth/auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, from } from 'rxjs';
import { tap } from 'rxjs/operators';
import { login, logout, refreshAccessToken } from './authService';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private userSubject = new BehaviorSubject<any>(null);
  user$ = this.userSubject.asObservable();

  async init() {
    try {
      await refreshAccessToken();
      this.userSubject.next({ authenticated: true });
    } catch {
      this.userSubject.next(null);
    }
  }

  async login(email: string, senha: string) {
    await login(email, senha);
    this.userSubject.next({ authenticated: true });
  }

  async logout() {
    await logout();
    this.userSubject.next(null);
  }

  isAuthenticated(): boolean {
    return this.userSubject.value !== null;
  }
}
// src/app/auth/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = () => {
  const auth   = inject(AuthService);
  const router = inject(Router);

  if (auth.isAuthenticated()) return true;
  return router.createUrlTree(['/login']);
};

O que todos os cenários têm em comum

Independente do stack, três regras nunca mudam:

1. Access token de vida curta + refresh token de vida longa

Token de 7 dias em localStorage é a combinação mais perigosa possível. Token de 15 minutos em memória, com renovação automática via cookie httpOnly, é a arquitetura correta.

2. Nunca armazene o access token em localStorage

localStorage persiste entre abas, sessões e recarregamentos. Qualquer script injetado via XSS tem acesso imediato. Memória RAM é limpa ao fechar a aba.

3. O payload do JWT é público

atob(token.split('.')[1]) decodifica o payload em qualquer console de browser. Nunca coloque CPF, email completo, número de cartão ou qualquer dado que não deva ser exposto no payload.

Referências

Tags: jwt, oauth2, php, javascript, react, vue, angular, segurança, autenticação, host-compartilhado