JWT em Node.js: access token e refresh token

    JWT (JSON Web Token) é o padrão para autenticação stateless em APIs REST — o servidor não precisa guardar sessões, a identidade do usuário viaja no próprio token. A combinação de access token de curta duração com refresh token rotativo é o padrão atual de segurança para APIs de produção.

    Assinar e verificar JWTs com jose

    jose é a biblioteca JWT recomendada para Node.js moderno (suporta Web Crypto API):

    typescript
    // npm install jose
    
    import { SignJWT, jwtVerify, type JWTPayload } from 'jose'
    
    const ACCESS_SECRET = new TextEncoder().encode(process.env.JWT_ACCESS_SECRET!)
    const REFRESH_SECRET = new TextEncoder().encode(process.env.JWT_REFRESH_SECRET!)
    
    export interface TokenPayload extends JWTPayload {
      userId: string
      email: string
      role: string
    }
    
    // Gerar access token (curta duração — 15 minutos):
    export async function gerarAccessToken(payload: Omit<TokenPayload, keyof JWTPayload>) {
      return new SignJWT(payload)
        .setProtectedHeader({ alg: 'HS256' })
        .setIssuedAt()
        .setExpirationTime('15m')
        .setIssuer('minha-api')
        .setAudience('web-app')
        .sign(ACCESS_SECRET)
    }
    
    // Gerar refresh token (longa duração — 30 dias):
    export async function gerarRefreshToken(userId: string) {
      return new SignJWT({ userId })
        .setProtectedHeader({ alg: 'HS256' })
        .setIssuedAt()
        .setExpirationTime('30d')
        .setJti(crypto.randomUUID())  // JWT ID único para revogação
        .sign(REFRESH_SECRET)
    }
    
    // Verificar e decodificar access token:
    export async function verificarAccessToken(token: string): Promise<TokenPayload> {
      const { payload } = await jwtVerify(token, ACCESS_SECRET, {
        issuer: 'minha-api',
        audience: 'web-app',
      })
      return payload as TokenPayload
    }

    Middleware de autenticação no Express

    Proteger rotas com middleware que valida o JWT:

    typescript
    // middleware/auth.ts
    import { Request, Response, NextFunction } from 'express'
    import { verificarAccessToken, type TokenPayload } from '../lib/jwt'
    
    // Estender o tipo Request para incluir o usuário:
    declare global {
      namespace Express {
        interface Request {
          usuario?: TokenPayload
        }
      }
    }
    
    export async function autenticar(req: Request, res: Response, next: NextFunction) {
      const authHeader = req.headers.authorization
    
      if (!authHeader?.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'Token não fornecido' })
      }
    
      const token = authHeader.slice(7)
    
      try {
        req.usuario = await verificarAccessToken(token)
        next()
      } catch (err) {
        // Distinguir token expirado de token inválido:
        const isExpired = (err as Error).name === 'JWTExpired'
        return res.status(401).json({
          error: isExpired ? 'Token expirado' : 'Token inválido',
          code: isExpired ? 'TOKEN_EXPIRED' : 'TOKEN_INVALID',
        })
      }
    }
    
    // Middleware de autorização por role:
    export function autorizar(...roles: string[]) {
      return (req: Request, res: Response, next: NextFunction) => {
        if (!roles.includes(req.usuario?.role ?? '')) {
          return res.status(403).json({ error: 'Permissão negada' })
        }
        next()
      }
    }
    
    // Usar nas rotas:
    app.get('/api/admin', autenticar, autorizar('admin'), handler)

    Refresh token rotativo com Redis

    Renovar o access token sem exigir novo login, com proteção contra roubo:

    typescript
    // Armazenar refresh tokens no Redis para revogação:
    import { redis } from '../lib/redis'
    
    // Ao fazer login:
    app.post('/api/auth/login', async (req, res) => {
      const usuario = await autenticarUsuario(req.body.email, req.body.password)
    
      const accessToken = await gerarAccessToken({
        userId: usuario.id,
        email: usuario.email,
        role: usuario.role,
      })
      const refreshToken = await gerarRefreshToken(usuario.id)
    
      // Armazenar refresh token no Redis (30 dias):
      const jti = extrairJti(refreshToken)  // decodificar sem verificar para pegar jti
      await redis.setex(`refresh:${jti}`, 30 * 24 * 3600, usuario.id)
    
      // Enviar refresh token como httpOnly cookie (mais seguro que body):
      res.cookie('refresh_token', refreshToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict',
        maxAge: 30 * 24 * 60 * 60 * 1000,
        path: '/api/auth',  // só enviado para rotas de auth
      })
    
      res.json({ accessToken, expiresIn: 900 })
    })
    
    // Renovar access token:
    app.post('/api/auth/refresh', async (req, res) => {
      const refreshToken = req.cookies.refresh_token
      if (!refreshToken) return res.status(401).json({ error: 'Refresh token ausente' })
    
      try {
        const { payload } = await jwtVerify(refreshToken, REFRESH_SECRET)
        const jti = payload.jti!
    
        // Verificar se o token está no Redis (não foi revogado):
        const userId = await redis.get(`refresh:${jti}`)
        if (!userId) return res.status(401).json({ error: 'Refresh token inválido ou revogado' })
    
        // Rotação: deletar o token atual e gerar um novo par:
        await redis.del(`refresh:${jti}`)
    
        const usuario = await buscarUsuario(userId)
        const novoAccessToken = await gerarAccessToken({ userId, email: usuario.email, role: usuario.role })
        const novoRefreshToken = await gerarRefreshToken(userId)
    
        const novoJti = extrairJti(novoRefreshToken)
        await redis.setex(`refresh:${novoJti}`, 30 * 24 * 3600, userId)
    
        res.cookie('refresh_token', novoRefreshToken, { httpOnly: true, secure: true, sameSite: 'strict', path: '/api/auth', maxAge: 30 * 24 * 60 * 60 * 1000 })
        res.json({ accessToken: novoAccessToken, expiresIn: 900 })
      } catch {
        res.status(401).json({ error: 'Refresh token expirado' })
      }
    })

    Logout e revogação de tokens

    Invalidar tokens antes do vencimento natural:

    typescript
    // Logout: revogar refresh token e bloquear access token até expirar
    app.post('/api/auth/logout', autenticar, async (req, res) => {
      const refreshToken = req.cookies.refresh_token
    
      // Revogar refresh token:
      if (refreshToken) {
        try {
          const { payload } = await jwtVerify(refreshToken, REFRESH_SECRET)
          await redis.del(`refresh:${payload.jti}`)
        } catch {
          // Token já expirado — não precisa fazer nada
        }
      }
    
      // Adicionar access token à blacklist até ele expirar naturalmente:
      // (necessário para logout imediato — sem isso, o access token ainda é válido)
      const accessToken = req.headers.authorization!.slice(7)
      const exp = req.usuario!.exp!
      const ttl = exp - Math.floor(Date.now() / 1000)
      if (ttl > 0) {
        await redis.setex(`blacklist:${req.usuario!.jti ?? accessToken}`, ttl, '1')
      }
    
      // Limpar cookie:
      res.clearCookie('refresh_token', { path: '/api/auth' })
      res.json({ message: 'Logout realizado com sucesso' })
    })
    
    // Adicionar verificação de blacklist no middleware:
    export async function autenticar(req, res, next) {
      // ... verificar token ...
      const naBlacklist = await redis.exists(`blacklist:${payload.jti}`)
      if (naBlacklist) return res.status(401).json({ error: 'Token revogado' })
      // ...
    }
    
    // Revogar TODOS os tokens de um usuário (ex: troca de senha):
    async function revogarTodosTokens(userId: string) {
      const keys = await redis.keys(`refresh:*`)
      for (const key of keys) {
        if (await redis.get(key) === userId) await redis.del(key)
      }
    }

    Boas práticas de segurança para JWT

    Erros comuns e como evitá-los:

    typescript
    // ❌ ERRO: usar algoritmo "none" (sem assinatura):
    // Nunca aceite tokens com alg: "none" — desative explicitamente
    
    // ❌ ERRO: segredo fraco:
    // JWT_SECRET=secret → facilmente quebrado por brute force
    // ✅ CORRETO: segredo forte (32+ bytes aleatórios):
    // openssl rand -base64 32
    
    // ❌ ERRO: armazenar dados sensíveis no payload:
    // { userId: '123', senha: 'hash' }  ← payload é base64, não criptografado!
    // O payload de um JWT é VISÍVEL para qualquer um que tenha o token.
    // ✅ CORRETO: apenas dados não-sensíveis + identificadores:
    // { userId: '123', role: 'user', email: 'user@example.com' }
    
    // ❌ ERRO: access token de longa duração:
    // .setExpirationTime('30d')  ← se vazar, atacante tem 30 dias
    // ✅ CORRETO: access token curto + refresh token:
    // .setExpirationTime('15m')  ← janela de exposição mínima
    
    // ❌ ERRO: armazenar JWT no localStorage:
    // localStorage.setItem('token', jwt)  ← vulnerável a XSS
    // ✅ CORRETO: access token em memória (variável JS), refresh em httpOnly cookie
    
    // ❌ ERRO: não verificar issuer e audience:
    // await jwtVerify(token, secret)  ← aceita tokens de outras APIs
    // ✅ CORRETO:
    // await jwtVerify(token, secret, { issuer: 'minha-api', audience: 'web-app' })
    
    // ✅ Rotação automática no cliente (React/Vue):
    // Interceptar 401 com código TOKEN_EXPIRED,
    // chamar /api/auth/refresh, repetir a requisição original,
    // nunca pedir login novamente (seamless refresh)

    $ runstack deploy --plan starter

    Não quer configurar manualmente?

    Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.

    Perguntas frequentes