feat(test-api): enhance test API server with Phase 4 JWT authentication
- Added JWT authentication middleware (verifyJWT) for testing SecurityManager - Enhanced all Endorser.ch API endpoints with JWT authentication: - /api/v2/report/offers (JWT required) - /api/v2/report/offersToPlansOwnedByMe (JWT required) - /api/v2/report/plansLastUpdatedBetween (JWT required) - Added Phase 4 test endpoints: - /api/v2/test/jwt-validation: Test JWT token validation - /api/v2/test/security-info: Get security configuration info - Enhanced JWT validation with: - Token format validation - Expiration checking - DID-based authorization (JWT DID must match recipientId) - Detailed error responses for authentication failures - Updated server startup logs to show JWT requirements - Enhanced usage examples with JWT authentication examples - Added comprehensive JWT security testing capabilities Test API server now fully supports Phase 4 SecurityManager testing
This commit is contained in:
@@ -17,6 +17,53 @@ const crypto = require('crypto');
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
|
// Phase 4: JWT authentication middleware for testing SecurityManager
|
||||||
|
function verifyJWT(req, res, next) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Missing or invalid Authorization header',
|
||||||
|
expected: 'Bearer <JWT_TOKEN>'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
||||||
|
|
||||||
|
// Simple JWT validation for testing (in production, use proper JWT library)
|
||||||
|
try {
|
||||||
|
const parts = token.split('.');
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
throw new Error('Invalid JWT format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode payload (base64url)
|
||||||
|
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
||||||
|
|
||||||
|
// Check expiration
|
||||||
|
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'JWT token expired',
|
||||||
|
expiredAt: new Date(payload.exp * 1000).toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add decoded claims to request for use in endpoints
|
||||||
|
req.jwtClaims = payload;
|
||||||
|
req.activeDid = payload.iss; // Assuming 'iss' contains the active DID
|
||||||
|
|
||||||
|
console.log(`✅ JWT verified for DID: ${req.activeDid}`);
|
||||||
|
next();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`❌ JWT verification failed: ${error.message}`);
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Invalid JWT token',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -155,12 +202,13 @@ app.get('/health', (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Endorser.ch API: Get offers to person
|
* Endorser.ch API: Get offers to person
|
||||||
|
* Phase 4: Requires JWT authentication
|
||||||
*/
|
*/
|
||||||
app.get('/api/v2/report/offers', (req, res) => {
|
app.get('/api/v2/report/offers', verifyJWT, (req, res) => {
|
||||||
const { recipientId, afterId } = req.query;
|
const { recipientId, afterId } = req.query;
|
||||||
|
|
||||||
console.log(`[${new Date().toISOString()}] GET /api/v2/report/offers`);
|
console.log(`[${new Date().toISOString()}] GET /api/v2/report/offers`);
|
||||||
console.log(` recipientId: ${recipientId}, afterId: ${afterId || 'none'}`);
|
console.log(` JWT DID: ${req.activeDid}, recipientId: ${recipientId}, afterId: ${afterId || 'none'}`);
|
||||||
|
|
||||||
if (!recipientId) {
|
if (!recipientId) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -168,6 +216,16 @@ app.get('/api/v2/report/offers', (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 4: Validate that JWT DID matches recipientId for security
|
||||||
|
if (req.activeDid !== recipientId) {
|
||||||
|
console.log(` ❌ JWT DID mismatch: ${req.activeDid} != ${recipientId}`);
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'JWT DID does not match recipientId',
|
||||||
|
jwtDid: req.activeDid,
|
||||||
|
requestedRecipientId: recipientId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const offers = generateMockOffers(recipientId, afterId);
|
const offers = generateMockOffers(recipientId, afterId);
|
||||||
|
|
||||||
console.log(` → 200 OK (${offers.data.length} offers, hitLimit: ${offers.hitLimit})`);
|
console.log(` → 200 OK (${offers.data.length} offers, hitLimit: ${offers.hitLimit})`);
|
||||||
@@ -176,12 +234,13 @@ app.get('/api/v2/report/offers', (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Endorser.ch API: Get offers to user's projects
|
* Endorser.ch API: Get offers to user's projects
|
||||||
|
* Phase 4: Requires JWT authentication
|
||||||
*/
|
*/
|
||||||
app.get('/api/v2/report/offersToPlansOwnedByMe', (req, res) => {
|
app.get('/api/v2/report/offersToPlansOwnedByMe', verifyJWT, (req, res) => {
|
||||||
const { afterId } = req.query;
|
const { afterId } = req.query;
|
||||||
|
|
||||||
console.log(`[${new Date().toISOString()}] GET /api/v2/report/offersToPlansOwnedByMe`);
|
console.log(`[${new Date().toISOString()}] GET /api/v2/report/offersToPlansOwnedByMe`);
|
||||||
console.log(` afterId: ${afterId || 'none'}`);
|
console.log(` JWT DID: ${req.activeDid}, afterId: ${afterId || 'none'}`);
|
||||||
|
|
||||||
const offers = {
|
const offers = {
|
||||||
data: [], // Simulate no offers to user's projects
|
data: [], // Simulate no offers to user's projects
|
||||||
@@ -194,12 +253,13 @@ app.get('/api/v2/report/offersToPlansOwnedByMe', (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Endorser.ch API: Get changes to starred projects
|
* Endorser.ch API: Get changes to starred projects
|
||||||
|
* Phase 4: Requires JWT authentication
|
||||||
*/
|
*/
|
||||||
app.post('/api/v2/report/plansLastUpdatedBetween', (req, res) => {
|
app.post('/api/v2/report/plansLastUpdatedBetween', verifyJWT, (req, res) => {
|
||||||
const { planIds, afterId } = req.body;
|
const { planIds, afterId } = req.body;
|
||||||
|
|
||||||
console.log(`[${new Date().toISOString()}] POST /api/v2/report/plansLastUpdatedBetween`);
|
console.log(`[${new Date().toISOString()}] POST /api/v2/report/plansLastUpdatedBetween`);
|
||||||
console.log(` planIds: ${JSON.stringify(planIds)}, afterId: ${afterId || 'none'}`);
|
console.log(` JWT DID: ${req.activeDid}, planIds: ${JSON.stringify(planIds)}, afterId: ${afterId || 'none'}`);
|
||||||
|
|
||||||
if (!planIds || !Array.isArray(planIds)) {
|
if (!planIds || !Array.isArray(planIds)) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -393,15 +453,59 @@ app.use((req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 4: JWT validation test endpoint
|
||||||
|
* Allows testing SecurityManager JWT generation and verification
|
||||||
|
*/
|
||||||
|
app.get('/api/v2/test/jwt-validation', verifyJWT, (req, res) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] GET /api/v2/test/jwt-validation`);
|
||||||
|
console.log(` JWT DID: ${req.activeDid}`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'JWT validation successful',
|
||||||
|
jwtClaims: req.jwtClaims,
|
||||||
|
activeDid: req.activeDid,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 4: Security test endpoint (no auth required)
|
||||||
|
* For testing SecurityManager initialization
|
||||||
|
*/
|
||||||
|
app.get('/api/v2/test/security-info', (req, res) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] GET /api/v2/test/security-info`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
securityInfo: {
|
||||||
|
jwtRequired: true,
|
||||||
|
supportedAlgorithms: ['HS256', 'ES256K'],
|
||||||
|
tokenExpiration: '60 minutes',
|
||||||
|
audience: 'endorser-api',
|
||||||
|
scope: 'notifications'
|
||||||
|
},
|
||||||
|
testEndpoints: {
|
||||||
|
jwtValidation: '/api/v2/test/jwt-validation',
|
||||||
|
offers: '/api/v2/report/offers',
|
||||||
|
offersToProjects: '/api/v2/report/offersToPlansOwnedByMe',
|
||||||
|
projectUpdates: '/api/v2/report/plansLastUpdatedBetween'
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`🚀 TimeSafari Test API Server running on port ${PORT}`);
|
console.log(`🚀 TimeSafari Test API Server running on port ${PORT}`);
|
||||||
console.log(`📋 Available endpoints:`);
|
console.log(`📋 Available endpoints:`);
|
||||||
console.log(` GET /health - Health check`);
|
console.log(` GET /health - Health check`);
|
||||||
console.log(` GET /api/v2/report/offers - Get offers to person`);
|
console.log(` GET /api/v2/report/offers - Get offers to person (JWT required)`);
|
||||||
console.log(` GET /api/v2/report/offersToPlansOwnedByMe - Get offers to user's projects`);
|
console.log(` GET /api/v2/report/offersToPlansOwnedByMe - Get offers to user's projects (JWT required)`);
|
||||||
console.log(` POST /api/v2/report/plansLastUpdatedBetween - Get changes to starred projects`);
|
console.log(` POST /api/v2/report/plansLastUpdatedBetween - Get changes to starred projects (JWT required)`);
|
||||||
console.log(` GET /api/v2/report/notifications/bundle - Get TimeSafari notification bundle`);
|
console.log(` GET /api/v2/report/notifications/bundle - Get TimeSafari notification bundle`);
|
||||||
|
console.log(` GET /api/v2/test/jwt-validation - Test JWT validation (JWT required)`);
|
||||||
|
console.log(` GET /api/v2/test/security-info - Get security configuration info`);
|
||||||
console.log(` POST /api/analytics/community-events - Send community analytics`);
|
console.log(` POST /api/analytics/community-events - Send community analytics`);
|
||||||
console.log(` GET /api/content/:slotId - Legacy content endpoint`);
|
console.log(` GET /api/content/:slotId - Legacy content endpoint`);
|
||||||
console.log(` GET /api/metrics - API metrics`);
|
console.log(` GET /api/metrics - API metrics`);
|
||||||
@@ -412,8 +516,10 @@ app.listen(PORT, () => {
|
|||||||
console.log(``);
|
console.log(``);
|
||||||
console.log(`📝 Usage examples:`);
|
console.log(`📝 Usage examples:`);
|
||||||
console.log(` curl http://localhost:${PORT}/health`);
|
console.log(` curl http://localhost:${PORT}/health`);
|
||||||
console.log(` curl "http://localhost:${PORT}/api/v2/report/offers?recipientId=did:example:testuser123&afterId=01HSE3R9MAC0FT3P3KZ382TWV7"`);
|
console.log(` curl http://localhost:${PORT}/api/v2/test/security-info`);
|
||||||
console.log(` curl -X POST http://localhost:${PORT}/api/v2/report/plansLastUpdatedBetween -H "Content-Type: application/json" -d '{"planIds":["plan-123","plan-456"],"afterId":"01HSE3R9MAC0FT3P3KZ382TWV8"}'`);
|
console.log(` curl -H "Authorization: Bearer <JWT_TOKEN>" "http://localhost:${PORT}/api/v2/test/jwt-validation"`);
|
||||||
|
console.log(` curl -H "Authorization: Bearer <JWT_TOKEN>" "http://localhost:${PORT}/api/v2/report/offers?recipientId=did:example:testuser123&afterId=01HSE3R9MAC0FT3P3KZ382TWV7"`);
|
||||||
|
console.log(` curl -H "Authorization: Bearer <JWT_TOKEN>" -X POST http://localhost:${PORT}/api/v2/report/plansLastUpdatedBetween -H "Content-Type: application/json" -d '{"planIds":["plan-123","plan-456"],"afterId":"01HSE3R9MAC0FT3P3KZ382TWV8"}'`);
|
||||||
console.log(` curl "http://localhost:${PORT}/api/v2/report/notifications/bundle?userDid=did:example:testuser123&starredPlanIds=[\"plan-123\",\"plan-456\"]"`);
|
console.log(` curl "http://localhost:${PORT}/api/v2/report/notifications/bundle?userDid=did:example:testuser123&starredPlanIds=[\"plan-123\",\"plan-456\"]"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user