Python Admin

Guia Completo de Autenticação em FastAPI: JWT, OAuth2 e Segurança de Endpoints Já leu

Por que Autenticação é Crítica em APIs Modernas Quando você constrói uma API, está criando um ponto de acesso a dados ou funcionalidades que podem ser sensíveis. Sem autenticação adequada, qualquer pessoa poderia acessar informações de outros usuários, modificar dados críticos ou executar ações não autorizadas. A autenticação resolve o problema fundamental: "quem você é?". A autorização — que vem depois — responde "o que você pode fazer?". Em FastAPI, a segurança não é um add-on bolado ao final do projeto. É um pilar arquitetural que deve estar presente desde o início. O framework oferece ferramentas nativas para implementar padrões modernos como JWT (JSON Web Tokens) e OAuth2, reduzindo drasticamente o risco de vulnerabilidades comuns em APIs REST. JWT e OAuth2: Os Pilares da Autenticação Moderna O que é JWT (JSON Web Token)? Um JWT é um padrão aberto (RFC 7519) que define uma forma compacta e segura de transmitir informações entre partes. Ele é composto por três partes separadas

Por que Autenticação é Crítica em APIs Modernas

Quando você constrói uma API, está criando um ponto de acesso a dados ou funcionalidades que podem ser sensíveis. Sem autenticação adequada, qualquer pessoa poderia acessar informações de outros usuários, modificar dados críticos ou executar ações não autorizadas. A autenticação resolve o problema fundamental: "quem você é?". A autorização — que vem depois — responde "o que você pode fazer?".

Em FastAPI, a segurança não é um add-on bolado ao final do projeto. É um pilar arquitetural que deve estar presente desde o início. O framework oferece ferramentas nativas para implementar padrões modernos como JWT (JSON Web Tokens) e OAuth2, reduzindo drasticamente o risco de vulnerabilidades comuns em APIs REST.

JWT e OAuth2: Os Pilares da Autenticação Moderna

O que é JWT (JSON Web Token)?

Um JWT é um padrão aberto (RFC 7519) que define uma forma compacta e segura de transmitir informações entre partes. Ele é composto por três partes separadas por pontos: header, payload e signature. O header define o tipo de token e o algoritmo de criptografia. O payload contém as claims (informações) que você quer transmitir, como o ID do usuário ou seu email. A signature garante que o token não foi alterado — qualquer mudança no header ou payload invalidaria a signature.

O grande diferencial do JWT é que ele é stateless. Diferente de sessões tradicionais, o servidor não precisa armazenar informações sobre cada token emitido. Quando o cliente envia um JWT, o servidor valida a signature e confia que as informações dentro do token são legítimas. Isso torna aplicações distribuídas muito mais escaláveis.

O que é OAuth2?

OAuth2 é um protocolo de autorização que define como terceiras partes podem obter acesso limitado aos recursos de um usuário sem conhecer sua senha. Ele estabelece fluxos padronizados para diferentes cenários: aplicações web, aplicações mobile, serviços backend-to-backend, etc. O fluxo mais comum em APIs é o Password Flow (ou Resource Owner Password Credentials), onde o cliente envia diretamente username e password para receber um token.

Em FastAPI, combinamos JWT com OAuth2: usamos OAuth2 para o fluxo de autenticação (receber credenciais e emitir um token) e JWT como o formato do token que trafega entre cliente e servidor.

Implementação Prática: Autenticação Completa com JWT e OAuth2

Estrutura Base e Dependências

Começamos importando as ferramentas necessárias do FastAPI e bibliotecas externas:

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel, EmailStr
from passlib.context import CryptContext
from datetime import datetime, timedelta, timezone
from typing import Optional
import jwt
import os

app = FastAPI(title="API Segura com JWT")

# Configurações
SECRET_KEY = os.getenv("SECRET_KEY", "sua-chave-super-secreta-aqui")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# Contexto para hash de senhas
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# Scheme OAuth2
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

Importante: Nunca deixe a SECRET_KEY hardcoded em produção. Use variáveis de ambiente. A SECRET_KEY deve ser uma string longa e aleatória — quanto mais complexa, melhor.

