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.
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:
| Termo | Função no fluxo |
|---|---|
| OAuth 2.0 | framework de autorização que define fluxos e uso de tokens |
| Bearer token | credencial enviada no cabeçalho Authorization; quem a possui pode usá-la |
| JWT | formato de token com claims e assinatura; OAuth2 não obriga seu uso |
| OpenID Connect | camada 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:
- access token com duração curta;
- refresh token aleatório, longo e de uso restrito ao endpoint de renovação;
- armazenamento do hash do refresh token no banco;
- rotação: cada renovação invalida o refresh token anterior;
- detecção de reutilização, encerrando a família de sessões;
- revogação no logout, troca de senha ou incidente;
- 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.
Cookie HttpOnly ou localStorage?
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.
Cookie HttpOnly
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,isseaud; - 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.