Introdução

Proteger uma API vai muito além de adicionar um campo de senha a um formulário. Você precisa de um esquema em que o servidor possa confiar que a requisição realmente vem de quem ela afirma ser. Os JSON Web Tokens resolvem isso de forma elegante: o servidor assina um token no momento do login, e as requisições subsequentes carregam esse token como prova de identidade. Sem tabela de sessões, sem estado compartilhado.

O que é JWT?

JSON Web Token (JWT) é um padrão aberto (RFC 7519) para transmitir informações entre partes como um objeto JSON assinado. Por ser assinado — seja com uma chave secreta ou com um par de chaves pública/privada — o servidor consegue verificar que o token não foi adulterado.

Por que usar JWT?

JWTs são compactos o suficiente para caber em um cabeçalho HTTP. São autocontidos: o token carrega a identidade do usuário e quaisquer claims necessários, dispensando consulta ao banco de dados a cada requisição. E por não haver sessão no lado do servidor para manter, a autenticação baseada em JWT escala horizontalmente sem coordenação entre servidores.

Como funciona a Autenticação JWT

  1. O usuário envia as credenciais (usuário e senha).
  2. O servidor as valida e assina um JWT com uma chave secreta.
  3. O servidor retorna o token no corpo da resposta.
  4. O cliente armazena o token (localStorage ou cookie HTTP-only).
  5. Cada requisição subsequente inclui o token no cabeçalho Authorization.
  6. O servidor verifica a assinatura antes de processar a requisição.

Implementando Autenticação JWT em TypeScript

A seguir, uma implementação passo a passo em Node.js com TypeScript.

Pré-requisitos

Node.js instalado e familiaridade básica com TypeScript e Express.js.

Configurando o Projeto

  1. Inicialize o projeto

    mkdir jwt-auth-typescript
    cd jwt-auth-typescript
    npm init -y
  2. Instale as dependências

    npm install express jsonwebtoken bcryptjs cors dotenv
  3. Instale TypeScript e os tipos

    npm install --save-dev typescript @types/express @types/jsonwebtoken @types/bcryptjs @types/cors
  4. Inicialize a configuração do TypeScript

    npx tsc --init

Estrutura do Projeto

jwt-auth-typescript/
├── src/
│   ├── index.ts
│   ├── routes/
│   │   └── auth.ts
│   ├── controllers/
│   │   └── authController.ts
│   ├── models/
│       └── user.ts
├── package.json
├── tsconfig.json
└── .env

Configurando o TypeScript (tsconfig.json)

Certifique-se de que o tsconfig.json inclui:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true
  }
}

Configurando as Variáveis de Ambiente

Crie um arquivo .env no diretório raiz:

PORT=5000
JWT_SECRET=your_jwt_secret_key

Criando o Servidor (index.ts)

import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import authRoutes from './routes/auth';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 5000;

app.use(cors());
app.use(express.json());

app.use('/api/auth', authRoutes);

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Criando o Modelo de Usuário (models/user.ts)

Em uma aplicação real você usaria um banco de dados. Para este exemplo, um array em memória é suficiente para demonstrar o fluxo de autenticação.

interface User {
  id: number;
  username: string;
  password: string;
}

const users: User[] = [];

export default users;
export type { User };

Criando as Rotas de Autenticação (routes/auth.ts)

import { Router } from 'express';
import { register, login, protectedRoute } from '../controllers/authController';

const router = Router();

router.post('/register', register);
router.post('/login', login);
router.get('/protected', protectedRoute);

export default router;

Implementando o Controller (controllers/authController.ts)

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import users, { User } from '../models/user';

const JWT_SECRET = process.env.JWT_SECRET || 'secret_key';

export const register = async (req: Request, res: Response) => {
  const { username, password } = req.body;

  // Check if user exists
  const userExists = users.find((user) => user.username === username);
  if (userExists) {
    return res.status(400).json({ message: 'User already exists' });
  }

  // Hash password
  const hashedPassword = await bcrypt.hash(password, 10);

  // Create user
  const newUser: User = {
    id: users.length + 1,
    username,
    password: hashedPassword,
  };
  users.push(newUser);

  res.status(201).json({ message: 'User registered successfully' });
};

export const login = async (req: Request, res: Response) => {
  const { username, password } = req.body;

  // Find user
  const user = users.find((user) => user.username === username);
  if (!user) {
    return res.status(400).json({ message: 'Invalid credentials' });
  }

  // Check password
  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) {
    return res.status(400).json({ message: 'Invalid credentials' });
  }

  // Sign token
  const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '1h' });

  res.json({ token });
};

export const protectedRoute = (req: Request, res: Response, next: NextFunction) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.sendStatus(401);
  }

  jwt.verify(token, JWT_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    (req as any).user = user;
    next();
  });
};

Protegendo Rotas

Mova a lógica de verificação para um array de middleware para que possa ser reutilizada em diferentes rotas:

export const protectedRoute = [
  (req: Request, res: Response, next: NextFunction) => {
    // Middleware to verify JWT
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
  
    if (!token) {
      return res.sendStatus(401);
    }
  
    jwt.verify(token, JWT_SECRET, (err, user) => {
      if (err) return res.sendStatus(403);
      (req as any).user = user;
      next();
    });
  },
  (req: Request, res: Response) => {
    // Protected route logic
    res.json({ message: 'This is a protected route', user: (req as any).user });
  }
];

Testando a Aplicação

  1. Compile o TypeScript

    npx tsc
  2. Inicie o servidor

    node dist/index.js
  3. Registre um usuário

    POST http://localhost:5000/api/auth/register
    Content-Type: application/json
    
    {
      "username": "testuser",
      "password": "password123"
    }
  4. Faça login

    POST http://localhost:5000/api/auth/login
    Content-Type: application/json
    
    {
      "username": "testuser",
      "password": "password123"
    }

    Você receberá um token JWT na resposta.

  5. Acesse a rota protegida

    GET http://localhost:5000/api/auth/protected
    Authorization: Bearer <your_jwt_token>

    Você deverá receber uma resposta indicando que o acesso foi concedido.

Considerações de Segurança

  • Sempre use HTTPS em produção. Um JWT enviado por HTTP simples pode ser interceptado.
  • Defina tempos de expiração curtos. Uma hora é razoável para tokens de acesso; use refresh tokens para sessões mais longas.
  • Prefira armazenar tokens em cookies HTTP-only, não no localStorage. O localStorage é legível por qualquer JavaScript na página.
  • Valide todas as entradas do usuário para prevenir ataques de injeção.

Conclusão

A autenticação JWT é uma solução prática para APIs sem estado. A implementação acima cobre registro, hash de senha com bcrypt, assinatura e verificação de tokens, e proteção de rotas como middleware. A partir dessa base, os próximos passos naturais são a rotação de refresh tokens e a migração do armazenamento em memória para um banco de dados real.

Leitura Complementar

Compartilhe sua Opinião

Se este conteúdo foi útil ou se você tiver dúvidas, entre em contato pelo Twitter.


Por Samuel Fajreldines, especialista em JavaScript, TypeScript, DevOps e Arquitetura Serverless.