FastAPI com JWT: autenticação OAuth2 segura em Python

Implemente autenticação JWT no FastAPI com OAuth2, hash Argon2, access token, rotas protegidas, escopos, testes e cuidados práticos para produção segura.

23 Sep 2026 12 min de leitura Equipe Python Dev BR

Para autenticar uma API FastAPI com JWT, verifique a senha usando Argon2, emita um access token de curta duração com sub e exp, e valide o Bearer token em uma dependência antes de executar cada rota protegida. Para um projeto simples, FastAPI + PyJWT + pwdlib resolvem o fluxo; para SSO, múltiplos clientes, MFA ou gestão corporativa de usuários, prefira um provedor de identidade compatível com OAuth 2.0/OpenID Connect.

Este guia constrói um login funcional, explica onde o OAuth2 entra, protege endpoints, adiciona autorização por escopo e testa os principais casos de falha. O exemplo é adequado para uma API própria e um cliente confiável. Ele não tenta substituir Keycloak, Auth0, Amazon Cognito, Microsoft Entra ID ou outro serviço de identidade quando o produto exige recursos mais amplos.

JWT, OAuth2 e Bearer token não são a mesma coisa

Os três termos aparecem juntos, mas representam partes diferentes:

TermoFunção no fluxo
OAuth 2.0framework de autorização que define fluxos e uso de tokens
Bearer tokencredencial enviada no cabeçalho Authorization; quem a possui pode usá-la
JWTformato de token com claims e assinatura; OAuth2 não obriga seu uso
OpenID Connectcamada de identidade sobre OAuth2, usada para login e SSO

Neste tutorial, o cliente envia usuário e senha para /token, recebe um JWT e depois chama rotas protegidas com:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

O JWT será assinado, não criptografado. Qualquer pessoa que obtenha o token consegue ler seu payload. Portanto, não coloque senha, CPF, chave de API ou informação sigilosa dentro dele. A assinatura serve para detectar alteração e comprovar que o token foi emitido por quem possui a chave.

Preparar o projeto

Crie um ambiente isolado. Se precisar revisar esse passo, veja o guia de ambiente virtual com venv ou use o uv para gerenciar dependências.

mkdir api-auth
cd api-auth
python -m venv .venv
source .venv/bin/activate  # Linux e macOS
# .venv\Scripts\Activate.ps1  # Windows PowerShell

python -m pip install "fastapi[standard]" pyjwt "pwdlib[argon2]" python-multipart

As dependências têm papéis distintos:

  • fastapi[standard]: framework e servidor ASGI;
  • pyjwt: criação e validação do JWT;
  • pwdlib[argon2]: hash de senha com Argon2;
  • python-multipart: leitura do formulário OAuth2 enviado ao endpoint de token.

Em produção, fixe versões no lockfile e faça atualização controlada. Para configuração por ambiente, use Pydantic Settings em vez de espalhar os.getenv() pelo código.

Configurar segredo e parâmetros do token

Gere um segredo aleatório uma vez:

python -c "import secrets; print(secrets.token_urlsafe(48))"

Salve o valor no gerenciador de segredos da sua plataforma ou em uma variável de ambiente local. Não faça commit da chave. O módulo secrets em Python é apropriado para esse tipo de valor criptograficamente forte.

Crie main.py:

from datetime import datetime, timedelta, timezone
import os

import jwt
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt.exceptions import InvalidTokenError
from pwdlib import PasswordHash
from pydantic import BaseModel, Field

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

SECRET_KEY = os.environ["JWT_SECRET_KEY"]
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 15

password_hash = PasswordHash.recommended()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

Definir ALGORITHM no servidor é importante. A aplicação não deve aceitar cegamente o algoritmo declarado no cabeçalho do token. O segredo também não pode ter um fallback fraco como "secret", pois um erro de configuração transformaria a aplicação em um ambiente inseguro sem aviso.

No terminal, exporte a variável antes de executar:

export JWT_SECRET_KEY='cole-aqui-o-segredo-gerado'
fastapi dev main.py

No Windows PowerShell:

$env:JWT_SECRET_KEY = "cole-aqui-o-segredo-gerado"
fastapi dev main.py

Modelos e repositório de usuários

Para manter o foco no fluxo de autenticação, o exemplo usa um dicionário em memória. Troque essa parte por PostgreSQL ou outro banco no projeto real; o artigo de Python com PostgreSQL ajuda nessa evolução.

Adicione ao arquivo:

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


class TokenData(BaseModel):
    username: str | None = None
    scopes: list[str] = Field(default_factory=list)


