Segurança & Compliance

Como Implementar Autenticação Segura em Plataformas SaaS

JWT, refresh tokens, MFA, SSO SAML — o que implementar em cada fase do seu SaaS e como evitar as vulnerabilidades mais comuns de autenticação.

Everton Tubarao··6 min de leitura

O que vai dar errado se você simplificar demais

Autenticação parece simples no MVP. Um JWT gerado no login, verificado no middleware — pronto. Mas sem refresh tokens rotativos, MFA, e controle de sessão, você abre vulnerabilidades sérias:

  • JWT de longa duração (30 dias) vazado em um log expõe conta por 30 dias sem forma de revogar
  • Sem detecção de reutilização de refresh token, attackers podem manter sessão após o usuário trocar de senha
  • Sem MFA, uma credencial comprometida é suficiente para acesso total

Autenticação segura não é complexa — é um conjunto de decisões claras implementadas com disciplina.


Estrutura de tokens recomendada

Access Token (JWT)

Expiração: 15–60 minutos.

Curto o suficiente para que um token vazado expire rápido. Longo o suficiente para não exigir refresh a cada request.

interface AccessTokenPayload {
  sub: string        // userId
  tid: string        // tenantId
  role: UserRole     // role no tenant
  exp: number        // Unix timestamp de expiração
  iat: number        // Issued at
  jti: string        // Unique token ID (para eventual revogação)
}

Algoritmo: ES256 (assimétrico) ou HS256 (simétrico). ES256 permite validação do token sem expor o secret — melhor para microserviços.

Refresh Token

Expiração: 7–30 dias.

Armazenado no banco de dados (não só no cliente). Isso permite revogação real.

CREATE TABLE refresh_tokens (
  id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id      UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  tenant_id    UUID NOT NULL,
  token_hash   TEXT NOT NULL UNIQUE,  -- hash do token, nunca o token em claro
  expires_at   TIMESTAMPTZ NOT NULL,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  last_used_at TIMESTAMPTZ,
  revoked_at   TIMESTAMPTZ,
  ip_address   INET,
  user_agent   TEXT
);

Rotação de refresh tokens

Cada vez que o refresh token é usado para gerar um novo access token, um novo refresh token é emitido e o antigo é invalidado:

async function refreshAccessToken(oldRefreshToken: string) {
  const tokenHash = hashToken(oldRefreshToken)
  const stored = await db.findRefreshToken(tokenHash)

  // Detecção de reutilização: token já foi usado → possível roubo
  if (stored.revoked_at) {
    await revokeAllUserSessions(stored.user_id)
    throw new SecurityError('Refresh token reuse detected — all sessions revoked')
  }

  // Invalida o token antigo
  await db.revokeRefreshToken(stored.id)

  // Gera par novo
  const newAccessToken = generateAccessToken(stored.user_id, stored.tenant_id)
  const newRefreshToken = await generateAndStoreRefreshToken(stored.user_id, stored.tenant_id)

  return { accessToken: newAccessToken, refreshToken: newRefreshToken }
}

Hashing de senha com Argon2

Nunca MD5, SHA1 ou SHA256 para senhas — são rápidos demais (ataques de força bruta viáveis). Nunca bcrypt com custo baixo. Use Argon2id:

import * as argon2 from 'argon2'

const ARGON2_OPTIONS = {
  type: argon2.argon2id,
  memoryCost: 65536,  // 64MB — inviabiliza GPU attacks
  timeCost: 3,
  parallelism: 4,
}

export async function hashPassword(password: string): Promise<string> {
  return argon2.hash(password, ARGON2_OPTIONS)
}

export async function verifyPassword(hash: string, password: string): Promise<boolean> {
  return argon2.verify(hash, password)
}

MFA (Multi-Factor Authentication)

TOTP (Google Authenticator, Authy)

O método mais simples e universal. O usuário escaneia um QR code e o app gera códigos de 6 dígitos a cada 30 segundos.

import { authenticator } from 'otplib'

// Enrollment: gerar secret e QR code
function generateMFASetup(userId: string, email: string) {
  const secret = authenticator.generateSecret()
  const otpauthUrl = authenticator.keyuri(email, 'SeuSaaS', secret)
  return { secret, otpauthUrl }  // otpauthUrl vira QR code no frontend
}

