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.
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 mantidapassport-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 →