class User(BaseModel):
    username: str
    full_name: str
    disabled: bool = False
    scopes: list[str] = []


class UserInDB(User):
    hashed_password: str


fake_users_db = {
    "ana": {
        "username": "ana",
        "full_name": "Ana Souza",
        # Hash gerado apenas para demonstração; gere o seu no ambiente local.
        "hashed_password": password_hash.hash("senha-de-demonstracao"),
        "disabled": False,
        "scopes": ["profile:read", "reports:read"],
    }
}


def get_user(username: str) -> UserInDB | None:
    data = fake_users_db.get(username)
    return UserInDB(**data) if data else None


def authenticate_user(username: str, password: str) -> UserInDB | None:
    user = get_user(username)
    if not user or not password_hash.verify(password, user.hashed_password):
        return None
    return user

A senha nunca é comparada com texto puro armazenado. O banco guarda somente o hash produzido por uma função específica para senhas. Argon2 é deliberadamente caro em memória e processamento, o que dificulta tentativas em massa depois de um vazamento.

O hash não deve ser recriado no carregamento da aplicação como no dicionário didático. Em um sistema real, ele é gerado no cadastro ou na troca de senha e persistido no banco. Para criar um hash local:

python -c "from pwdlib import PasswordHash; print(PasswordHash.recommended().hash('troque-esta-senha'))"

Criar o access token JWT

O claim sub identifica o sujeito do token. exp limita sua validade. Também incluiremos os escopos permitidos:

def create_access_token(
    subject: str,
    scopes: list[str],
    expires_delta: timedelta | None = None,
) -> str:
    now = datetime.now(timezone.utc)
    expires_at = now + (
        expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    payload = {
        "sub": subject,
        "scopes": scopes,
        "iat": now,
        "exp": expires_at,
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

Use datas conscientes de fuso (timezone.utc). Um access token curto reduz a janela de abuso se ele vazar. Quinze minutos é um ponto inicial, não uma regra universal: APIs de alto risco podem usar menos; aplicações internas de baixo risco podem aceitar outra política.

Você também pode incluir iss (emissor), aud (público esperado) e um identificador jti. Se usar esses claims, valide-os explicitamente na decodificação. Evite colocar permissões que mudam com frequência em tokens longos, pois o JWT continuará carregando o estado antigo até expirar.

Criar o endpoint de login

OAuth2PasswordRequestForm lê os campos username, password e scope no formato esperado pelo Swagger UI:

@app.post("/token", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends()) -> Token:
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Usuário ou senha inválidos",
            headers={"WWW-Authenticate": "Bearer"},
        )

    access_token = create_access_token(
        subject=user.username,
        scopes=user.scopes,
    )
    return Token(access_token=access_token, token_type="bearer")

Retornar a mesma mensagem para usuário inexistente e senha incorreta reduz vazamento de informação sobre contas cadastradas. Em uma aplicação exposta, adicione rate limit por IP e por conta, alertas para tentativas repetidas e, quando necessário, MFA.

Esse fluxo de senha é aceitável em exemplos, CLIs próprias e aplicações first-party controladas. Para login social, aplicações de terceiros ou SSO corporativo, use Authorization Code com PKCE por meio de um provedor OpenID Connect. Não peça a senha do Google ou da Microsoft dentro da sua API.

Validar o token e obter o usuário atual

A dependência abaixo extrai o token do cabeçalho, valida assinatura e expiração, lê o sub e confirma se o usuário ainda existe e está ativo:

credentials_exception = HTTPException(
    status_code=status.HTTP_401_UNAUTHORIZED,
    detail="Credenciais inválidas ou expiradas",
    headers={"WWW-Authenticate": "Bearer"},
)


def get_current_user(token: str = Depends(oauth2_scheme)) -> UserInDB:
    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=[ALGORITHM],
        )
        token_data = TokenData(
            username=payload.get("sub"),
            scopes=payload.get("scopes", []),
        )
    except InvalidTokenError as exc:
        raise credentials_exception from exc

    if not token_data.username:
        raise credentials_exception

    user = get_user(token_data.username)
    if not user or user.disabled:
        raise credentials_exception

    return user


@app.get("/me", response_model=User)
def read_current_user(
    current_user: UserInDB = Depends(get_current_user),
) -> User:
    return current_user

Abra http://127.0.0.1:8000/docs, clique em Authorize, informe ana e senha-de-demonstracao e execute GET /me. O Swagger UI solicitará o token e o enviará como Bearer nas chamadas seguintes.

Pelo terminal, o login fica assim:

curl -X POST http://127.0.0.1:8000/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'username=ana&password=senha-de-demonstracao'

Depois, use o valor de access_token:

curl http://127.0.0.1:8000/me \
  -H "Authorization: Bearer SEU_TOKEN"

Adicionar autorização por escopos

Autenticação responde quem é a pessoa. Autorização responde o que ela pode fazer. Uma API que valida o login e libera tudo ainda está incompleta.

Para um projeto pequeno, crie uma dependência que verifica os escopos presentes no usuário atual:

from collections.abc import Callable


def require_scope(scope: str) -> Callable:
    def dependency(
        current_user: UserInDB = Depends(get_current_user),
    ) -> UserInDB:
        if scope not in current_user.scopes:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Escopo necessário: {scope}",
            )
        return current_user

    return dependency


@app.get("/reports")
def list_reports(
    current_user: UserInDB = Depends(require_scope("reports:read")),
):
    return {
        "items": [
            {"month": "2026-08", "status": "closed"},
            {"month": "2026-09", "status": "open"},
        ],
        "requested_by": current_user.username,
    }

Aqui, a fonte atual das permissões é o registro do usuário no servidor, e não apenas o conteúdo do JWT. Isso permite retirar um escopo imediatamente no banco, embora exija consulta a cada requisição. Outra arquitetura confia nos escopos do token até a expiração, ganhando desempenho e aceitando uma janela curta de desatualização. Documente qual estratégia o projeto adotou.

Em sistemas maiores, use o suporte de SecurityScopes do FastAPI ou centralize políticas em uma camada própria. Não espalhe comparações improvisadas de cargo (if role == "admin") em dezenas de endpoints.

Testar login e rotas protegidas

Testes devem cobrir sucesso, senha errada, token ausente, token adulterado, expiração e falta de permissão. Instale o pytest se ainda não estiver no projeto:

python -m pip install pytest httpx

Crie test_auth.py:

from fastapi.testclient import TestClient

from main import app

client = TestClient(app)


def login() -> str:
    response = client.post(
        "/token",
        data={"username": "ana", "password": "senha-de-demonstracao"},
    )
    assert response.status_code == 200
    return response.json()["access_token"]


def test_login_e_consulta_perfil():
    token = login()
    response = client.get(
        "/me",
        headers={"Authorization": f"Bearer {token}"},
    )

    assert response.status_code == 200
    assert response.json()["username"] == "ana"


def test_senha_incorreta_retorna_401():
    response = client.post(
        "/token",
        data={"username": "ana", "password": "errada"},
    )
    assert response.status_code == 401


def test_rota_protegida_sem_token_retorna_401():
    response = client.get("/me")
    assert response.status_code == 401


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

Execute:

pytest -q

Para aprofundar fixtures, parametrização, mocks e cobertura, leia testes com pytest. Evite usar a senha de demonstração fora de testes locais e nunca copie credenciais de produção para a suíte.

Access token e refresh token

Um access token curto melhora segurança, mas força novo login frequente. O refresh token permite obter outro access token sem pedir a senha novamente.

Uma implementação segura costuma seguir estas regras:

  1. access token com duração curta;
  2. refresh token aleatório, longo e de uso restrito ao endpoint de renovação;
  3. armazenamento do hash do refresh token no banco;
  4. rotação: cada renovação invalida o refresh token anterior;
  5. detecção de reutilização, encerrando a família de sessões;
  6. revogação no logout, troca de senha ou incidente;
  7. registro de dispositivo, criação e última utilização quando isso for proporcional ao risco.

Não transforme um refresh token em um JWT válido por meses apenas para evitar persistência. Logout, revogação e resposta a vazamento ficam muito mais difíceis. Para a maioria dos produtos, uma sessão persistida e rotacionada é mais operável.

Não existe resposta única. Há dois modelos comuns:

Bearer token controlado pelo JavaScript

O frontend lê o token e monta o cabeçalho Authorization. É simples para SPA e clientes móveis, mas um XSS pode roubar o token se ele estiver em localStorage ou memória acessível.

O navegador envia o cookie automaticamente, e JavaScript não consegue lê-lo quando HttpOnly está ativo. Configure também Secure e SameSite. Como cookies são enviados automaticamente, você precisa analisar e mitigar CSRF, especialmente em operações de escrita.

Em ambos os casos, XSS continua perigoso: mesmo sem ler o cookie, código malicioso pode executar ações enquanto a sessão está ativa. Use Content Security Policy, escape de saída, dependências revisadas e nenhuma injeção arbitrária de HTML.

HS256, RS256 ou EdDSA?