// Verificação no login
function verifyMFACode(secret: string, userProvidedCode: string): boolean {
  return authenticator.verify({ token: userProvidedCode, secret })
}

Backup codes: gere 8–10 códigos de uso único no enrollment. Se o usuário perder o autenticador, usa um backup code.

function generateBackupCodes(): string[] {
  return Array.from({ length: 10 }, () =>
    crypto.randomBytes(5).toString('hex').toUpperCase()
  )
}

Por tenant (B2B)

Em B2B, admins podem tornar MFA obrigatório para toda a organização:

// Em cada request autenticado
async function enforceSecurityPolicy(userId: string, tenantId: string) {
  const policy = await getTenantSecurityPolicy(tenantId)
  const user = await getUser(userId)

  if (policy.mfa_required && !user.mfa_enabled) {
    throw new SecurityPolicyError('MFA_REQUIRED', 'MFA obrigatório nesta organização')
  }
}

SSO com SAML 2.0 (enterprise)

SSO é pré-requisito para vender para enterprises. Quando um prospecto pede "integração com o Azure AD/Okta", está pedindo SAML 2.0 (ou OIDC).

O fluxo SAML

Usuário → SeuSaaS (Service Provider)
  → Redirect para IdP (Azure AD, Okta, Google Workspace)
  → Usuário autentica no IdP
  → IdP envia SAML Assertion assinada para SeuSaaS
  → SeuSaaS valida assinatura e cria sessão

Implementação prática

Não implemente SAML do zero. Use:

  • @node-saml/node-saml (Node.js) — biblioteca madura e mantida
  • passport-saml — para projetos com Passport.js
  • Clerk ou Auth0 com SAML habilitado — terceirize completamente

Configuração por tenant (cada cliente configura seu próprio IdP):

CREATE TABLE sso_configurations (
  id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id      UUID NOT NULL UNIQUE REFERENCES tenants(id),
  provider       TEXT NOT NULL,           -- 'azure-ad', 'okta', 'google'
  entity_id      TEXT NOT NULL,           -- EntryID do IdP
  sso_url        TEXT NOT NULL,           -- SSO endpoint do IdP
  certificate    TEXT NOT NULL,           -- Certificado X.509 do IdP
  email_attribute TEXT DEFAULT 'email',  -- Atributo SAML que contém o email
  created_at     TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Rate limiting e proteção contra força bruta

import { RateLimiterRedis } from 'rate-limiter-flexible'

const loginLimiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'login_fail',
  points: 10,         // 10 tentativas
  duration: 900,      // em 15 minutos
  blockDuration: 900, // bloqueia por 15 minutos
})

async function login(email: string, password: string, ip: string) {
  try {
    await loginLimiter.consume(`${ip}_${email}`)
  } catch {
    throw new RateLimitError('Muitas tentativas. Tente novamente em 15 minutos.')
  }

  const user = await authenticateUser(email, password)
  if (!user) {
    // Incrementa o contador mesmo em falha
    await loginLimiter.penalty(`${ip}_${email}`, 1)
    throw new AuthError('Credenciais inválidas')
  }

  // Limpa o contador em sucesso
  await loginLimiter.delete(`${ip}_${email}`)
  return createSession(user)
}

Onde usar biblioteca vs. construir próprio

| Funcionalidade | Construa próprio | Use biblioteca/SaaS | |---|---|---| | JWT geração/verificação | ✗ | jose, jsonwebtoken | | Hash de senha | ✗ | argon2, bcrypt | | TOTP (Google Auth) | ✗ | otplib | | SAML 2.0 | ✗ | node-saml, Auth0, Clerk | | Refresh token lifecycle | ✓ (lógica de negócio) | — | | Políticas de segurança por tenant | ✓ | — | | Session management | Depende | Lucia, ou Auth0 |


Precisa de uma revisão da autenticação do seu SaaS ou está implementando do zero?

Solicitar auditoria de segurança → · Segurança por design em SaaS → · Falar com especialista →

Precisa de ajuda com segurança & compliance?

A Codevops transforma ideias em produtos reais. Cuidamos de toda a parte técnica para que você foque no seu negócio. Respondemos em até 12 horas.

Falar com especialista →