Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2024-05-23 - JWT Blacklist Bypass
**Vulnerability:** The application was not checking the JWT blacklist during request authentication, allowing revoked tokens (e.g., after logout) to remain valid until expiration.
**Learning:** Using `request.jwtVerify()` from `@fastify/jwt` only verifies the signature and expiration. It does not perform custom checks like blacklisting. A custom middleware wrapping `jwtVerify` is required.
**Prevention:** Always implement a central authentication middleware that explicitly checks the token status against a revocation list (Redis/Database) in addition to signature verification.
10 changes: 10 additions & 0 deletions backend/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'js'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
setupFiles: ['<rootDir>/jest.setup.js'],
};
7 changes: 7 additions & 0 deletions backend/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test_db';
process.env.JWT_SECRET = 'test-secret-for-unit-tests-only-must-be-long-enough';
process.env.NODE_ENV = 'test';
process.env.LUNES_RPC_URL = 'http://localhost:9933';
process.env.CONTRACT_ADDRESS = '0x123';
process.env.PRIVATE_KEY = '0xabc';
process.env.REDIS_URL = 'redis://localhost:6379';
100 changes: 10 additions & 90 deletions backend/src/modules/ama/ama.routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FastifyInstance } from 'fastify';
import { AmaController } from './ama.controller';
import { authenticate } from '../../shared/middleware';

