implementação da validação por jwt

This commit is contained in:
2026-05-28 09:46:53 -05:00
parent f148d430b3
commit a4cbc28d1d
6 changed files with 211 additions and 3 deletions

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
DATABASE_URL="postgresql://postgres:postgres@db:5432/gameverse_catalog"
# JWT Config
JWT_ISSUER="https://auth.local"
JWT_AUDIENCE="internal-apis"
JWT_PUBLIC_KEY_PEM="-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA...\n-----END PUBLIC KEY-----"

View File

@@ -8,6 +8,7 @@ import unicodedata
from app.schemas.game import GameCreate, GameUpdate, GameResponse, StandardResponse from app.schemas.game import GameCreate, GameUpdate, GameResponse, StandardResponse
from app.models.game import Game from app.models.game import Game
from app.db.database import get_db from app.db.database import get_db
from app.core.security import get_current_user, UserAuth
router = APIRouter() router = APIRouter()
@@ -38,7 +39,7 @@ def read_games(skip: int = 0, limit: int = 100, genre: Optional[str] = None, pla
return {"success": True, "message": "Lista de jogos", "data": data} return {"success": True, "message": "Lista de jogos", "data": data}
@router.post("/", response_model=StandardResponse, status_code=status.HTTP_201_CREATED) @router.post("/", response_model=StandardResponse, status_code=status.HTTP_201_CREATED)
def create_game(game: GameCreate, db: Session = Depends(get_db)): def create_game(game: GameCreate, db: Session = Depends(get_db), current_user: UserAuth = Depends(get_current_user)):
slug = generate_slug(game.title) slug = generate_slug(game.title)
# Verifica se já existe um jogo com esse slug # Verifica se já existe um jogo com esse slug
@@ -71,7 +72,7 @@ def read_game(id_ou_slug: str, db: Session = Depends(get_db)):
return {"success": True, "message": "Detalhes do jogo", "data": data} return {"success": True, "message": "Detalhes do jogo", "data": data}
@router.patch("/{id}", response_model=StandardResponse) @router.patch("/{id}", response_model=StandardResponse)
def update_game(id: int, game: GameUpdate, db: Session = Depends(get_db)): def update_game(id: int, game: GameUpdate, db: Session = Depends(get_db), current_user: UserAuth = Depends(get_current_user)):
db_game = db.query(Game).filter(Game.id == id).first() db_game = db.query(Game).filter(Game.id == id).first()
if not db_game: if not db_game:
@@ -98,7 +99,7 @@ def update_game(id: int, game: GameUpdate, db: Session = Depends(get_db)):
return {"success": True, "message": "Jogo atualizado com sucesso", "data": data} return {"success": True, "message": "Jogo atualizado com sucesso", "data": data}
@router.delete("/{id_ou_slug}", response_model=StandardResponse) @router.delete("/{id_ou_slug}", response_model=StandardResponse)
def delete_game(id_ou_slug: str, db: Session = Depends(get_db)): def delete_game(id_ou_slug: str, db: Session = Depends(get_db), current_user: UserAuth = Depends(get_current_user)):
if id_ou_slug.isdigit(): if id_ou_slug.isdigit():
db_game = db.query(Game).filter(Game.id == int(id_ou_slug)).first() db_game = db.query(Game).filter(Game.id == int(id_ou_slug)).first()
else: else:

View File

@@ -5,6 +5,11 @@ class Settings(BaseSettings):
API_V1_STR: str = "/api/v1" API_V1_STR: str = "/api/v1"
DATABASE_URL: str DATABASE_URL: str
# Configurações de JWT
JWT_ISSUER: str
JWT_AUDIENCE: str
JWT_PUBLIC_KEY_PEM: str
class Config: class Config:
env_file = ".env" env_file = ".env"
case_sensitive = True case_sensitive = True

54
app/core/security.py Normal file
View File

@@ -0,0 +1,54 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from pydantic import BaseModel
from app.core.config import settings
security = HTTPBearer()
class UserAuth(BaseModel):
id: str
token: str
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> UserAuth:
token = credentials.credentials
try:
# A chave pública pode vir com '\n' escapado do .env
public_key = settings.JWT_PUBLIC_KEY_PEM.replace('\\n', '\n')
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
issuer=settings.JWT_ISSUER,
audience=settings.JWT_AUDIENCE
)
sub = payload.get("sub")
if not sub or not str(sub).strip():
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token claims",
headers={"WWW-Authenticate": "Bearer"},
)
return UserAuth(id=str(sub), token=token)
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token expired",
headers={"WWW-Authenticate": "Bearer"},
)
except jwt.InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid access token",
headers={"WWW-Authenticate": "Bearer"},
)
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)

