Autenticação Segura: MFA, Passkeys, WebAuthn e Boas Práticas de Sessão
A autenticação é a porta de entrada de qualquer sistema moderno. Protegê-la adequadamente é responsabilidade não apenas técnica, mas também ética. Nos últimos anos, assistimos a uma evolução significativa: saímos da era das senhas simples para um modelo multi-camadas, onde fatores de autenticação diversos trabalham juntos. Este artigo explora essa jornada, desde a autenticação multifator tradicional até as passkeys, tecnologia que promete revolucionar a forma como nos autenticamos. Vamos entender os conceitos, as diferenças reais e como implementar cada um deles de forma segura.
MFA (Multi-Factor Authentication) — Os Pilares Tradicionais
A autenticação multifator é baseada no princípio de que você conhece (algo que sabe), possui (algo que tem) ou é (algo que é). Combinando dois ou mais desses fatores, reduzimos drasticamente o risco de acesso não autorizado, pois um atacante precisaria comprometer múltiplas camadas simultaneamente.
Os Três Fatores de Autenticação
O fator de conhecimento (something you know) é aquilo que apenas você deveria saber: uma senha, um PIN ou respostas a perguntas de segurança. O fator de posse (something you have) envolve um dispositivo físico ou digital que você controla: um telefone, um cartão de segurança ou um token TOTP. O fator biométrico (something you are) utiliza características únicas do seu corpo: impressão digital, reconhecimento facial ou íris. Na prática, a maioria dos sistemas implementa autenticação em dois fatores (2FA), combinando geralmente algo que você sabe com algo que você possui.
TOTP (Time-based One-Time Password)
TOTP é um dos mecanismos MFA mais populares. Ele gera códigos de 6 dígitos que expiram a cada 30 segundos, baseados em um segredo compartilhado entre servidor e cliente. O algoritmo é determinístico: sincronizando o relógio entre ambos os lados, o código pode ser regenerado infinitamente. O grande vantágio é que funciona offline — não requer conexão com internet.
Aqui está uma implementação prática em Python usando a biblioteca pyotp:
import pyotp
import qrcode
from io import BytesIO
class TOTPManager:
def __init__(self):
self.totp = None
self.secret = None
def generate_secret(self):
"""Gera um segredo TOTP único para o usuário"""
self.secret = pyotp.random_base32()
self.totp = pyotp.TOTP(self.secret)
return self.secret
def get_provisioning_uri(self, username, issuer="MeuApp"):
"""Retorna a URI para gerar QR Code"""
if not self.totp:
self.generate_secret()
return self.totp.provisioning_uri(
name=username,
issuer_name=issuer
)
def generate_qr_code(self, uri):
"""Gera imagem PNG do QR Code"""
qr = qrcode.QRCode()
qr.add_data(uri)
qr.make()
img = qr.make_image()
buffer = BytesIO()
img.save(buffer, format='PNG')
return buffer.getvalue()
def verify_token(self, token):
"""Verifica se o token TOTP é válido"""
if not self.totp:
return False
return self.totp.verify(token)
# Uso prático
manager = TOTPManager()
secret = manager.generate_secret()
print(f"Segredo: {secret}")
uri = manager.get_provisioning_uri("usuario@exemplo.com")
print(f"URI para QR Code: {uri}")
# Cliente escaneia o QR Code com seu autenticador
# e digita o código gerado
token = "123456" # Código do autenticador
print(f"Token válido: {manager.verify_token(token)}")
SMS e Email — Fatores Secundários Convenientes (Mas Não Ideais)
SMS e email são convenientes para o usuário, mas apresentam vulnerabilidades. Ataques de SIM swapping permitem que hackers controlem o número de telefone sem acessar fisicamente o dispositivo. Emails também podem ser comprometidos. Ainda assim, são melhores que nada. Se implementar esses canais, sempre prefira TOTP como opção primária e reserve SMS/email como fallback.
import smtplib
from email.mime.text import MIMEText
import secrets
class EmailOTPManager:
def __init__(self, smtp_server, smtp_user, smtp_password):
self.smtp_server = smtp_server
self.smtp_user = smtp_user
self.smtp_password = smtp_password
self.otp_store = {} # Em produção, use banco de dados
def generate_otp(self, email, validity_minutes=10):
"""Gera um OTP único e o armazena com timestamp"""
otp = secrets.randbelow(1000000)
otp = str(otp).zfill(6)
import time
self.otp_store[email] = {
'otp': otp,
'created_at': time.time(),
'validity': validity_minutes * 60
}
return otp
def send_otp(self, email, otp):
"""Envia OTP por email"""
message = MIMEText(f"Seu código de autenticação: {otp}")
message['Subject'] = 'Código de Autenticação'
message['From'] = self.smtp_user
message['To'] = email
with smtplib.SMTP_SSL(self.smtp_server, 465) as server:
server.login(self.smtp_user, self.smtp_password)
server.send_message(message)
def verify_otp(self, email, otp):
"""Verifica se o OTP é válido e não expirou"""
if email not in self.otp_store:
return False
stored = self.otp_store[email]
import time
elapsed = time.time() - stored['created_at']
# Verifica se expirou
if elapsed > stored['validity']:
del self.otp_store[email]
return False
# Verifica o valor
if stored['otp'] != otp:
return False
# Remove o OTP após uso bem-sucedido
del self.otp_store[email]
return True
WebAuthn — A Revolução na Autenticação
WebAuthn é um padrão aberto desenvolvido pelo FIDO Alliance e W3C que fundamenta a autenticação sem senha. Ele usa criptografia assimétrica: a chave privada fica no dispositivo do usuário, e apenas a chave pública é armazenada no servidor. Isso torna praticamente impossível que um servidor comprometido revele as credenciais do usuário, pois ele nunca as vê.
Como WebAuthn Funciona
O fluxo é simples: durante o registro, o navegador se comunica com um autenticador (uma chave de segurança, biometria do dispositivo ou software). O autenticador gera um par de chaves criptográficas. A chave privada nunca sai do dispositivo; apenas a chave pública é enviada ao servidor. Mais tarde, durante a autenticação, o servidor envia um desafio (um número aleatório), o autenticador o assina com a chave privada, e o servidor verifica a assinatura com a chave pública. Nenhuma senha passa pela internet.
Implementando WebAuthn com Python e JavaScript
No backend, usaremos a biblioteca webauthn:
from flask import Flask, jsonify, request
from webauthn import (
generate_registration_data,
verify_registration_response,
generate_authentication_data,
verify_authentication_response,
options_to_json
)
from webauthn.helpers.structs import (
AuthenticatorSelectionCriteria,
ResidentKeyRequirement,
UserVerificationRequirement,
)
import json
import base64
app = Flask(__name__)
app.config['SECRET_KEY'] = 'sua-chave-secreta'
# Simulando um banco de dados
users_db = {}
challenges = {}
@app.route('/api/register/start', methods=['POST'])
def register_start():
"""Inicia o processo de registro WebAuthn"""
data = request.json
username = data.get('username')
email = data.get('email')
# Gera dados de registro
registration_data = generate_registration_data(
rp_id="localhost",
rp_name="Meu App Seguro",
user_id=username.encode(),
user_name=username,
user_display_name=email,
authenticator_selection=AuthenticatorSelectionCriteria(
resident_key=ResidentKeyRequirement.PREFERRED,
user_verification=UserVerificationRequirement.PREFERRED,
)
)
# Armazena o desafio para verificação posterior
challenge = registration_data.challenge.decode()
challenges[username] = challenge
return jsonify({
'options': json.loads(options_to_json(registration_data.credential_creation_options))
})
@app.route('/api/register/complete', methods=['POST'])
def register_complete():
"""Completa o registro WebAuthn"""
data = request.json
username = data.get('username')
try:
# Verifica a resposta do autenticador
verification = verify_registration_response(
credential=data.get('credential'),
expected_challenge=challenges[username].encode(),
expected_rp_id="localhost",
expected_origin="http://localhost:3000",
)
# Armazena a credencial pública
if username not in users_db:
users_db[username] = []
users_db[username].append({
'credential_id': base64.b64encode(verification.credential_id).decode(),
'public_key': base64.b64encode(verification.credential_public_key).decode(),
'sign_count': verification.sign_count
})
del challenges[username]
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 400
@app.route('/api/authenticate/start', methods=['POST'])
def authenticate_start():
"""Inicia o processo de autenticação WebAuthn"""
data = request.json
username = data.get('username')
auth_data = generate_authentication_data(
rp_id="localhost",
rp_name="Meu App Seguro"
)
challenge = auth_data.challenge.decode()
challenges[username] = challenge
return jsonify({
'options': json.loads(options_to_json(auth_data.credential_request_options))
})
@app.route('/api/authenticate/complete', methods=['POST'])
def authenticate_complete():
"""Completa a autenticação WebAuthn"""
data = request.json
username = data.get('username')
try:
user_credentials = users_db.get(username, [])
if not user_credentials:
return jsonify({'error': 'Usuário não encontrado'}), 404
# Em um caso real, você tentaria verificar contra todas as credenciais
credential = user_credentials[0]
verify_authentication_response(
credential=data.get('credential'),
expected_challenge=challenges[username].encode(),
expected_rp_id="localhost",
expected_origin="http://localhost:3000",
credential_public_key=base64.b64decode(credential['public_key']),
credential_current_sign_count=credential['sign_count']
)
del challenges[username]
return jsonify({'success': True, 'message': 'Autenticado com sucesso'})
except Exception as e:
return jsonify({'error': str(e)}), 400
if __name__ == '__main__':
app.run(debug=True)
No frontend (JavaScript):
class WebAuthnManager {
constructor(apiBaseUrl = '/api') {
this.apiBaseUrl = apiBaseUrl;
}
async startRegistration(username, email) {
const response = await fetch(`${this.apiBaseUrl}/register/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email })
});
const { options } = await response.json();
// Converte strings base64 em ArrayBuffers
options.challenge = this.base64ToArrayBuffer(options.challenge);
options.user.id = this.base64ToArrayBuffer(options.user.id);
const attestation = await navigator.credentials.create({ publicKey: options });
return this.completeRegistration(username, attestation);
}
async completeRegistration(username, attestation) {
const credential = {
id: this.arrayBufferToBase64(attestation.id),
rawId: this.arrayBufferToBase64(attestation.rawId),
type: attestation.type,
response: {
clientDataJSON: this.arrayBufferToBase64(attestation.response.clientDataJSON),
attestationObject: this.arrayBufferToBase64(attestation.response.attestationObject)
}
};
const response = await fetch(`${this.apiBaseUrl}/register/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, credential })
});
return response.json();
}
async startAuthentication(username) {
const response = await fetch(`${this.apiBaseUrl}/authenticate/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
const { options } = await response.json();
options.challenge = this.base64ToArrayBuffer(options.challenge);
const assertion = await navigator.credentials.get({ publicKey: options });
return this.completeAuthentication(username, assertion);
}
async completeAuthentication(username, assertion) {
const credential = {
id: this.arrayBufferToBase64(assertion.id),
rawId: this.arrayBufferToBase64(assertion.rawId),
type: assertion.type,
response: {
clientDataJSON: this.arrayBufferToBase64(assertion.response.clientDataJSON),
authenticatorData: this.arrayBufferToBase64(assertion.response.authenticatorData),
signature: this.arrayBufferToBase64(assertion.response.signature)
}
};
const response = await fetch(`${this.apiBaseUrl}/authenticate/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, credential })
});
return response.json();
}
base64ToArrayBuffer(base64) {
const binary_string = atob(base64);
const bytes = new Uint8Array(binary_string.length);
for (let i = 0; i < binary_string.length; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
}
// Uso
const webauthn = new WebAuthnManager();
document.getElementById('register-btn').addEventListener('click', async () => {
const username = document.getElementById('username').value;
const email = document.getElementById('email').value;
const result = await webauthn.startRegistration(username, email);
console.log('Registro:', result);
});
document.getElementById('login-btn').addEventListener('click', async () => {
const username = document.getElementById('username').value;
const result = await webauthn.startAuthentication(username);
console.log('Autenticação:', result);
});
Passkeys — O Futuro Sem Senhas
Passkeys são uma abstração de WebAuthn que tornam a experiência do usuário quase invisível. Ao invés de ter que clicar em botões e autorizar autenticadores, o navegador ou dispositivo gerencia tudo automaticamente. Apple, Google e Microsoft integraram passkeys em seus sistemas operacionais, armazenando-as sincronizadas na nuvem.
Diferenças entre WebAuthn e Passkeys
WebAuthn é o protocolo técnico subjacente; passkeys são a implementação prática desse protocolo com experiência de usuário aprimorada. Uma passkey pode ser sincronizada entre dispositivos (como iCloud Keychain ou Google Password Manager), enquanto uma chave de segurança física não. Passkeys funcionam offline e online, e eliminam completamente a necessidade de senhas — algo que nem mesmo MFA tradicional consegue fazer, pois geralmente requer uma senha como primeiro fator.
A implementação técnica é praticamente idêntica à do WebAuthn apresentada acima. A diferença está em como o navegador e o sistema operacional lidam com o armazenamento e recuperação da chave privada, não em como o servidor valida a autenticação.
Boas Práticas para Implementar Passkeys
Sempre ofereça passkeys como opção primária, com fallbacks para autenticação tradicional. Não force o usuário a registrar passkeys imediatamente; deixe como uma opção aprimorada. Se o usuário perder acesso a todas as suas passkeys, tenha um mecanismo de recuperação (como email de recuperação ou suporte humano). Teste em múltiplos navegadores e dispositivos, pois o suporte ainda está em evolução.
from datetime import datetime, timedelta
from flask import session
class PasskeyRecoveryManager:
def __init__(self, db):
self.db = db
def generate_recovery_code(self, username, quantity=10):
"""Gera códigos de recuperação para caso de perda de passkeys"""
recovery_codes = [secrets.token_urlsafe(16) for _ in range(quantity)]
hashed_codes = [self._hash_code(code) for code in recovery_codes]
self.db.save_recovery_codes(username, {
'codes': hashed_codes,
'created_at': datetime.utcnow(),
'used': [False] * quantity
})
return recovery_codes # Mostrar ao usuário uma única vez
def use_recovery_code(self, username, code):
"""Marca um código de recuperação como usado"""
recovery = self.db.get_recovery_codes(username)
if not recovery:
return False
for i, stored_hash in enumerate(recovery['codes']):
if self._verify_code(code, stored_hash) and not recovery['used'][i]:
recovery['used'][i] = True
self.db.save_recovery_codes(username, recovery)
return True
return False
def get_unused_recovery_codes_count(self, username):
"""Retorna quantidade de códigos não utilizados"""
recovery = self.db.get_recovery_codes(username)
if not recovery:
return 0
return sum(1 for used in recovery['used'] if not used)
@staticmethod
def _hash_code(code):
"""Hasheia o código para armazenamento seguro"""
import hashlib
return hashlib.sha256(code.encode()).hexdigest()
@staticmethod
def _verify_code(code, stored_hash):
"""Verifica se o código corresponde ao hash"""
code_hash = hashlib.sha256(code.encode()).hexdigest()
return code_hash == stored_hash
Gerenciamento Seguro de Sessões
Sessões são o elo entre autenticação e autorização. Uma vez que o usuário se autentica, você precisa manter esse estado de forma segura durante toda a sua interação com o aplicativo. Sessões mal gerenciadas podem anular todos os esforços em autenticação forte.
Sessões vs. Tokens JWT
Historicamente, sessões baseadas em cookies foram o padrão. O servidor armazena um identificador de sessão, envia ao navegador via cookie, e o navegador reenvia automaticamente em cada requisição. Tokens JWT (JSON Web Tokens) são uma alternativa mais recente: o servidor envia um token assinado ao cliente, que o armazena e reenvia manualmente a cada requisição. JWT não requer armazenamento no servidor, o que é vantajoso para APIs distribuídas. No entanto, sessões são mais seguras contra XSS e CSRF quando implementadas corretamente, pois o cookie pode ser marcado como httpOnly.
Implementando Sessões Seguras em Flask
from flask import Flask, request, session, jsonify
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps
import secrets
from datetime import datetime, timedelta
app = Flask(__name__)
app.config['SECRET_KEY'] = secrets.token_hex(32)
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS apenas
app.config['SESSION_COOKIE_HTTPONLY'] = True # Inacessível a JavaScript
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Proteção contra CSRF
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)
# Simulando um banco de dados
users_db = {
'joao': generate_password_hash('senha_segura_123')
}
session_store = {} # Em produção, use Redis ou similar
def require_auth(f):
"""Decorator para proteger rotas autenticadas"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return jsonify({'error': 'Não autenticado'}), 401
session_id = session.get('_session_id')
if session_id not in session_store:
return jsonify({'error': 'Sessão inválida'}), 401
session_data = session_store[session_id]
# Verifica se expirou
if session_data['expires_at'] < datetime.utcnow():
del session_store[session_id]
return jsonify({'error': 'Sessão expirada'}), 401
# Atualiza timestamp de atividade (idle timeout)
session_data['last_activity'] = datetime.utcnow()
return f(*args, **kwargs)
return decorated_function
@app.route('/api/login', methods=['POST'])
def login():
"""Autentica o usuário e cria uma sessão segura"""
data = request.json
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'error': 'Username e password obrigatórios'}), 400
if username not in users_db:
# Não revela se o usuário existe (time-constant comparison)
return jsonify({'error': 'Credenciais inválidas'}), 401
if not check_password_hash(users_db[username], password):
return jsonify({'error': 'Credenciais inválidas'}), 401
# Cria uma sessão segura
session_id = secrets.token_urlsafe(32)
session_store[session_id] = {
'user_id': username,
'created_at': datetime.utcnow(),
'expires_at': datetime.utcnow() + timedelta(hours=1),
'last_activity': datetime.utcnow(),
'ip_address': request.remote_addr,
'user_agent': request.headers.get('User-Agent')
}
# Atribui ao session do Flask
session.permanent = True
session['user_id'] = username
session['_session_id'] = session_id
return jsonify({
'message': 'Login bem-sucedido',
'user_id': username
})
@app.route('/api/protected-resource', methods=['GET'])
@require_auth
def protected_resource():
"""Exemplo de rota protegida"""
return jsonify({
'message': f'Olá, {session["user_id"]}!',
'data': 'Conteúdo sensível'
})
@app.route('/api/logout', methods=['POST'])
@require_auth
def logout():
"""Encerra a sessão do usuário"""
session_id = session.get('_session_id')
if session_id in session_store:
del session_store[session_id]
session.clear()
return jsonify({'message': 'Logout bem-sucedido'})
@app.before_request
def cleanup_expired_sessions():
"""Remove sessões expiradas (executar periodicamente em produção)"""
expired = [
sid for sid, data in session_store.items()
if data['expires_at'] < datetime.utcnow()
]
for sid in expired:
del session_store[sid]
Proteção contra CSRF, XSS e Session Fixation
CSRF (Cross-Site Request Forgery) ocorre quando um site externo força seu navegador a fazer uma requisição autenticada no seu aplicativo. Proteja-se gerando um token CSRF único por sessão e validando-o em requisições que modificam dados. XSS (Cross-Site Scripting) permite injetar JavaScript malicioso; mitigue usando Content Security Policy (CSP), sanitizando inputs e nunca confiando em dados do cliente. Session Fixation é quando um atacante força você a usar uma sessão que ele conhece; sempre regenere o ID da sessão após autenticação bem-sucedida.
from flask_wtf.csrf import CSRFProtect
import html
csrf = CSRFProtect(app)
@app.before_request
def regenerate_session_on_auth():
"""Regenera o ID da sessão após autenticação para evitar session fixation"""
if 'user_id' in session and '_session_regenerated' not in session:
old_session_id = session.get('_session_id')
# Cria nova sessão
new_session_id = secrets.token_urlsafe(32)
if old_session_id and old_session_id in session_store:
session_store[new_session_id] = session_store.pop(old_session_id)
session['_session_id'] = new_session_id
session['_session_regenerated'] = True
@app.route('/api/safe-create', methods=['POST'])
@csrf.protect
@require_auth
def safe_create():
"""Exemplo de POST protegido por CSRF token"""
data = request.json
title = html.escape(data.get('title', '')) # Sanitiza para evitar XSS
return jsonify({'message': f'Criado com sucesso: {title}'})
Timeout de Sessão e Idle Timeout
Defina dois tipos de timeout: um timeout absoluto (a sessão expira independentemente da atividade após X horas) e um idle timeout (a sessão expira se nenhuma atividade for detectada por Y minutos). O idle timeout protege contra dispositivos deixados desbloqueados em locais públicos.
class SessionManager:
ABSOLUTE_TIMEOUT = timedelta(hours=8)
IDLE_TIMEOUT = timedelta(minutes=30)
@staticmethod
def is_session_valid(session_data):
"""Verifica se uma sessão é válida"""
now = datetime.utcnow()
# Timeout absoluto
if now - session_data['created_at'] > SessionManager.ABSOLUTE_TIMEOUT:
return False
# Idle timeout
if now - session_data['last_activity'] > SessionManager.IDLE_TIMEOUT:
return False
return True
Conclusão
Aprendemos que a segurança de autenticação é uma progressão natural: começamos com senhas simples, evoluímos para multifator com TOTP e SMS, depois para WebAuthn com criptografia assimétrica, e agora migramos para passkeys que oferecem segurança e conveniência simultâneas. O segundo ponto importante é que autenticação e sessão são complementares; uma autenticação forte é inútil se as sessões são gerenciadas inadequadamente. Sempre use HTTPS, marque cookies como httpOnly e SameSite, regenere IDs de sessão após autenticação, e implemente timeouts bem definidos. Por fim, escolha a abordagem certa para seu contexto: se você precisa suportar usuários mais convencionais, comece com MFA tradicional; se está construindo um aplicativo moderno, implemente WebAuthn e passkeys desde o início como opção primária, com fallbacks para segurança máxima.