Autenticação de APIs com tokens e JWT em Python
Proteja suas APIs com tokens de acesso e JWT. Autenticação stateless explicada na prática.
Por que APIs precisam de autenticação?
Uma API sem autenticação é como uma casa com a porta aberta. Qualquer pessoa pode acessar, modificar ou deletar dados. Em um cenário real, você precisa controlar:
- Quem está acessando: identificar o usuário por trás da requisição
- O que pode acessar: diferentes usuários podem ter diferentes permissões
- Rastreabilidade: saber quem fez o quê e quando
- Proteção contra abusos: limitar requisições por usuário
Sem autenticação, sua API fica vulnerável a acessos indevidos, vazamento de dados e ataques automatizados.
Tipos de autenticação para APIs
Existem várias formas de autenticar uma API. Cada uma tem seus prós e contras:
| Tipo | Como funciona | Quando usar |
|---|---|---|
| API Key | Chave fixa enviada em cada requisição | APIs simples, integrações entre serviços |
| Token (Bearer) | Token gerado após login, enviado no header | APIs com login de usuários |
| JWT | Token autocontido com dados do usuário | APIs stateless, microsserviços |
| OAuth 2.0 | Delegação de acesso via provedor externo | Login com Google, GitHub, etc. |
| Basic Auth | Usuário e senha codificados em Base64 | Testes, APIs internas |
Neste artigo, vamos focar em API Keys e JWT, que são as abordagens mais comuns para APIs Python.
Autenticação com API Key
A forma mais simples de proteger uma API. O cliente envia uma chave fixa no header de cada requisição.
from flask import Flask, jsonify, request
from functools import wraps
app = Flask(__name__)
# em producao, use variaveis de ambiente
API_KEYS = {
"chave-cliente-a-123": "Cliente A",
"chave-cliente-b-456": "Cliente B"
}
def requer_api_key(f):
@wraps(f)
def decorada(*args, **kwargs):
api_key = request.headers.get("X-API-Key")
if not api_key:
return jsonify({"erro": "API key nao fornecida"}), 401
if api_key not in API_KEYS:
return jsonify({"erro": "API key invalida"}), 401
return f(*args, **kwargs)
return decorada
@app.route("/api/dados")
@requer_api_key
def dados_protegidos():
api_key = request.headers.get("X-API-Key")
cliente = API_KEYS[api_key]
return jsonify({"mensagem": f"Acesso autorizado para {cliente}"})
Testando:
# sem api key (401)
curl http://127.0.0.1:5000/api/dados
# com api key valida (200)
curl http://127.0.0.1:5000/api/dados -H "X-API-Key: chave-cliente-a-123"
API Keys são simples, mas têm limitações: não expiram automaticamente, não carregam informações do usuário e, se vazarem, o acesso é comprometido até a troca manual.
O que é JWT?
JWT (JSON Web Token) é um padrão aberto (RFC 7519) para transmitir informações de forma segura entre duas partes. Um token JWT é composto por três partes separadas por pontos:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.DGz5QTk_EaNILM7g19hNLQ
|_____________________||___________________||________________________|
HEADER PAYLOAD ASSINATURA
Header
Contém o algoritmo de assinatura e o tipo do token:
{
"alg": "HS256",
"typ": "JWT"
}
Payload
Contém os dados (claims) que você quer transmitir:
{
"user_id": 1,
"email": "maria@email.com",
"exp": 1711036800
}
Assinatura
É o hash do header + payload usando uma chave secreta. Garante que o token não foi alterado:
HMACSHA256(
base64(header) + "." + base64(payload),
chave_secreta
)
Importante: o payload do JWT é apenas codificado em Base64, não é criptografado. Qualquer pessoa pode ler o conteúdo. A assinatura garante apenas que o token não foi modificado. Nunca coloque senhas ou dados sensíveis no payload.
Instalando o PyJWT
pip install pyjwt
Gerando e verificando tokens:
import jwt
from datetime import datetime, timedelta, timezone
CHAVE_SECRETA = "minha-chave-super-secreta"
# gerar token
payload = {
"user_id": 1,
"email": "maria@email.com",
"exp": datetime.now(timezone.utc) + timedelta(hours=1)
}
token = jwt.encode(payload, CHAVE_SECRETA, algorithm="HS256")
print(token)
# verificar token
try:
dados = jwt.decode(token, CHAVE_SECRETA, algorithms=["HS256"])
print(dados) # {"user_id": 1, "email": "maria@email.com", "exp": ...}
except jwt.ExpiredSignatureError:
print("Token expirado")
except jwt.InvalidTokenError:
print("Token invalido")
O campo exp define quando o token expira. O PyJWT verifica automaticamente e levanta ExpiredSignatureError se o token estiver vencido.
Implementando JWT com Flask
Vamos criar uma API completa com registro, login e rotas protegidas.
Configuração inicial
from flask import Flask, jsonify, request
from functools import wraps
from datetime import datetime, timedelta, timezone
from werkzeug.security import generate_password_hash, check_password_hash
import jwt
app = Flask(__name__)
app.config["SECRET_KEY"] = "troque-por-uma-chave-segura"
# banco em memoria (em producao, use um banco real)
usuarios = []
proximo_id = 1
Rota de registro
@app.route("/api/registro", methods=["POST"])
def registrar():
global proximo_id
dados = request.get_json()
if not dados or "email" not in dados or "senha" not in dados:
return jsonify({"erro": "Email e senha sao obrigatorios"}), 400
# verificar se email ja existe
if any(u["email"] == dados["email"] for u in usuarios):
return jsonify({"erro": "Email ja cadastrado"}), 409
novo_usuario = {
"id": proximo_id,
"email": dados["email"],
"senha_hash": generate_password_hash(dados["senha"]),
"nome": dados.get("nome", "")
}
usuarios.append(novo_usuario)
proximo_id += 1
return jsonify({
"mensagem": "Usuario criado com sucesso",
"id": novo_usuario["id"]
}), 201
Rota de login (gera o token)
@app.route("/api/login", methods=["POST"])
def login():
dados = request.get_json()
if not dados or "email" not in dados or "senha" not in dados:
return jsonify({"erro": "Email e senha sao obrigatorios"}), 400
usuario = next((u for u in usuarios if u["email"] == dados["email"]), None)
if usuario is None or not check_password_hash(usuario["senha_hash"], dados["senha"]):
return jsonify({"erro": "Email ou senha incorretos"}), 401
# gerar token JWT
token = jwt.encode(
{
"user_id": usuario["id"],
"email": usuario["email"],
"exp": datetime.now(timezone.utc) + timedelta(hours=24)
},
app.config["SECRET_KEY"],
algorithm="HS256"
)
return jsonify({"token": token}), 200
Decorador para proteger rotas
def token_obrigatorio(f):
@wraps(f)
def decorada(*args, **kwargs):
token = None
# buscar token no header Authorization
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "):
token = auth_header.split(" ")[1]
if not token:
return jsonify({"erro": "Token nao fornecido"}), 401
try:
dados = jwt.decode(
token,
app.config["SECRET_KEY"],
algorithms=["HS256"]
)
usuario_atual = next(
(u for u in usuarios if u["id"] == dados["user_id"]),
None
)
if usuario_atual is None:
return jsonify({"erro": "Usuario nao encontrado"}), 401
except jwt.ExpiredSignatureError:
return jsonify({"erro": "Token expirado"}), 401
except jwt.InvalidTokenError:
return jsonify({"erro": "Token invalido"}), 401
return f(usuario_atual, *args, **kwargs)
return decorada
Rotas protegidas
@app.route("/api/perfil")
@token_obrigatorio
def perfil(usuario_atual):
return jsonify({
"id": usuario_atual["id"],
"email": usuario_atual["email"],
"nome": usuario_atual["nome"]
})
@app.route("/api/dados-secretos")
@token_obrigatorio
def dados_secretos(usuario_atual):
return jsonify({
"mensagem": f"Ola, {usuario_atual['nome']}! Estes sao seus dados secretos.",
"dados": [1, 2, 3, 4, 5]
})
Fluxo completo de autenticação
Testando o fluxo passo a passo:
# 1. registrar usuario
curl -X POST http://127.0.0.1:5000/api/registro \
-H "Content-Type: application/json" \
-d '{"email": "maria@email.com", "senha": "minhasenha123", "nome": "Maria"}'
# 2. fazer login (recebe o token)
curl -X POST http://127.0.0.1:5000/api/login \
-H "Content-Type: application/json" \
-d '{"email": "maria@email.com", "senha": "minhasenha123"}'
# 3. acessar rota protegida (substitua pelo token recebido)
curl http://127.0.0.1:5000/api/perfil \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6..."
# 4. sem token (recebe 401)
curl http://127.0.0.1:5000/api/perfil
Refresh Tokens
Tokens JWT com vida curta (15 minutos a 1 hora) são mais seguros, mas obrigam o usuário a fazer login frequentemente. Refresh tokens resolvem esse problema.
Como funciona
- No login, o servidor gera dois tokens: um access token (curta duração) e um refresh token (longa duração)
- O cliente usa o access token para acessar rotas protegidas
- Quando o access token expira, o cliente envia o refresh token para obter um novo access token
- O refresh token é armazenado de forma segura (httpOnly cookie ou storage seguro)
@app.route("/api/login", methods=["POST"])
def login_com_refresh():
dados = request.get_json()
usuario = next((u for u in usuarios if u["email"] == dados["email"]), None)
if usuario is None or not check_password_hash(usuario["senha_hash"], dados["senha"]):
return jsonify({"erro": "Credenciais invalidas"}), 401
# access token: curta duracao
access_token = jwt.encode(
{
"user_id": usuario["id"],
"tipo": "access",
"exp": datetime.now(timezone.utc) + timedelta(minutes=30)
},
app.config["SECRET_KEY"],
algorithm="HS256"
)
# refresh token: longa duracao
refresh_token = jwt.encode(
{
"user_id": usuario["id"],
"tipo": "refresh",
"exp": datetime.now(timezone.utc) + timedelta(days=30)
},
app.config["SECRET_KEY"],
algorithm="HS256"
)
return jsonify({
"access_token": access_token,
"refresh_token": refresh_token
}), 200
@app.route("/api/refresh", methods=["POST"])
def refresh():
dados = request.get_json()
refresh_token = dados.get("refresh_token")
if not refresh_token:
return jsonify({"erro": "Refresh token nao fornecido"}), 400
try:
payload = jwt.decode(
refresh_token,
app.config["SECRET_KEY"],
algorithms=["HS256"]
)
if payload.get("tipo") != "refresh":
return jsonify({"erro": "Token invalido"}), 401
novo_access_token = jwt.encode(
{
"user_id": payload["user_id"],
"tipo": "access",
"exp": datetime.now(timezone.utc) + timedelta(minutes=30)
},
app.config["SECRET_KEY"],
algorithm="HS256"
)
return jsonify({"access_token": novo_access_token}), 200
except jwt.ExpiredSignatureError:
return jsonify({"erro": "Refresh token expirado, faca login novamente"}), 401
except jwt.InvalidTokenError:
return jsonify({"erro": "Token invalido"}), 401
Boas práticas de segurança
1. Nunca exponha a chave secreta
import os
# ERRADO: chave fixa no codigo
app.config["SECRET_KEY"] = "minha-chave"
# CORRETO: variavel de ambiente
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")
if not app.config["SECRET_KEY"]:
raise RuntimeError("SECRET_KEY nao definida")
2. Use senhas fortes para a chave secreta
import secrets
# gerar uma chave segura
chave = secrets.token_hex(32)
print(chave) # 64 caracteres hexadecimais
3. Defina expiração nos tokens
# ERRADO: token sem expiracao
token = jwt.encode({"user_id": 1}, chave, algorithm="HS256")
# CORRETO: token com expiracao
token = jwt.encode(
{"user_id": 1, "exp": datetime.now(timezone.utc) + timedelta(hours=1)},
chave,
algorithm="HS256"
)
4. Use HTTPS em produção
Tokens trafegam pelo header HTTP. Sem HTTPS, qualquer intermediário pode interceptá-los. Em desenvolvimento, HTTP é aceitável, mas em produção, HTTPS é obrigatório.
5. Não armazene dados sensíveis no JWT
# ERRADO: senha no payload
payload = {"user_id": 1, "senha": "abc123"}
# CORRETO: apenas identificadores
payload = {"user_id": 1, "email": "maria@email.com"}
6. Valide todos os campos
# sempre verifique se o usuario ainda existe
# (pode ter sido deletado apos a emissao do token)
usuario = buscar_usuario_por_id(payload["user_id"])
if usuario is None:
return jsonify({"erro": "Usuario nao encontrado"}), 401
Comparação: sessões vs JWT
| Aspecto | Sessões (cookies) | JWT |
|---|---|---|
| Estado | Stateful (servidor guarda sessão) | Stateless (tudo está no token) |
| Armazenamento | Servidor (memória/banco) | Cliente (header) |
| Escalabilidade | Requer sessão compartilhada entre servidores | Escala naturalmente |
| Revogação | Fácil (apaga a sessão) | Difícil (precisa de blacklist) |
| Uso ideal | Sites com login | APIs, microsserviços, SPAs |
Resumo
| Conceito | Descrição |
|---|---|
| API Key | Chave fixa para autenticação simples |
| JWT | Token autocontido com header, payload e assinatura |
| PyJWT | Biblioteca Python para criar e verificar JWTs |
| Decorador | Função que protege rotas verificando o token |
| Refresh Token | Token de longa duração para renovar access tokens |
| Boas práticas | Variáveis de ambiente, expiração, HTTPS, sem dados sensíveis |
Com autenticação implementada, sua API está protegida. No próximo artigo, vamos aprender a testar e documentar APIs para que outros desenvolvedores consigam integrá-las facilmente.