Especialização em Backend com Python & FastAPI • FastAPI & Python 3.12+ • Pydantic v2, SQLModel, Async/Await e Microsserviços


🗺️ Mapa Conceitual do Tópico

flowchart TD
    A["Cliente envia usuário/senha"] --> B["POST /auth/token (OAuth2PasswordRequestForm)"]
    B --> C["Valida credenciais no banco"]
    C -->|"ok"| D["jwt.encode(header.payload.signature)"]
    D --> E["access_token + token_type=bearer"]
    C -->|"falha"| F["401 Unauthorized"]

    G["Requisição protegida"] --> H["Authorization: Bearer <token>"]
    H --> I["OAuth2PasswordBearer extrai o token"]
    I --> J["jwt.decode() valida assinatura e exp"]
    J -->|"válido"| K["Depends(get_current_user)"]
    J -->|"expirado/inválido"| L["401 Unauthorized"]

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style D fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style E fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style F fill:#ffebee,stroke:#e53935,stroke-width:2px
    style J fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style K fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style L fill:#ffebee,stroke:#e53935,stroke-width:2px

🏛️ 1. OAuth2 Password Flow e JWT: autenticação sem estado no servidor

OAuth2 é uma especificação de autorização, não de autenticação — mas o FastAPI a utiliza como esqueleto padronizado para descrever “como o cliente envia credenciais e recebe um token” de um jeito que o Swagger UI entende nativamente (o botão “Authorize” na documentação interativa vem daqui). O fluxo relevante para APIs internas é o Resource Owner Password Credentials (Password Flow): o cliente envia usuário e senha diretamente ao backend, que devolve um token de acesso.

OAuth2PasswordRequestForm: o contrato do login

O FastAPI já fornece a dependência OAuth2PasswordRequestForm, que espera os dados como application/x-www-form-urlencoded (não JSON) com os campos username e password — mantendo compatibilidade com clientes OAuth2 padrão, incluindo o próprio Swagger UI:

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm

app = FastAPI()

@app.post("/auth/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    usuario = autenticar_usuario(form_data.username, form_data.password)
    if not usuario:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Usuário ou senha incorretos",
            headers={"WWW-Authenticate": "Bearer"},
        )
    token = criar_access_token({"sub": usuario.email})
    return {"access_token": token, "token_type": "bearer"}

Anatomia de um JWT

Um JSON Web Token é composto por três segmentos codificados em Base64URL e separados por pontos: header.payload.signature. O header declara o algoritmo (HS256, RS256); o payload carrega as claims — dados sobre o usuário e metadados do token; a signature garante que o payload não foi adulterado. É importante entender que o payload não é criptografado, apenas assinado — qualquer um pode decodificar e ler o conteúdo (em jwt.io, por exemplo). Por isso, dados sensíveis (senha, número de cartão) nunca devem ir dentro de um JWT.

As claims padrão mais usadas: sub (subject — identidade do usuário), exp (expiration — timestamp Unix após o qual o token é rejeitado), iat (issued at — quando foi emitido) e iss (issuer — quem emitiu).

import jwt
from datetime import datetime, timedelta, timezone

SECRET_KEY = "chave-secreta-de-no-minimo-32-bytes-em-producao"
ALGORITHM = "HS256"

def criar_access_token(dados: dict, expira_em: timedelta = timedelta(minutes=15)) -> str:
    payload = dados.copy()
    agora = datetime.now(timezone.utc)
    payload.update({"iat": agora, "exp": agora + expira_em})
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def decodificar_token(token: str) -> dict:
    return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

OAuth2PasswordBearer: extraindo e validando o token em cada rota

OAuth2PasswordBearer(tokenUrl="auth/token") é uma dependência que sabe procurar o header Authorization: Bearer <token>, extrair apenas o token e injetá-lo em qualquer rota — além de documentar automaticamente no OpenAPI qual endpoint emite o token:

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")

async def get_current_user(token: str = Depends(oauth2_scheme)):
    credenciais_invalidas = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Não foi possível validar as credenciais",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = decodificar_token(token)
        email: str | None = payload.get("sub")
        if email is None:
            raise credenciais_invalidas
    except jwt.ExpiredSignatureError:
        raise credenciais_invalidas
    except jwt.InvalidTokenError:
        raise credenciais_invalidas
    return buscar_usuario_por_email(email)

Simétrico (HS256) vs assimétrico (RS256)

Com HS256, a mesma chave secreta assina e valida o token — simples, mas exige que todo serviço que precisa validar o token conheça o segredo, o que é arriscado em arquiteturas de microsserviços. Com RS256, a assinatura usa um par de chaves RSA: o serviço de autenticação assina com a chave privada, e qualquer outro serviço (gateway, microsserviço consumidor) valida com a chave pública, sem nunca ter acesso ao segredo que permitiria forjar tokens. Essa é a escolha padrão quando múltiplos serviços independentes precisam verificar tokens emitidos por uma autoridade central.

Access token curto + refresh token longo

Como o JWT é stateless, revogar um token antes do exp não é trivial (o servidor não guarda estado de sessão para invalidar). A mitigação padrão é usar dois tokens: um access_token de vida curta (5-15 min), usado nas requisições, e um refresh_token de vida longa, usado apenas para obter um novo access_token quando o atual expira. O refresh_token fica sujeito a uma blacklist (em Redis, por exemplo) que permite revogação real em caso de logout ou comprometimento — o custo de consultar essa blacklist é pago só ocasionalmente (na renovação), não em cada requisição autenticada.