Pular para o conteúdoPedro Farbo
Lição 12 / 2560 min

Autenticação com JWT

Autenticação com JWT

Autenticação é fundamental para proteger sua API. Vamos implementar um sistema completo com JWT (JSON Web Tokens).

Instalando Dependências

bash
npm install jsonwebtoken bcryptnpm install -D @types/jsonwebtoken @types/bcrypt

Como JWT Funciona

1. Cliente envia credenciais (email/senha)
2. Servidor valida e gera token JWT
3. Cliente armazena token e envia em cada requisição
4. Servidor valida token e autoriza acesso
┌─────────────┐     POST /login      ┌─────────────┐
│   Cliente   │ ──────────────────▶ │   Servidor   │
└─────────────┘   email + senha      └─────────────┘
                                            │
                                            │ Valida
                                            ▼
┌─────────────┐     JWT Token        ┌─────────────┐
│   Cliente   │ ◀────────────────── │   Servidor   │
└─────────────┘                      └─────────────┘
       │
       │ Armazena token
       │
       ▼
┌─────────────┐  GET /api/profile    ┌─────────────┐
│   Cliente   │ ──────────────────▶ │   Servidor   │
└─────────────┘  Authorization:      └─────────────┘
                 Bearer <token>

Configuração JWT

typescript
// src/config/auth.tsexport const authConfig = {  jwt: {    secret: process.env.JWT_SECRET || 'default-secret-change-me',    expiresIn: process.env.JWT_EXPIRES_IN || '7d',    refreshExpiresIn: '30d',  },  bcrypt: {    saltRounds: 10,  },};

Service de Autenticação

typescript
// src/services/auth.service.tsimport { hash, compare } from 'bcrypt';import jwt from 'jsonwebtoken';import { prisma } from '../lib/prisma';import { authConfig } from '../config/auth';import { UnauthorizedError, ConflictError } from '../errors'; interface TokenPayload {  userId: string;  email: string;  role: string;} interface AuthResponse {  user: { id: string; email: string; name: string; role: string };  token: string;  refreshToken: string;} export class AuthService {  async register(data: {    name: string;    email: string;    password: string;  }): Promise<AuthResponse> {    // Verificar se email existe    const existingUser = await prisma.user.findUnique({      where: { email: data.email },    });     if (existingUser) {      throw new ConflictError('Email já cadastrado');    }     // Hash da senha    const hashedPassword = await hash(data.password, authConfig.bcrypt.saltRounds);     // Criar usuário    const user = await prisma.user.create({      data: {        name: data.name,        email: data.email,        password: hashedPassword,      },    });     // Gerar tokens    const tokens = this.generateTokens({      userId: user.id,      email: user.email,      role: user.role,    });     return {      user: {        id: user.id,        email: user.email,        name: user.name,        role: user.role,      },      ...tokens,    };  }   async login(email: string, password: string): Promise<AuthResponse> {    // Buscar usuário    const user = await prisma.user.findUnique({      where: { email },    });     if (!user) {      throw new UnauthorizedError('Credenciais inválidas');    }     // Verificar senha    const validPassword = await compare(password, user.password);     if (!validPassword) {      throw new UnauthorizedError('Credenciais inválidas');    }     // Gerar tokens    const tokens = this.generateTokens({      userId: user.id,      email: user.email,      role: user.role,    });     return {      user: {        id: user.id,        email: user.email,        name: user.name,        role: user.role,      },      ...tokens,    };  }   async refreshToken(refreshToken: string): Promise<{ token: string }> {    try {      const payload = jwt.verify(        refreshToken,        authConfig.jwt.secret      ) as TokenPayload;       const token = jwt.sign(        { userId: payload.userId, email: payload.email, role: payload.role },        authConfig.jwt.secret,        { expiresIn: authConfig.jwt.expiresIn }      );       return { token };    } catch {      throw new UnauthorizedError('Refresh token inválido');    }  }   private generateTokens(payload: TokenPayload) {    const token = jwt.sign(payload, authConfig.jwt.secret, {      expiresIn: authConfig.jwt.expiresIn,    });     const refreshToken = jwt.sign(payload, authConfig.jwt.secret, {      expiresIn: authConfig.jwt.refreshExpiresIn,    });     return { token, refreshToken };  }   verifyToken(token: string): TokenPayload {    return jwt.verify(token, authConfig.jwt.secret) as TokenPayload;  }}

