FastAPI Auth em 2026: Auth0 vs Supabase vs Clerk vs Firebase na prática

Published on: 2026-06-27
Post image
pt fastapi autenticacao auth0 supabase clerk firebase jwt python oauth2 rbac jwks webhooks

Toda API em FastAPI chega num ponto em que a pergunta deixa de ser “como deixo isso rápido?” e vira “como o usuário faz login?”. É uma decisão que parece pequena, mas que define custo, latência e até a arquitetura do seu banco pelos próximos anos. Este artigo é um guia prático, com código, para integrar quatro dos provedores de autenticação mais usados num back-end FastAPI — Auth0, Supabase Auth, Clerk e Firebase Authentication.

A ideia é didática e completa: para cada provedor você vai ver como validar o token, como fazer o cadeado aparecer no /docs, como aplicar controle de acesso por papéis (RBAC), como lidar com login social, refresh e webhooks. No fim, um comparativo e critérios para escolher com base na sua arquitetura, não no hype.

Por que autenticação em FastAPI é um problema diferente

O primeiro ponto a entender é que FastAPI não é Django nem Express. Ele é assíncrono por natureza, depende muito de injeção de dependência e gira em torno de modelos Pydantic e da documentação automática do OpenAPI. Quatro coisas importam de verdade:

  • Validação assíncrona: nada de verificação de JWT bloqueando o event loop dentro de um Depends().
  • Integração com o esquema de segurança do OpenAPI: o cadeado tem que aparecer nas rotas protegidas do /docs.
  • Compatibilidade com injeção de dependência: algo como user = Depends(get_current_user) precisa parecer natural.
  • Token, não sessão: API em FastAPI é stateless. O padrão é Bearer token.

Os dois modelos de validação de token

No fundo, todos esses provedores caem em um de dois modelos — e isso explica quase toda a diferença de latência:

  • Validação local com segredo compartilhado (HS256): o token é assinado com um segredo que você controla, então o FastAPI valida tudo localmente, sem chamada de rede. É o caminho clássico do Supabase.
  • Validação por chave pública via JWKS (RS256/ES256): o provedor assina com chave privada e você busca a chave pública num endpoint JWKS. É o caso de Auth0, Clerk, Firebase (e do Supabase com chaves assimétricas). Mais flexível para rotação de chaves, mas exige cache para não pagar rede a cada requisição.

Base comum: a dependência, o cadeado no /docs e o RBAC

Independente do provedor, vale ter um núcleo reutilizável: um modelo de usuário, um esquema de segurança que ativa o cadeado no /docs e uma fábrica de dependências para exigir papéis. Só o get_current_user muda de provedor para provedor; o resto fica igual.

# auth/core.py — building blocks shared by every provider
from pydantic import BaseModel
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer

# HTTPBearer makes the padlock show up on protected routes in /docs.
bearer = HTTPBearer(auto_error=True)

class User(BaseModel):
    id: str
    email: str | None = None
    roles: list[str] = []

# get_current_user is provider-specific (defined in each section below).
# require_roles is reusable across ALL providers.
def require_roles(*allowed: str):
    async def checker(user: "User" = Depends(get_current_user)) -> "User":
        if not set(allowed).intersection(user.roles):
            raise HTTPException(status.HTTP_403_FORBIDDEN, "Insufficient role")
        return user
    return checker

Com isso, proteger uma rota e exigir um papel fica declarativo:

@app.get("/me")
async def me(user: User = Depends(get_current_user)):
    return user

@app.get("/admin")
async def admin_area(user: User = Depends(require_roles("admin"))):
    return {"ok": True, "as": user.email}

Auth0 na prática

O Auth0 usa RS256 com JWKS. O jeito certo é cachear o cliente JWKS, validar audience e issuer, e usar o esquema OAuth2AuthorizationCodeBearer para o botão “Authorize” do /docs funcionar de verdade. O RBAC do Auth0 entrega as permissões no claim permissions.

import jwt
from jwt import PyJWKClient
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2AuthorizationCodeBearer

AUTH0_DOMAIN = "your-tenant.us.auth0.com"
API_AUDIENCE = "https://api.your-app.com"
ISSUER = f"https://{AUTH0_DOMAIN}/"

# Cache signing keys; the client refreshes them when a new 'kid' appears.
jwks = PyJWKClient(f"https://{AUTH0_DOMAIN}/.well-known/jwks.json")