/**
* Rotas para AMA (Ask Me Anything)
Expand Down Expand Up @@ -133,16 +134,7 @@ export const amaRoutes = async (server: FastifyInstance) => {
}
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, amaController.getAmaSessions.bind(amaController));

// GET /ama/sessions/:id - Obter sessão AMA específica
Expand All @@ -163,16 +155,7 @@ export const amaRoutes = async (server: FastifyInstance) => {
200: { $ref: 'amaSessionResponse#' }
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, amaController.getAmaSession.bind(amaController));

// POST /ama/sessions - Criar nova sessão AMA
Expand All @@ -187,16 +170,7 @@ export const amaRoutes = async (server: FastifyInstance) => {
201: { $ref: 'amaSessionResponse#' }
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, amaController.createAmaSession.bind(amaController));

// PUT /ama/sessions/:id - Atualizar sessão AMA
Expand Down Expand Up @@ -231,16 +205,7 @@ export const amaRoutes = async (server: FastifyInstance) => {
200: { $ref: 'amaSessionResponse#' }
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, amaController.updateAmaSession.bind(amaController));

// GET /ama/sessions/:sessionId/questions - Listar perguntas de uma sessão
Expand Down Expand Up @@ -291,16 +256,7 @@ export const amaRoutes = async (server: FastifyInstance) => {
}
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, amaController.getAmaQuestions.bind(amaController));

// POST /ama/sessions/:sessionId/questions - Criar pergunta em uma sessão
Expand Down Expand Up @@ -329,16 +285,7 @@ export const amaRoutes = async (server: FastifyInstance) => {
}
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, amaController.createAmaQuestion.bind(amaController));

// PUT /ama/questions/:questionId/answer - Responder pergunta
Expand Down Expand Up @@ -367,16 +314,7 @@ export const amaRoutes = async (server: FastifyInstance) => {
}
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, amaController.answerAmaQuestion.bind(amaController));

// POST /ama/questions/:questionId/vote - Votar em pergunta
Expand Down Expand Up @@ -405,16 +343,7 @@ export const amaRoutes = async (server: FastifyInstance) => {
}
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, amaController.voteAmaQuestion.bind(amaController));

// GET /ama/sessions/:sessionId/stats - Estatísticas da sessão
Expand Down Expand Up @@ -449,15 +378,6 @@ export const amaRoutes = async (server: FastifyInstance) => {
}
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, amaController.getAmaStats.bind(amaController));
};
91 changes: 23 additions & 68 deletions backend/src/modules/analytics/analytics.routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FastifyInstance } from 'fastify';
import { AnalyticsController } from './analytics.controller';
import { authenticate } from '../../shared/middleware';

/**
* Rotas para Analytics
Expand Down Expand Up @@ -64,16 +65,7 @@ export const analyticsRoutes = async (server: FastifyInstance) => {
200: { $ref: 'dashboardMetricsResponse#' }
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, analyticsController.getDashboardMetrics.bind(analyticsController));

// GET /analytics/projects - Métricas de projetos
Expand Down Expand Up @@ -108,16 +100,7 @@ export const analyticsRoutes = async (server: FastifyInstance) => {
}
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, analyticsController.getProjectsMetrics.bind(analyticsController));

// GET /analytics/users - Métricas de usuários (admin only)
Expand Down Expand Up @@ -161,21 +144,16 @@ export const analyticsRoutes = async (server: FastifyInstance) => {
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
const user = (request as any).user;

// Verificar se é admin
if (user.role !== 'ADMIN') {
return reply.status(403).send({
success: false,
error: 'Acesso negado - apenas administradores'
});
}
} catch (err) {
return reply.status(401).send({
await authenticate(request, reply);
if (reply.sent) return;

const user = (request as any).user;

// Verificar se é admin
if (user.role !== 'ADMIN') {
return reply.status(403).send({
success: false,
error: 'Token de autorização inválido'
error: 'Acesso negado - apenas administradores'
});
}
}
Expand Down Expand Up @@ -229,21 +207,16 @@ export const analyticsRoutes = async (server: FastifyInstance) => {
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
const user = (request as any).user;

// Verificar se é admin
if (user.role !== 'ADMIN') {
return reply.status(403).send({
success: false,
error: 'Acesso negado - apenas administradores'
});
}
} catch (err) {
return reply.status(401).send({
await authenticate(request, reply);
if (reply.sent) return;

const user = (request as any).user;

// Verificar se é admin
if (user.role !== 'ADMIN') {
return reply.status(403).send({
success: false,
error: 'Token de autorização inválido'
error: 'Acesso negado - apenas administradores'
});
}
}
Expand Down Expand Up @@ -287,16 +260,7 @@ export const analyticsRoutes = async (server: FastifyInstance) => {
}
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, analyticsController.getPerformanceMetrics.bind(analyticsController));

// POST /analytics/events - Registrar evento de analytics
Expand All @@ -317,16 +281,7 @@ export const analyticsRoutes = async (server: FastifyInstance) => {
}
}
},
preHandler: async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
return reply.status(401).send({
success: false,
error: 'Token de autorização inválido'
});
}
}
preHandler: authenticate
}, analyticsController.trackEvent.bind(analyticsController));

await server.register(async function (server) {}, { prefix: '/analytics' });
Expand Down
6 changes: 6 additions & 0 deletions backend/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,12 @@ export class AuthService {
// Verificar token
async verifyToken(token: string): Promise<TokenPayload> {
try {
// Verificar se o token está na blacklist
const isBlacklisted = await this.isTokenBlacklisted(token);
if (isBlacklisted) {
throw new Error('Token revogado ou inválido');
}

const decoded = jwt.verify(token, this.JWT_SECRET) as TokenPayload;

if (decoded.type !== 'access') {
Expand Down
7 changes: 2 additions & 5 deletions backend/src/modules/projects/project.routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FastifyInstance } from 'fastify';
import { logger } from '../../shared/logger';
import { authenticate } from '../../shared/middleware';

/**
* Rotas para gerenciamento de projetos
Expand Down Expand Up @@ -151,11 +152,7 @@ export const projectRoutes = async (server: FastifyInstance) => {
server.addHook('preHandler', async (request, reply) => {
// Aplicar apenas para métodos que precisam de autenticação
if (['POST', 'PUT', 'DELETE'].includes(request.method)) {
try {
await request.jwtVerify();
} catch (err) {
reply.send(err);
}
await authenticate(request, reply);
}
});

Expand Down
Loading
Loading