Python

Autenticação e Segurança em APIs Python Já leu

11 min de leitura

Autenticação e Segurança em APIs Python
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

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)
  • OAuth2PasswordBearer integra 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.
Comentários

Mais em Python

Artigo 38 — Processamento de Linguagem Natural (NLP)
Artigo 38 — Processamento de Linguagem Natural (NLP)

Processamento de Linguagem Natural é o campo da IA que lida com texto e fala...

Artigo 51 — Projeto 4: Dashboard Analítico com Dash e Banco de Dados Real
Artigo 51 — Projeto 4: Dashboard Analítico com Dash e Banco de Dados Real

O Projeto 4 é um dashboard analítico completo — uma aplicação web interativa...

Artigo 46 — Logging, Monitoramento e Observabilidade
Artigo 46 — Logging, Monitoramento e Observabilidade

Um sistema em produção que você não consegue observar é uma caixa preta. Quan...