Modelos Pydantic

Define os modelos que representam usuários e tokens:

class Usuario(BaseModel):
    id: int
    username: str
    email: EmailStr
    full_name: Optional[str] = None
    disabled: Optional[bool] = False

class UsuarioInDB(Usuario):
    hashed_password: str

class Token(BaseModel):
    access_token: str
    token_type: str
    expires_in: int

class TokenData(BaseModel):
    username: Optional[str] = None

Funções Auxiliares

Implementamos funções para hash de senhas, validação e criação de tokens:

def hash_password(password: str) -> str:
    """Gera hash bcrypt da senha"""
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Verifica se a senha corresponde ao hash"""
    return pwd_context.verify(plain_password, hashed_password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """Cria um JWT assinado"""
    to_encode = data.copy()

    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)

    to_encode.update({"exp": expire})

    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def decode_token(token: str) -> dict:
    """Decodifica e valida um JWT"""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Token inválido"
            )
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Token expirado"
        )
    except jwt.InvalidTokenError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Token inválido"
        )

Banco de Dados Mock

Para fins didáticos, usamos um dicionário em memória. Em produção, seria um banco de dados real:

# Simulando um banco de dados
fake_users_db = {
    "joao": {
        "id": 1,
        "username": "joao",
        "email": "joao@example.com",
        "full_name": "João Silva",
        "hashed_password": hash_password("senha123"),
        "disabled": False,
    },
    "maria": {
        "id": 2,
        "username": "maria",
        "email": "maria@example.com",
        "full_name": "Maria Santos",
        "hashed_password": hash_password("outrasenha456"),
        "disabled": False,
    }
}

def get_user(username: str) -> Optional[UsuarioInDB]:
    """Busca usuário no 'banco de dados'"""
    if username in fake_users_db:
        user_dict = fake_users_db[username]
        return UsuarioInDB(**user_dict)
    return None

def authenticate_user(username: str, password: str) -> Optional[UsuarioInDB]:
    """Valida credenciais do usuário"""
    user = get_user(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user

Dependências e Endpoints Protegidos

Aqui criamos a dependência que valida o token e protege os endpoints:

async def get_current_user(token: str = Depends(oauth2_scheme)) -> UsuarioInDB:
    """Dependência que valida o token e retorna o usuário atual"""
    payload = decode_token(token)
    username: str = payload.get("sub")
    user = get_user(username)
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Usuário não encontrado"
        )
    return user

async def get_current_active_user(
    current_user: UsuarioInDB = Depends(get_current_user)
) -> UsuarioInDB:
    """Valida se o usuário está ativo"""
    if current_user.disabled:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Usuário desativado"
        )
    return current_user

# Endpoint para login
@app.post("/token", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    """Autentica o usuário e retorna um JWT"""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Username ou senha incorretos",
            headers={"WWW-Authenticate": "Bearer"},
        )

    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username},
        expires_delta=access_token_expires
    )

    return {
        "access_token": access_token,
        "token_type": "bearer",
        "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60
    }

# Endpoint protegido
@app.get("/usuarios/me", response_model=Usuario)
async def read_users_me(current_user: UsuarioInDB = Depends(get_current_active_user)):
    """Retorna informações do usuário autenticado"""
    return current_user

# Outro endpoint protegido
@app.get("/dados-sensiveis")
async def dados_sensiveis(current_user: UsuarioInDB = Depends(get_current_active_user)):
    """Apenas usuários autenticados acessam"""
    return {
        "message": f"Olá {current_user.full_name}!",
        "data": "Informação confidencial apenas para você"
    }

Segurança de Endpoints: Validação, Rate Limiting e Boas Práticas

Validação de Entrada e HTTPS

Toda entrada de dados deve ser validada. Pydantic já faz grande parte do trabalho, mas você pode adicionar validadores customizados:

from pydantic import validator

class UsuarioRegistro(BaseModel):
    username: str
    email: EmailStr
    password: str

    @validator('username')
    def username_alphanumeric(cls, v):
        assert v.isalnum(), 'username deve conter apenas letras e números'
        assert len(v) >= 3, 'username deve ter pelo menos 3 caracteres'
        return v

    @validator('password')
    def password_strength(cls, v):
        if len(v) < 8:
            raise ValueError('Senha deve ter no mínimo 8 caracteres')
        if not any(char.isupper() for char in v):
            raise ValueError('Senha deve conter pelo menos uma letra maiúscula')
        return v

Em produção, sempre use HTTPS. O HTTP expõe tokens em texto plano. Com HTTPS, a comunicação é criptografada, protegendo JWTs em trânsito.

Refresh Tokens e Expiração

Tokens com expiração curta (15-30 minutos) são mais seguros. Para manter a sessão viva sem pedir as credenciais novamente, use refresh tokens:

def create_tokens(username: str) -> dict:
    """Cria access_token e refresh_token"""
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": username, "type": "access"},
        expires_delta=access_token_expires
    )

    refresh_token_expires = timedelta(days=7)
    refresh_token = create_access_token(
        data={"sub": username, "type": "refresh"},
        expires_delta=refresh_token_expires
    )

    return {
        "access_token": access_token,
        "refresh_token": refresh_token,
        "token_type": "bearer"
    }

@app.post("/refresh")
async def refresh_access_token(refresh_token: str):
    """Emite um novo access_token usando o refresh_token"""
    payload = decode_token(refresh_token)
    if payload.get("type") != "refresh":
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Token inválido"
        )

    username = payload.get("sub")
    new_access_token = create_access_token(
        data={"sub": username, "type": "access"},
        expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    return {"access_token": new_access_token, "token_type": "bearer"}

Rate Limiting

Proteja seus endpoints contra ataques de força bruta usando rate limiting:

from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter

@app.post("/token")
@limiter.limit("5/minute")
async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
    """Máximo 5 tentativas por minuto"""
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    # ... resto do código

CORS (Cross-Origin Resource Sharing)

Configure CORS apropriadamente para aceitar requisições apenas de origens confiáveis:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://seu-frontend.com"],  # Específico em produção
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
)

Testando a Segurança

Escreva testes para garantir que sua autenticação funciona corretamente:

from fastapi.testclient import TestClient

client = TestClient(app)

def test_login_sucesso():
    """Testa login com credenciais válidas"""
    response = client.post(
        "/token",
        data={"username": "joao", "password": "senha123"}
    )
    assert response.status_code == 200
    assert "access_token" in response.json()
    assert response.json()["token_type"] == "bearer"

def test_login_falha():
    """Testa login com senha incorreta"""
    response = client.post(
        "/token",
        data={"username": "joao", "password": "senhaerrada"}
    )
    assert response.status_code == 401

def test_endpoint_protegido_sem_token():
    """Testa acesso a endpoint protegido sem token"""
    response = client.get("/usuarios/me")
    assert response.status_code == 403

def test_endpoint_protegido_com_token():
    """Testa acesso a endpoint protegido com token válido"""
    # Primeiro faz login
    login_response = client.post(
        "/token",
        data={"username": "joao", "password": "senha123"}
    )
    token = login_response.json()["access_token"]

    # Acessa endpoint protegido
    response = client.get(
        "/usuarios/me",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 200
    assert response.json()["username"] == "joao"

def test_token_expirado():
    """Testa acesso com token expirado"""
    # Cria um token que expira em 1 segundo
    token = create_access_token(
        data={"sub": "joao"},
        expires_delta=timedelta(seconds=1)
    )

    # Aguarda a expiração
    import time
    time.sleep(2)

    response = client.get(
        "/usuarios/me",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 401

Conclusão

Dominar autenticação em FastAPI significa entender três pontos essenciais: JWT é o formato de token stateless que torna APIs escaláveis, removendo a necessidade de armazenar sessões no servidor; OAuth2 é o protocolo que define fluxos seguros para autenticação, e em FastAPI você o implementa com OAuth2PasswordBearer e OAuth2PasswordRequestForm; e segurança vai além da autenticação — inclui validação de entrada, expiração de tokens, rate limiting, HTTPS e testes. Aplicar esses conceitos desde o início do projeto evita refatorações custosas e vulnerabilidades graves.

Referências


Artigos relacionados