Middleware de Autenticação

typescript
// src/middlewares/auth.middleware.tsimport { Request, Response, NextFunction } from 'express';import { AuthService } from '../services/auth.service';import { UnauthorizedError } from '../errors'; // Extender tipo Requestdeclare global {  namespace Express {    interface Request {      user?: {        userId: string;        email: string;        role: string;      };    }  }} const authService = new AuthService(); export function authenticate(req: Request, res: Response, next: NextFunction) {  const authHeader = req.headers.authorization;   if (!authHeader) {    throw new UnauthorizedError('Token não fornecido');  }   const [type, token] = authHeader.split(' ');   if (type !== 'Bearer' || !token) {    throw new UnauthorizedError('Formato de token inválido');  }   try {    const payload = authService.verifyToken(token);    req.user = payload;    next();  } catch (error) {    throw new UnauthorizedError('Token inválido ou expirado');  }} // Middleware opcional - não bloqueia se não tiver tokenexport function optionalAuth(req: Request, res: Response, next: NextFunction) {  const authHeader = req.headers.authorization;   if (authHeader) {    const [type, token] = authHeader.split(' ');     if (type === 'Bearer' && token) {      try {        req.user = authService.verifyToken(token);      } catch {        // Ignora token inválido      }    }  }   next();}

Controller e Rotas

typescript
// src/controllers/auth.controller.tsimport { Request, Response } from 'express';import { AuthService } from '../services/auth.service'; const authService = new AuthService(); export class AuthController {  async register(req: Request, res: Response) {    const result = await authService.register(req.body);    res.status(201).json({ success: true, data: result });  }   async login(req: Request, res: Response) {    const { email, password } = req.body;    const result = await authService.login(email, password);    res.json({ success: true, data: result });  }   async refresh(req: Request, res: Response) {    const { refreshToken } = req.body;    const result = await authService.refreshToken(refreshToken);    res.json({ success: true, data: result });  }   async me(req: Request, res: Response) {    const user = await prisma.user.findUnique({      where: { id: req.user!.userId },      select: { id: true, email: true, name: true, role: true },    });    res.json({ success: true, data: user });  }}
typescript
// src/routes/auth.routes.tsimport { Router } from 'express';import { AuthController } from '../controllers/auth.controller';import { authenticate } from '../middlewares/auth.middleware';import { validate } from '../middlewares/validate.middleware';import { registerSchema, loginSchema } from '../schemas/auth.schema'; const router = Router();const authController = new AuthController(); router.post('/register', validate({ body: registerSchema }), authController.register);router.post('/login', validate({ body: loginSchema }), authController.login);router.post('/refresh', authController.refresh);router.get('/me', authenticate, authController.me); export default router;

Protegendo Rotas

typescript
// Rotas públicasapp.use('/api/auth', authRoutes);app.use('/api/products', productRoutes); // Listagem pública // Rotas protegidasapp.use('/api/orders', authenticate, orderRoutes);app.use('/api/profile', authenticate, profileRoutes);

Resumo

  • ✅ JWT para autenticação stateless
  • ✅ Bcrypt para hash de senhas
  • ✅ Refresh tokens para renovação
  • ✅ Middleware de autenticação
  • ✅ Proteção de rotas

Próxima aula: Autorização e RBAC! 👥

Gostou do conteúdo? Sua contribuição ajuda a manter tudo online e gratuito!

PIX:0737160d-e98f-4a65-8392-5dba70e7ff3e