Security Fixes

This commit is contained in:
Eric Gullickson
2025-08-24 14:39:50 -05:00
parent 000e71a026
commit e22d643ae3
19 changed files with 187 additions and 8838 deletions

View File

@@ -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

View File

@@ -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(),
}),
],
});

View File

@@ -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'
});
}
});
};

View File

@@ -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,
});
});
};

View File

@@ -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', () => ({