Security Fixes
This commit is contained in:
@@ -19,7 +19,7 @@ RUN npm install && npm cache clean --force
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build:docker
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Production runtime
|
||||
FROM node:20-alpine AS production
|
||||
@@ -30,8 +30,9 @@ RUN apk add --no-cache dumb-init
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
# Copy package files and any lock file generated in builder stage
|
||||
COPY package*.json ./
|
||||
COPY --from=builder /app/package-lock.json ./
|
||||
|
||||
# Install only production dependencies
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
@@ -56,8 +56,8 @@ make test
|
||||
- `database.ts` - PostgreSQL connection pool
|
||||
- `redis.ts` - Redis client and cache service
|
||||
|
||||
### Security (`src/core/security/`)
|
||||
- `auth.middleware.ts` - JWT authentication via Auth0
|
||||
### Security (Fastify Plugin)
|
||||
- `src/core/plugins/auth.plugin.ts` - Auth plugin (mock user in dev; plan for Auth0 JWT)
|
||||
|
||||
### Logging (`src/core/logging/`)
|
||||
- `logger.ts` - Structured logging with Winston
|
||||
@@ -89,7 +89,7 @@ Run tests:
|
||||
npm test
|
||||
|
||||
# Specific feature
|
||||
npm run test:feature -- --feature=vehicles
|
||||
npm test -- features/vehicles
|
||||
|
||||
# Watch mode
|
||||
npm run test:watch
|
||||
@@ -100,5 +100,5 @@ npm run test:watch
|
||||
See `.env.example` for required variables. Key variables:
|
||||
- Database connection (DB_*)
|
||||
- Redis connection (REDIS_*)
|
||||
- Auth0 configuration (AUTH0_*)
|
||||
- External API keys
|
||||
- Auth0 configuration (AUTH0_*) — backend currently uses mock auth; JWT enforcement planned
|
||||
- External API keys
|
||||
|
||||
8723
backend/package-lock.json
generated
8723
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,7 @@
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon --watch src --exec ts-node src/index.ts",
|
||||
"build": "tsc",
|
||||
"build:docker": "tsc --project tsconfig.build.json",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"start": "node dist/index.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
@@ -39,7 +38,8 @@
|
||||
"@fastify/type-provider-typebox": "^4.0.0",
|
||||
"@sinclair/typebox": "^0.31.28",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
"@fastify/autoload": "^5.8.0"
|
||||
"@fastify/autoload": "^5.8.0",
|
||||
"get-jwks": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const envSchema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
||||
NODE_ENV: z.string().default('development'),
|
||||
PORT: z.string().transform(Number).default('3001'),
|
||||
|
||||
// Database
|
||||
@@ -22,11 +22,11 @@ const envSchema = z.object({
|
||||
REDIS_HOST: z.string().default('localhost'),
|
||||
REDIS_PORT: z.string().transform(Number).default('6379'),
|
||||
|
||||
// Auth0
|
||||
AUTH0_DOMAIN: z.string().default('localhost'),
|
||||
AUTH0_CLIENT_ID: z.string().default('development'),
|
||||
AUTH0_CLIENT_SECRET: z.string().default('development'),
|
||||
AUTH0_AUDIENCE: z.string().default('https://api.motovaultpro.com'),
|
||||
// Auth0 - Required for JWT validation
|
||||
AUTH0_DOMAIN: z.string().min(1, 'AUTH0_DOMAIN is required for JWT authentication'),
|
||||
AUTH0_CLIENT_ID: z.string().min(1, 'AUTH0_CLIENT_ID is required'),
|
||||
AUTH0_CLIENT_SECRET: z.string().min(1, 'AUTH0_CLIENT_SECRET is required'),
|
||||
AUTH0_AUDIENCE: z.string().min(1, 'AUTH0_AUDIENCE is required for JWT validation'),
|
||||
|
||||
// External APIs
|
||||
GOOGLE_MAPS_API_KEY: z.string().default('development'),
|
||||
@@ -45,7 +45,4 @@ export type Environment = z.infer<typeof envSchema>;
|
||||
// Validate and export - now with defaults for build-time compilation
|
||||
export const env = envSchema.parse(process.env);
|
||||
|
||||
// Convenience exports
|
||||
export const isDevelopment = env.NODE_ENV === 'development';
|
||||
export const isProduction = env.NODE_ENV === 'production';
|
||||
export const isTest = env.NODE_ENV === 'test';
|
||||
// Environment configuration validated and exported
|
||||
@@ -3,10 +3,9 @@
|
||||
* @ai-context All features use this for consistent logging
|
||||
*/
|
||||
import * as winston from 'winston';
|
||||
import { env, isDevelopment } from '../config/environment';
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: isDevelopment ? 'debug' : 'info',
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
@@ -14,16 +13,10 @@ export const logger = winston.createLogger({
|
||||
),
|
||||
defaultMeta: {
|
||||
service: 'motovaultpro-backend',
|
||||
environment: env.NODE_ENV,
|
||||
},
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: isDevelopment
|
||||
? winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
: winston.format.json(),
|
||||
format: winston.format.json(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* @ai-summary Fastify JWT authentication plugin using Auth0
|
||||
* @ai-context Validates JWT tokens in production, mocks in development
|
||||
* @ai-context Validates JWT tokens against Auth0 JWKS endpoint
|
||||
*/
|
||||
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import fp from 'fastify-plugin';
|
||||
import buildGetJwks from 'get-jwks';
|
||||
import { env } from '../config/environment';
|
||||
import { logger } from '../logging/logger';
|
||||
|
||||
@@ -11,20 +12,67 @@ declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
||||
}
|
||||
interface FastifyRequest {
|
||||
jwtVerify(): Promise<void>;
|
||||
user?: any;
|
||||
}
|
||||
}
|
||||
|
||||
const authPlugin: FastifyPluginAsync = async (fastify) => {
|
||||
// For now, use mock authentication in all environments
|
||||
// The frontend Auth0 flow should work independently
|
||||
// TODO: Implement proper JWKS validation when needed for API security
|
||||
|
||||
fastify.decorate('authenticate', async (request: FastifyRequest, _reply: FastifyReply) => {
|
||||
(request as any).user = { sub: 'dev-user-123' };
|
||||
|
||||
if (env.NODE_ENV === 'development') {
|
||||
logger.debug('Using mock user for development', { userId: 'dev-user-123' });
|
||||
} else {
|
||||
logger.info('Using mock authentication - Auth0 handled by frontend', { userId: 'dev-user-123' });
|
||||
// Initialize JWKS client for Auth0 public key retrieval
|
||||
const getJwks = buildGetJwks({
|
||||
ttl: 60 * 60 * 1000, // 1 hour cache
|
||||
});
|
||||
|
||||
// Register @fastify/jwt with Auth0 JWKS validation
|
||||
await fastify.register(require('@fastify/jwt'), {
|
||||
decode: { complete: true },
|
||||
secret: async (_request: FastifyRequest, token: any) => {
|
||||
try {
|
||||
const { header: { kid, alg }, payload: { iss } } = token;
|
||||
|
||||
// Validate issuer matches Auth0 domain
|
||||
const expectedIssuer = `https://${env.AUTH0_DOMAIN}/`;
|
||||
if (iss !== expectedIssuer) {
|
||||
throw new Error(`Invalid issuer: ${iss}`);
|
||||
}
|
||||
|
||||
// Get public key from Auth0 JWKS endpoint
|
||||
return getJwks.getPublicKey({ kid, domain: env.AUTH0_DOMAIN, alg });
|
||||
} catch (error) {
|
||||
logger.error('JWKS key retrieval failed', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
domain: env.AUTH0_DOMAIN
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
verify: {
|
||||
allowedIss: `https://${env.AUTH0_DOMAIN}/`,
|
||||
allowedAud: env.AUTH0_AUDIENCE,
|
||||
},
|
||||
});
|
||||
|
||||
// Decorate with authenticate function that validates JWT
|
||||
fastify.decorate('authenticate', async function(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
|
||||
logger.info('JWT authentication successful', {
|
||||
userId: request.user?.sub?.substring(0, 8) + '...',
|
||||
audience: env.AUTH0_AUDIENCE
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn('JWT authentication failed', {
|
||||
path: request.url,
|
||||
method: request.method,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
|
||||
reply.code(401).send({
|
||||
error: 'Unauthorized',
|
||||
message: 'Invalid or missing JWT token'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,7 +17,6 @@ const errorPlugin: FastifyPluginAsync = async (fastify) => {
|
||||
|
||||
reply.status(500).send({
|
||||
error: 'Internal server error',
|
||||
message: process.env.NODE_ENV === 'development' ? error.message : undefined,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -10,13 +10,17 @@ import { cacheService } from '../../../../core/config/redis';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
// Mock auth middleware to bypass JWT validation in tests
|
||||
jest.mock('../../../../core/security/auth.middleware', () => ({
|
||||
authMiddleware: (req: any, _res: any, next: any) => {
|
||||
req.user = { sub: 'test-user-123' };
|
||||
next();
|
||||
}
|
||||
}));
|
||||
// Mock auth plugin to bypass JWT validation in tests
|
||||
jest.mock('../../../../core/plugins/auth.plugin', () => {
|
||||
const fastifyPlugin = require('fastify-plugin');
|
||||
return {
|
||||
default: fastifyPlugin(async function(fastify) {
|
||||
fastify.decorate('authenticate', async function(request, _reply) {
|
||||
request.user = { sub: 'test-user-123' };
|
||||
});
|
||||
}, { name: 'auth-plugin' })
|
||||
};
|
||||
});
|
||||
|
||||
// Mock external VIN decoder
|
||||
jest.mock('../../external/vpic/vpic.client', () => ({
|
||||
|
||||
Reference in New Issue
Block a user