140
jwtimplementation.md Normal file
View File

@@ -0,0 +1,140 @@
# Guia didatico: validar JWT nos servicos consumidores (sem JWKS)
Este guia mostra a forma mais simples de integrar os servicos:
1. Auth assina o token com `JWT_PRIVATE_KEY_PEM`
2. cada servico consumidor valida localmente com `JWT_PUBLIC_KEY_PEM`
## 1) Configuracao em cada servico consumidor
Cada servico precisa ter no `.env`:
```env
JWT_ISSUER="https://auth.local"
JWT_AUDIENCE="internal-apis"
JWT_PUBLIC_KEY_PEM="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
```
Importante:
- o `JWT_PUBLIC_KEY_PEM` deve ser exatamente a chave publica do Auth
- mantenha os `\n` literais no `.env`
## 2) O que validar
Para toda rota protegida:
1. ler `Authorization`
2. extrair `Bearer <token>`
3. validar assinatura RS256 com `JWT_PUBLIC_KEY_PEM`
4. validar `iss`, `aud` e `exp`
5. validar se `sub` existe
6. montar `request.auth = { id: sub, token }`
Se algo falhar, retornar `401`.
## 3) Exemplo pratico (Node/Express + jose)
```ts
import express from "express";
import { importSPKI, jwtVerify } from "jose";
const app = express();
const ISSUER = process.env.JWT_ISSUER!;
const AUDIENCE = process.env.JWT_AUDIENCE!;
const PUBLIC_KEY_PEM = process.env.JWT_PUBLIC_KEY_PEM!.replace(/\\n/g, "\n");
let publicKeyPromise: ReturnType<typeof importSPKI> | null = null;
function getPublicKey() {
if (!publicKeyPromise) {
publicKeyPromise = importSPKI(PUBLIC_KEY_PEM, "RS256");
}
return publicKeyPromise;
}
function authMiddleware() {
return async (req, res, next) => {
try {
const authHeader = req.header("authorization");
if (!authHeader) {
return res
.status(401)
.json({ message: "Missing Authorization header" });
}
const [scheme, token] = authHeader.split(" ");
if (!scheme || !token || scheme.toLowerCase() !== "bearer") {
return res
.status(401)
.json({ message: "Authorization must be Bearer token" });
}
const publicKey = await getPublicKey();
const { payload } = await jwtVerify(token, publicKey, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ["RS256"],
});
if (typeof payload.sub !== "string" || !payload.sub.trim()) {
return res.status(401).json({ message: "Invalid token claims" });
}
req.auth = {
id: payload.sub,
token,
};
return next();
} catch {
return res
.status(401)
.json({ message: "Invalid or expired access token" });
}
};
}
app.get("/profile/me", authMiddleware(), (req, res) => {
return res.json({ userId: req.auth.id });
});
```
## 4) Propagacao entre servicos (A -> B)
Quando API A chamar API B em nome do usuario:
- repasse o mesmo `Authorization: Bearer <token>`
- API B valida esse token com `JWT_PUBLIC_KEY_PEM`
- API B usa `sub` como identidade confiavel
```ts
const authHeader = req.header("authorization");
const response = await fetch("http://courses-service/courses/me", {
headers: { Authorization: authHeader! },
});
```
## 5) Checklist rapido
- todos os servicos com o mesmo `JWT_PUBLIC_KEY_PEM`
- middleware valida assinatura + `iss` + `aud` + `exp`
- `sub` obrigatorio
- retorna `401` para token ausente/invalido/expirado
- usa `sub` como ID confiavel
## 6) Trade-off dessa abordagem
Vantagem:
- muito simples para implementar e explicar
Desvantagem:
- quando trocar a chave publica, precisa atualizar o `.env` de todos os servicos consumidores

View File

@@ -6,3 +6,5 @@ sqlalchemy>=2.0.25
psycopg2-binary>=2.9.9 psycopg2-binary>=2.9.9
alembic>=1.13.1 alembic>=1.13.1
pytest>=8.0.0 pytest>=8.0.0
PyJWT>=2.8.0
cryptography>=42.0.0