Para uma API única que emite e verifica seus próprios tokens, HS256 é suficiente se o segredo for forte e bem protegido. Em uma arquitetura com vários serviços, algoritmos assimétricos podem ser melhores:

  • o emissor assina com a chave privada;
  • APIs verificam com a chave pública;
  • serviços verificadores não recebem poder para emitir tokens;
  • a rotação pode ser publicada via JWKS e identificada por kid.

RS256 é amplamente suportado; EdDSA oferece chaves menores e boa segurança quando toda a infraestrutura é compatível. A decisão deve considerar bibliotecas, provedor de identidade, rotação e capacidade operacional, não apenas preferência criptográfica.

Checklist de produção

Antes de publicar uma API com autenticação, revise:

  • HTTPS obrigatório em todos os ambientes públicos;
  • segredo ou chave privada fora do repositório;
  • hash de senha com Argon2 ou política equivalente atual;
  • access token com expiração curta;
  • validação de assinatura, exp, algoritmo e, quando usados, iss e aud;
  • mensagens de login que não enumeram usuários;
  • rate limit e monitoramento de tentativas suspeitas;
  • autorização separada da autenticação;
  • revogação e rotação de refresh token;
  • logs sem senha, token completo ou dados pessoais desnecessários;
  • testes para 401, 403, expiração e token adulterado;
  • procedimento documentado para rotação de chaves;
  • CORS restrito aos clientes realmente permitidos;
  • política de cookie e CSRF definida, se houver autenticação por cookie.

Se a API recebe webhooks, JWT de usuário não substitui a verificação da origem. Use assinatura por mensagem conforme o guia de HMAC para autenticar webhooks. Para uma visão mais ampla de rotas, validação e documentação, consulte também APIs REST com FastAPI.

Quando usar um provedor de identidade

Implemente o fluxo diretamente quando você está estudando, construindo um serviço interno pequeno ou controlando integralmente clientes e requisitos. Considere um provedor de identidade quando precisar de:

  • login social ou corporativo;
  • MFA e recuperação de conta madura;
  • SSO entre aplicações;
  • federação com empresas clientes;
  • gestão centralizada de sessões e dispositivos;
  • conformidade e auditoria mais exigentes;
  • vários backends consumindo tokens do mesmo emissor.

Nesses casos, o FastAPI normalmente atua como resource server: recebe o access token, busca a chave pública do emissor via JWKS e valida assinatura, issuer, audience e escopos. A aplicação deixa cadastro, login e MFA com o provedor, mas continua responsável por autorização de negócio.

Próximo passo

Comece executando o exemplo local, substitua o dicionário por uma tabela de usuários e escreva testes para token expirado e usuário desativado. Depois, escolha conscientemente entre manter a autenticação na própria API ou delegá-la a um provedor OpenID Connect.

O ponto principal não é apenas “fazer o JWT funcionar”. Uma autenticação segura combina senha com hash apropriado, token curto, validação rígida, autorização explícita, possibilidade de revogação e observabilidade sem vazamento de credenciais.

Perguntas frequentes

Como implementar autenticação JWT no FastAPI?

Verifique a senha contra um hash Argon2, gere um JWT assinado com sub e exp, e valide o Bearer token em uma dependência aplicada às rotas protegidas. O cliente envia o token no cabeçalho Authorization: Bearer ....

JWT precisa ser salvo no banco de dados?

O access token normalmente não. Sua assinatura e expiração permitem validação stateless. Refresh tokens, revogações, sessões e eventos de segurança, porém, geralmente precisam de persistência ou lista de bloqueio.

Qual algoritmo usar para assinar JWT no FastAPI?

HS256 atende uma aplicação única que protege bem o segredo. RS256 ou EdDSA facilitam cenários nos quais vários serviços precisam verificar tokens sem receber a chave capaz de emiti-los. Sempre fixe no servidor os algoritmos aceitos.

Onde guardar o token JWT no frontend?

Cookies HttpOnly, Secure e com SameSite adequado reduzem a exposição do token ao JavaScript, mas pedem análise de CSRF. localStorage simplifica o uso de Bearer tokens, porém amplia o impacto de XSS. Escolha conforme a arquitetura e o modelo de ameaça.

OAuth2PasswordBearer implementa OAuth2 completo?

Não. Ele extrai o Bearer token e descreve o esquema na documentação OpenAPI. Login, emissão, renovação, revogação, usuários e políticas de segurança continuam sob responsabilidade da aplicação ou do provedor de identidade.

E
Equipe Python Dev BR

Contribuidor do Python Dev BR

Artigos relacionados