# Powers the "Authorize" button + padlock in /docs.
oauth2 = OAuth2AuthorizationCodeBearer(
    authorizationUrl=f"https://{AUTH0_DOMAIN}/authorize?audience={API_AUDIENCE}",
    tokenUrl=f"https://{AUTH0_DOMAIN}/oauth/token",
    scopes={"openid": "OpenID", "profile": "Profile", "email": "Email"},
)

async def get_current_user(token: str = Depends(oauth2)) -> User:
    try:
        key = jwks.get_signing_key_from_jwt(token).key
        payload = jwt.decode(
            token, key, algorithms=["RS256"],
            audience=API_AUDIENCE, issuer=ISSUER,
        )
    except jwt.PyJWTError:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
    # Auth0 RBAC ships granted permissions in the 'permissions' claim.
    return User(
        id=payload["sub"],
        email=payload.get("email"),
        roles=payload.get("permissions", []),
    )

Para exigir uma permissão (scope) específica, basta uma dependência:

def require_scope(scope: str):
    async def checker(user: User = Depends(get_current_user)) -> User:
        if scope not in user.roles:
            raise HTTPException(status.HTTP_403_FORBIDDEN, f"Missing scope: {scope}")
        return user
    return checker

@app.get("/reports", dependencies=[Depends(require_scope("read:reports"))])
async def reports():
    return {"data": []}

E quando é comunicação máquina-a-máquina (um worker chamando a API), o fluxo é client credentials:

import httpx

async def get_m2m_token() -> str:
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            f"https://{AUTH0_DOMAIN}/oauth/token",
            json={
                "grant_type": "client_credentials",
                "client_id": "YOUR_CLIENT_ID",
                "client_secret": "YOUR_CLIENT_SECRET",
                "audience": API_AUDIENCE,
            },
        )
    resp.raise_for_status()
    return resp.json()["access_token"]

Supabase Auth na prática

O Supabase é o nativo do Postgres: a tabela de usuários é uma tabela Postgres de verdade. O caminho clássico valida o JWT localmente com HS256 — sem chamada de rede — e lê o papel do claim role (ou de app_metadata, se você usar papéis customizados).

import os
import jwt
from fastapi import Depends, HTTPException, status

SUPABASE_JWT_SECRET = os.environ["SUPABASE_JWT_SECRET"]

async def get_current_user(creds = Depends(bearer)) -> User:
    try:
        payload = jwt.decode(
            creds.credentials, SUPABASE_JWT_SECRET,
            algorithms=["HS256"], audience="authenticated",
        )
    except jwt.PyJWTError:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
    meta = payload.get("app_metadata", {})
    return User(
        id=payload["sub"],
        email=payload.get("email"),
        roles=meta.get("roles", [payload.get("role", "authenticated")]),
    )

Projetos mais novos do Supabase podem assinar com chaves assimétricas (ES256/RS256). Aí a validação passa a ser via JWKS, igual ao Auth0 — útil para rotação de chaves sem redeploy:

from jwt import PyJWKClient

SUPABASE_URL = os.environ["SUPABASE_URL"]
jwks = PyJWKClient(f"{SUPABASE_URL}/auth/v1/.well-known/jwks.json")

key = jwks.get_signing_key_from_jwt(token).key
payload = jwt.decode(token, key, algorithms=["ES256", "RS256"], audience="authenticated")

O login (e o refresh) costuma ser feito pelo cliente oficial, inclusive social. No back-end dá para usar o supabase-py para fluxos administrativos ou server-side:

from supabase import create_client

sb = create_client(SUPABASE_URL, os.environ["SUPABASE_ANON_KEY"])

# Email + password
sb.auth.sign_up({"email": "a@b.com", "password": "secret123"})
session = sb.auth.sign_in_with_password({"email": "a@b.com", "password": "secret123"})

# Social login (Google, GitHub, ...): returns the URL to redirect the user to
redirect = sb.auth.sign_in_with_oauth({"provider": "google"})

# Refresh the access token when it expires
fresh = sb.auth.refresh_session(session.session.refresh_token)
access_token = fresh.session.access_token

O grande diferencial: empurrar a permissão para o próprio banco com Row Level Security. O Postgres passa a saber quem é o usuário:

-- Each task belongs to a user from Supabase's auth.users table.
CREATE TABLE tasks (
    id SERIAL PRIMARY KEY,
    user_id UUID REFERENCES auth.users(id),
    title TEXT NOT NULL
);

ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users manage own tasks" ON tasks
FOR ALL USING (auth.uid() = user_id);

No FastAPI, o endpoint fica simples: extrai o user_id do token e consulta direto com SQLAlchemy assíncrono.

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

@app.get("/tasks")
async def list_tasks(user: User = Depends(get_current_user),
                     db: AsyncSession = Depends(get_db)):
    rows = await db.execute(select(Task).where(Task.user_id == user.id))
    return rows.scalars().all()

Clerk na prática

O Clerk usa RS256 com JWKS, e é forte em organizações e multi-tenancy: os tokens trazem org_id e org_role. A validação segue o padrão JWKS, conferindo o issuer.

import os
import jwt
from jwt import PyJWKClient
from fastapi import Depends, HTTPException, status

CLERK_ISSUER = "https://your-app.clerk.accounts.dev"
jwks = PyJWKClient(f"{CLERK_ISSUER}/.well-known/jwks.json")

async def get_current_user(creds = Depends(bearer)) -> User:
    try:
        key = jwks.get_signing_key_from_jwt(creds.credentials).key
        payload = jwt.decode(
            creds.credentials, key, algorithms=["RS256"], issuer=CLERK_ISSUER,
        )
    except jwt.PyJWTError:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid session")
    role = payload.get("org_role")  # e.g. "org:admin"
    return User(
        id=payload["sub"],
        email=payload.get("email"),
        roles=[role] if role else [],
    )

Exigir um papel dentro da organização é uma dependência como qualquer outra:

def require_org_role(role: str):
    async def checker(user: User = Depends(get_current_user)) -> User:
        if role not in user.roles:
            raise HTTPException(status.HTTP_403_FORBIDDEN, "Not allowed in this org")
        return user
    return checker

@app.get("/org/settings",
         dependencies=[Depends(require_org_role("org:admin"))])
async def org_settings():
    return {"ok": True}

O ponto forte do Clerk são os webhooks (para sincronizar usuários no seu banco). A verificação correta usa o Svix — nunca um HMAC feito à mão, que é um buraco de segurança comum:

from fastapi import Request, HTTPException
from svix.webhooks import Webhook, WebhookVerificationError

CLERK_WEBHOOK_SECRET = os.environ["CLERK_WEBHOOK_SECRET"]

@app.post("/webhooks/clerk")
async def clerk_webhook(request: Request):
    body = await request.body()
    try:
        # Clerk signs webhooks with Svix; verify with the official library.
        event = Webhook(CLERK_WEBHOOK_SECRET).verify(body, dict(request.headers))
    except WebhookVerificationError:
        raise HTTPException(status_code=400, detail="Invalid signature")

    if event["type"] == "user.created":
        data = event["data"]
        await upsert_user(data["id"], data.get("email_addresses"))
    return {"received": True}

Firebase Authentication na prática

Com o Firebase, o caminho recomendado é usar o SDK firebase-admin: o verify_id_token já cuida do cache dos certificados do Google e ainda checa revogação. Faz tudo o que você faria à mão, melhor.

import firebase_admin
from firebase_admin import auth, credentials
from fastapi import Depends, HTTPException, status

firebase_admin.initialize_app(credentials.Certificate("service-account.json"))

async def get_current_user(creds = Depends(bearer)) -> User:
    try:
        # verify_id_token handles Google's cert caching AND revocation.
        decoded = auth.verify_id_token(creds.credentials, check_revoked=True)
    except (auth.InvalidIdTokenError, auth.ExpiredIdTokenError,
            auth.RevokedIdTokenError):
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
    return User(
        id=decoded["uid"],
        email=decoded.get("email"),
        roles=decoded.get("roles", []),  # from custom claims
    )

O RBAC no Firebase é feito com custom claims, que viajam dentro do ID token:

# Set roles once (e.g. from an admin task). They ride inside the ID token.
auth.set_custom_user_claims(uid, {"roles": ["admin", "editor"]})
# The user must refresh the ID token for the new claims to take effect.

Login por telefone/SMS é nativo e funciona globalmente, mas os SMS são cobrados à parte — algo a considerar no custo desde o primeiro envio.

Refresh tokens: o padrão que vale para todos

