Uma API sem autenticação é uma porta aberta. Segurança não é um recurso opcional que se adiciona no final — é uma responsabilidade que permeia toda a arquitetura do sistema. Neste artigo veremos os principais mecanismos de autenticação usados em APIs Python modernas: API Keys, JWT, OAuth2, hashing de senhas e boas práticas de segurança que todo desenvolvedor precisa conhecer.
Hashing de Senhas
Nunca armazene senhas em texto puro. Use hashing com sal — bcrypt é o padrão da indústria:
pip install passlib[bcrypt]
from passlib.context import CryptContext
# Contexto de criptografia
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_senha(senha: str) -> str:
"""Gera hash seguro da senha."""
return pwd_context.hash(senha)
def verificar_senha(senha_plana: str, hash_armazenado: str) -> bool:
"""Verifica se a senha corresponde ao hash."""
return pwd_context.verify(senha_plana, hash_armazenado)
# Uso
hash1 = hash_senha("minha_senha_secreta")
hash2 = hash_senha("minha_senha_secreta")
print(hash1) # $2b$12$... — diferente a cada vez
print(hash1 == hash2) # False — sal aleatório
print(verificar_senha("minha_senha_secreta", hash1)) # True
print(verificar_senha("senha_errada", hash1)) # False
O bcrypt é intencionalmente lento — dificulta ataques de força bruta mesmo que o banco de hashes seja comprometido.
Autenticação por API Key
A forma mais simples — adequada para comunicação entre serviços:
from fastapi import FastAPI, Security, HTTPException, status
from fastapi.security import APIKeyHeader, APIKeyQuery
from typing import Annotated
app = FastAPI()
# API Key pode vir no header ou na query string
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
API_KEY_QUERY = APIKeyQuery(name="api_key", auto_error=False)
# Em produção, armazene no banco com hash
CHAVES_VALIDAS = {
"chave-servico-a": {"nome": "Serviço A", "permissoes": ["leitura"]},
"chave-servico-b": {"nome": "Serviço B", "permissoes": ["leitura", "escrita"]},
}
async def verificar_api_key(
header_key: Annotated[str | None, Security(API_KEY_HEADER)] = None,
query_key: Annotated[str | None, Security(API_KEY_QUERY)] = None,
) -> dict:
chave = header_key or query_key
if not chave or chave not in CHAVES_VALIDAS:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="API Key inválida ou ausente.",
headers={"WWW-Authenticate": "ApiKey"}
)
return CHAVES_VALIDAS[chave]
@app.get("/dados", dependencies=[Security(verificar_api_key)])
async def dados_protegidos():
return {"dados": "conteúdo protegido"}
@app.get("/dados-com-contexto")
async def dados_com_contexto(
servico: Annotated[dict, Security(verificar_api_key)]
):
return {
"servico": servico["nome"],
"permissoes": servico["permissoes"],
"dados": "conteúdo personalizado"
}
JWT: JSON Web Tokens
JWT é o padrão mais usado para autenticação stateless em APIs REST:
pip install python-jose[cryptography]
Estrutura de um JWT:
header.payload.signature
eyJhbGci... .eyJzdWIi... .SflKxwRJ...
from jose import JWTError, jwt
from datetime import datetime, timedelta
from typing import Optional
import os
# Configurações
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-nunca-use-em-producao")
ALGORITMO = "HS256"
EXPIRACAO_MINUTOS = 30
def criar_token(dados: dict, expira_em: Optional[timedelta] = None) -> str:
"""Gera um JWT assinado."""
payload = dados.copy()
expiracao = datetime.utcnow() + (expira_em or timedelta(minutes=EXPIRACAO_MINUTOS))
payload.update({"exp": expiracao, "iat": datetime.utcnow()})
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITMO)
def decodificar_token(token: str) -> dict:
"""Decodifica e valida um JWT."""
try:
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITMO])
except JWTError as e:
raise ValueError(f"Token inválido: {e}")
# Tokens de acesso e refresh
def criar_par_tokens(usuario_id: int, email: str) -> dict:
access_token = criar_token(
{"sub": str(usuario_id), "email": email, "tipo": "access"},
expira_em=timedelta(minutes=30)
)
refresh_token = criar_token(
{"sub": str(usuario_id), "tipo": "refresh"},
expira_em=timedelta(days=7)
)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
"expira_em": 1800
}
OAuth2 com FastAPI
FastAPI tem suporte nativo ao fluxo OAuth2 com senha:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel, EmailStr
from passlib.context import CryptContext
from typing import Annotated
from datetime import datetime, timedelta
from jose import JWTError, jwt
import os
app = FastAPI(title="API com OAuth2")
pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2 = OAuth2PasswordBearer(tokenUrl="/auth/login")
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret")
ALGORITMO = "HS256"
# Modelos
class Usuario(BaseModel):
id: int
nome: str
email: str
ativo: bool = True
admin: bool = False
class UsuarioDB(Usuario):
senha_hash: str
class TokenResposta(BaseModel):
access_token: str
refresh_token: str
token_type: str
expira_em: int
class TokenPayload(BaseModel):
sub: str
email: str
admin: bool = False
# Banco em memória
_usuarios: dict = {
1: UsuarioDB(
id=1, nome="Ana Admin", email="ana@email.com",
admin=True,
senha_hash=pwd_ctx.hash("senha123")
),
2: UsuarioDB(
id=2, nome="Bruno User", email="bruno@email.com",
senha_hash=pwd_ctx.hash("senha456")
),
}
# Funções auxiliares
def buscar_por_email(email: str) -> UsuarioDB | None:
return next((u for u in _usuarios.values() if u.email == email), None)
def criar_access_token(usuario: UsuarioDB) -> str:
payload = {
"sub": str(usuario.id),
"email": usuario.email,
"admin": usuario.admin,
"exp": datetime.utcnow() + timedelta(minutes=30)
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITMO)
def criar_refresh_token(usuario_id: int) -> str:
payload = {
"sub": str(usuario_id),
"tipo": "refresh",
"exp": datetime.utcnow() + timedelta(days=7)
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITMO)
# Dependência de autenticação
async def usuario_atual(
token: Annotated[str, Depends(oauth2)]
) -> UsuarioDB:
excecao = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token inválido ou expirado.",
headers={"WWW-Authenticate": "Bearer"}
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITMO])
user_id = int(payload.get("sub", 0))
usuario = _usuarios.get(user_id)
if not usuario or not usuario.ativo:
raise excecao
return usuario
except JWTError:
raise excecao
async def usuario_admin(
usuario: Annotated[UsuarioDB, Depends(usuario_atual)]
) -> UsuarioDB:
if not usuario.admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Requer privilégios de administrador."
)
return usuario
# Endpoints de autenticação
@app.post("/auth/login", response_model=TokenResposta, tags=["Auth"])
async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
usuario = buscar_por_email(form.username)
if not usuario or not pwd_ctx.verify(form.password, usuario.senha_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="E-mail ou senha incorretos.",
headers={"WWW-Authenticate": "Bearer"}
)
if not usuario.ativo:
raise HTTPException(status_code=400, detail="Conta desativada.")
return TokenResposta(
access_token = criar_access_token(usuario),
refresh_token = criar_refresh_token(usuario.id),
token_type = "bearer",
expira_em = 1800
)
@app.post("/auth/refresh", tags=["Auth"])
async def renovar_token(refresh_token: str):
try:
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITMO])
if payload.get("tipo") != "refresh":
raise ValueError("Token não é do tipo refresh.")
user_id = int(payload["sub"])
usuario = _usuarios.get(user_id)
if not usuario:
raise ValueError("Usuário não encontrado.")
return {
"access_token": criar_access_token(usuario),
"token_type": "bearer",
"expira_em": 1800
}
except (JWTError, ValueError) as e:
raise HTTPException(status_code=401, detail=str(e))
# Endpoints protegidos
@app.get("/perfil", response_model=Usuario, tags=["Usuários"])
async def meu_perfil(usuario: Annotated[UsuarioDB, Depends(usuario_atual)]):
return usuario
@app.get("/admin/usuarios", tags=["Admin"])
async def listar_usuarios(
_: Annotated[UsuarioDB, Depends(usuario_admin)]
):
return [
{"id": u.id, "nome": u.nome, "email": u.email, "admin": u.admin}
for u in _usuarios.values()
]
Rate Limiting
Protege a API contra abuso e ataques de força bruta:
pip install slowapi
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi import Request
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.post("/auth/login")
@limiter.limit("5/minute") # máximo 5 tentativas por minuto por IP
async def login_com_limite(request: Request, form: Annotated[OAuth2PasswordRequestForm, Depends()]):
# lógica de login...
pass
@app.get("/dados")
@limiter.limit("100/minute")
async def dados_com_limite(request: Request):
return {"dados": "conteúdo"}
HTTPS e Cabeçalhos de Segurança
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
# Redireciona HTTP → HTTPS em produção
# app.add_middleware(HTTPSRedirectMiddleware)
# Rejeita hosts não autorizados
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["meusite.com", "api.meusite.com", "localhost"]
)
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""Adiciona cabeçalhos de segurança HTTP essenciais."""
async def dispatch(self, request: Request, call_next):
resposta = await call_next(request)
resposta.headers["X-Content-Type-Options"] = "nosniff"
resposta.headers["X-Frame-Options"] = "DENY"
resposta.headers["X-XSS-Protection"] = "1; mode=block"
resposta.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
resposta.headers["Content-Security-Policy"] = "default-src 'self'"
resposta.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return resposta
app.add_middleware(SecurityHeadersMiddleware)
Variáveis de Ambiente e Configuração Segura
from pydantic_settings import BaseSettings
from functools import lru_cache
class Configuracoes(BaseSettings):
# App
nome_app: str = "API Escolar"
versao: str = "1.0.0"
debug: bool = False
ambiente: str = "producao"
# Segurança
secret_key: str
algoritmo_jwt: str = "HS256"
expiracao_access: int = 30 # minutos
expiracao_refresh: int = 10080 # 7 dias em minutos
# Banco
database_url: str
# CORS
origens_permitidas: list[str] = ["http://localhost:3000"]
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
@lru_cache
def get_configuracoes() -> Configuracoes:
return Configuracoes()
# .env
# SECRET_KEY=sua-chave-super-secreta-com-mais-de-32-caracteres
# DATABASE_URL=postgresql://user:senha@localhost/escola
# DEBUG=false
# AMBIENTE=producao
Checklist de Segurança
"""
Checklist de segurança para APIs Python em produção:
✅ Senhas
- Nunca armazene em texto puro
- Use bcrypt, argon2 ou scrypt
- Valide força mínima da senha
✅ Tokens JWT
- Use SECRET_KEY longa e aleatória (mínimo 32 chars)
- Defina expiração curta para access token (15-30 min)
- Use refresh tokens de longa duração separadamente
- Valide algoritmo explicitamente — nunca aceite "none"
✅ Autenticação
- Limite tentativas de login (rate limiting)
- Implemente bloqueio temporário após N falhas
- Use HTTPS em produção — nunca HTTP
✅ Dados
- Valide e sanitize toda entrada do usuário
- Use parâmetros em consultas SQL — nunca concatene strings
- Não exponha stack traces em produção
✅ Configuração
- Segredos em variáveis de ambiente — nunca no código
- Adicione .env ao .gitignore
- Cabeçalhos de segurança HTTP em todas as respostas
✅ Dependências
- Mantenha dependências atualizadas
- Use pip audit para verificar vulnerabilidades
- Revise permissões de cada pacote
"""
Exemplo Completo: Testando a Autenticação
from fastapi.testclient import TestClient
client = TestClient(app)
def test_login_sucesso():
resposta = client.post("/auth/login", data={
"username": "ana@email.com",
"password": "senha123"
})
assert resposta.status_code == 200
dados = resposta.json()
assert "access_token" in dados
assert "refresh_token" in dados
assert dados["token_type"] == "bearer"
def test_login_senha_errada():
resposta = client.post("/auth/login", data={
"username": "ana@email.com",
"password": "errada"
})
assert resposta.status_code == 401
def test_rota_protegida_sem_token():
resposta = client.get("/perfil")
assert resposta.status_code == 401
def test_rota_protegida_com_token():
login = client.post("/auth/login", data={
"username": "ana@email.com",
"password": "senha123"
})
token = login.json()["access_token"]
resposta = client.get(
"/perfil",
headers={"Authorization": f"Bearer {token}"}
)
assert resposta.status_code == 200
assert resposta.json()["email"] == "ana@email.com"
def test_acesso_admin_sem_privilegio():
login = client.post("/auth/login", data={
"username": "bruno@email.com",
"password": "senha456"
})
token = login.json()["access_token"]
resposta = client.get(
"/admin/usuarios",
headers={"Authorization": f"Bearer {token}"}
)
assert resposta.status_code == 403
Resumo
- Nunca armazene senhas em texto puro — use bcrypt via
passlib - API Keys são simples e eficientes para comunicação entre serviços
- JWT permite autenticação stateless — o servidor não precisa armazenar sessões
- Separe access token (curto prazo) de refresh token (longo prazo)
OAuth2PasswordBearerintegra o fluxo OAuth2 nativamente ao FastAPI e ao Swagger- Rate limiting protege contra força bruta e abuso de endpoints sensíveis
- Cabeçalhos de segurança HTTP são a primeira linha de defesa no navegador
- Segredos sempre em variáveis de ambiente — nunca commitados no repositório
Referências e Leituras Complementares
- FastAPI — segurança e autenticação — https://fastapi.tiangolo.com/tutorial/security/
- python-jose — JWT — https://python-jose.readthedocs.io/en/latest/
- passlib — hashing de senhas — https://passlib.readthedocs.io/en/stable/
- OWASP API Security Top 10 — https://owasp.org/www-project-api-security/
- pydantic-settings — https://docs.pydantic.dev/latest/concepts/pydantic_settings/
- slowapi — rate limiting — https://slowapi.readthedocs.io/en/latest/
- RHEE, Maddie; STAMM, Dominic. The Web Application Hacker's Handbook. 2. ed. Wiley, 2011. — fundamentos de segurança em aplicações web.
- PERCIVAL, Harry; GREGORY, Bob. Architecture Patterns with Python. O'Reilly Media, 2020. Cap. 11 — autenticação e autorização em arquiteturas orientadas a evento.