Um ponto que confunde quem vem de sessões: numa API stateless, o back-end FastAPI normalmente não gerencia refresh. O access token é curto (minutos), e quem troca o refresh token por um novo access token é o cliente (SDK do provedor no front-end ou app). O back-end só valida o access token que chega — como nos exemplos acima.

Quando você precisa fazer isso no servidor (um job, um BFF), é uma chamada ao endpoint de token do provedor: no Supabase, refresh_session (mostrado acima); no Auth0/Clerk/Firebase, o próprio SDK expõe o método equivalente. A regra de ouro: nunca guarde refresh tokens em local inseguro e mantenha o access token com vida curta.

Números de referência: latência e custo

Os valores a seguir são de referência (2026) e servem para comparar ordens de grandeza, não como medida exata — a latência real varia conforme máquina, cache e topologia.

Latência de validação de token (P95):

  • Supabase Auth: ~2 ms (HS256 local)
  • Clerk: ~8 ms (RS256 + JWKS em cache)
  • Auth0: ~9 ms (RS256 + JWKS em cache)
  • Firebase: ~12 ms (RS256 + certificado do Google em cache)

Latência de cold start (primeira requisição após o deploy):

  • Supabase Auth: ~2 ms (sem chamada externa)
  • Clerk / Auth0: ~45–52 ms (busca inicial do JWKS)
  • Firebase: ~187 ms (busca do certificado do Google)

Custo mensal estimado a 100 mil MAU:

  • Supabase Auth: ~US$ 25
  • Firebase Auth: ~US$ 275
  • Clerk: ~US$ 1.800
  • Auth0: ~US$ 2.400 a US$ 6.000

A leitura é coerente: para um back-end FastAPI puro, a validação local do Supabase tende a ser mais rápida, mais barata e mais simples. O resto se paga quando você precisa de recursos que só os provedores caros entregam.

Pontos de atenção (onde vale olhar com lupa)

  • Cacheie o JWKS e pré-carregue no startup. O PyJWKClient faz I/O de rede; sem cache, ele bloqueia o event loop. Pré-carregar as chaves no startup mantém o P95 baixo e mata o cold start.
  • Sempre valide audience e issuer. Validar só a assinatura não basta — sem checar aud e iss, um token válido de outro projeto/app pode passar.
  • Verifique webhook do jeito certo. Clerk usa Svix; validar com HMAC manual é inseguro. Sempre use a biblioteca oficial do provedor.
  • O segredo HS256 do Supabase dificulta rotação. Se isso for crítico, migre para as chaves assimétricas (JWKS).
  • Custom claims do Firebase não atualizam na hora. Depois de set_custom_user_claims, o usuário precisa renovar o ID token para os novos papéis valerem.
  • Preço e SMS mudam. Todos os valores são estimativas de 2026 e variam por plano e região; confira a página oficial antes de decidir.

Como decidir na prática

  • Se você já tem Postgres, comece pelo Supabase: validação local, RLS no banco e custo imbatível.
  • Se você tem front-end React/Next.js, o Clerk pode se pagar em semanas economizadas de UI e em multi-tenancy pronto.
  • Se o cliente exige SAML, SCIM ou SOC 2, o Auth0 deixa de ser caro e passa a ser requisito.
  • Se você vive no Google Cloud ou tem app mobile Firebase, o Firebase encaixa e ainda traz login por telefone nativo.
  • Independente da escolha, padronize o get_current_user como uma dependência única e reutilize require_roles. Trocar de provedor vira mudar uma função, não reescrever a API.

Conclusão

O ponto central não é coroar um provedor, e sim deixar explícito o porquê: num back-end FastAPI puro, validação local de JWT vence validação por rede em latência, custo e simplicidade. Quando a sua auth é basicamente “e-mail, login social e alguns papéis”, pagar por SAML e detecção de anomalia é gastar em recurso que você não usa.

A lição madura é alinhar o provedor à arquitetura: quem vive no Postgres ganha com o Supabase; quem vive no React ganha com o Clerk; quem vende para grandes empresas precisa do Auth0; quem está no Google Cloud encaixa o Firebase. Com o get_current_user isolado e o RBAC reutilizável, você mantém a porta aberta para mudar de ideia sem dor. De olho daqui para frente: rode um teste pequeno no seu perfil de carga e confira os preços oficiais no dia da decisão. Às vezes a melhor decisão de infraestrutura é a mais sem-graça: o banco já tem os seus dados — talvez ele possa cuidar dos seus usuários também. 🚀