Add security fixes and deployment configuration
Security fixes: - Remove hardcoded MongoDB credentials from server.js (fail fast in production) - Add OAuth state parameter validation for CSRF protection - Add input validation for unitCode parameter to prevent NoSQL injection - Add isValidUnitCode helper function Deployment: - Update docker-compose.yml to use env_file and environment variables - Create .env.example with all required configuration variables
This commit is contained in:
20
.env.example
Normal file
20
.env.example
Normal file
@ -0,0 +1,20 @@
|
||||
# MongoDB Connection
|
||||
MONGO_URI=mongodb://username:password@host:27017
|
||||
|
||||
# Google OAuth Credentials (from Google Cloud Console)
|
||||
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=your-client-secret
|
||||
GOOGLE_CALLBACK_URL=https://apartments.maverickapplications.com/api/auth/google/callback
|
||||
|
||||
# JWT Configuration
|
||||
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
JWT_SECRET=generate-a-secure-random-string-minimum-32-characters
|
||||
|
||||
# Application URLs
|
||||
FRONTEND_URL=https://apartments.maverickapplications.com
|
||||
|
||||
# Activity Logging Level: all, navigation, none
|
||||
ACTIVITY_LOG_LEVEL=all
|
||||
|
||||
# Node Environment
|
||||
NODE_ENV=production
|
||||
@ -3,10 +3,18 @@ services:
|
||||
build: .
|
||||
container_name: apartment-api
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=8080 # Changed from 3000 to 8080
|
||||
- MONGO_URI=mongodb://admin:password123@mongodb:27017
|
||||
- PORT=8080
|
||||
- MONGO_URI=${MONGO_URI}
|
||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||
- GOOGLE_CALLBACK_URL=${GOOGLE_CALLBACK_URL}
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- FRONTEND_URL=${FRONTEND_URL}
|
||||
- ACTIVITY_LOG_LEVEL=${ACTIVITY_LOG_LEVEL:-all}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const passport = require('passport');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const crypto = require('crypto');
|
||||
const authConfig = require('../config/auth');
|
||||
const { requireAuth, generateToken } = require('../middleware/auth');
|
||||
const { logActivity, ACTIONS } = require('../services/activityLogger');
|
||||
@ -9,25 +10,53 @@ const router = express.Router();
|
||||
|
||||
/**
|
||||
* GET /auth/google
|
||||
* Initiates Google OAuth flow
|
||||
* Initiates Google OAuth flow with state parameter for CSRF protection
|
||||
*/
|
||||
router.get('/google', passport.authenticate('google', {
|
||||
scope: authConfig.google.scope,
|
||||
session: false
|
||||
}));
|
||||
router.get('/google', (req, res, next) => {
|
||||
// Generate cryptographically secure state parameter
|
||||
const state = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
// Store state in a short-lived cookie for validation
|
||||
res.cookie('oauth_state', state, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 5 * 60 * 1000 // 5 minutes
|
||||
});
|
||||
|
||||
passport.authenticate('google', {
|
||||
scope: authConfig.google.scope,
|
||||
session: false,
|
||||
state: state
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /auth/google/callback
|
||||
* Handles OAuth callback from Google
|
||||
* Validates state parameter for CSRF protection
|
||||
* On success: generates JWT, sets cookie, redirects to frontend
|
||||
* On failure: redirects to login with error
|
||||
*/
|
||||
router.get('/google/callback',
|
||||
router.get('/google/callback', (req, res, next) => {
|
||||
// Validate state parameter to prevent CSRF
|
||||
const stateFromCookie = req.cookies.oauth_state;
|
||||
const stateFromQuery = req.query.state;
|
||||
|
||||
// Clear the state cookie immediately
|
||||
res.clearCookie('oauth_state');
|
||||
|
||||
if (!stateFromCookie || !stateFromQuery || stateFromCookie !== stateFromQuery) {
|
||||
console.warn('OAuth state mismatch - potential CSRF attack');
|
||||
return res.redirect(`${process.env.FRONTEND_URL}/login?error=invalid_state`);
|
||||
}
|
||||
|
||||
// State is valid, proceed with authentication
|
||||
passport.authenticate('google', {
|
||||
session: false,
|
||||
failureRedirect: `${process.env.FRONTEND_URL}/login?error=auth_failed`
|
||||
}),
|
||||
async (req, res) => {
|
||||
})(req, res, next);
|
||||
}, async (req, res) => {
|
||||
try {
|
||||
const user = req.user;
|
||||
|
||||
|
||||
26
server.js
26
server.js
@ -15,7 +15,11 @@ const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Configuration
|
||||
const MONGO_URI = process.env.MONGO_URI || "mongodb://admin:password123@localhost:27017";
|
||||
if (!process.env.MONGO_URI && process.env.NODE_ENV === 'production') {
|
||||
console.error('❌ MONGO_URI environment variable is required in production');
|
||||
process.exit(1);
|
||||
}
|
||||
const MONGO_URI = process.env.MONGO_URI || "mongodb://localhost:27017";
|
||||
const DB_NAME = "apartments";
|
||||
const UNITS_COLLECTION = "units_migration_test";
|
||||
const PRICES_COLLECTION = "unit_prices_migration_test";
|
||||
@ -80,6 +84,15 @@ const getYesterdayDate = () => {
|
||||
return yesterday.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
// Helper function to validate unit code (prevents NoSQL injection)
|
||||
const isValidUnitCode = (unitCode) => {
|
||||
// Allow alphanumeric characters, hyphens, and underscores, max 20 chars
|
||||
return typeof unitCode === 'string' &&
|
||||
unitCode.length > 0 &&
|
||||
unitCode.length <= 20 &&
|
||||
/^[A-Za-z0-9_-]+$/.test(unitCode);
|
||||
};
|
||||
|
||||
// Mount auth routes
|
||||
app.use('/auth', authRoutes);
|
||||
|
||||
@ -374,6 +387,11 @@ app.get('/unit/:unitCode/price-history', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { unitCode } = req.params;
|
||||
|
||||
// Validate unitCode to prevent NoSQL injection
|
||||
if (!isValidUnitCode(unitCode)) {
|
||||
return res.status(400).json({ error: 'Invalid unit code format' });
|
||||
}
|
||||
|
||||
const priceHistory = await db.collection(PRICES_COLLECTION)
|
||||
.find({ unit_code: unitCode })
|
||||
.sort({ date_checked: 1 })
|
||||
@ -1578,6 +1596,12 @@ app.get('/available-units-enhanced', requireAuth, async (req, res) => {
|
||||
app.get('/unit/:unitCode', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { unitCode } = req.params;
|
||||
|
||||
// Validate unitCode to prevent NoSQL injection
|
||||
if (!isValidUnitCode(unitCode)) {
|
||||
return res.status(400).json({ error: 'Invalid unit code format' });
|
||||
}
|
||||
|
||||
const today = getTodayDate();
|
||||
|
||||
const unit = await db.collection(UNITS_COLLECTION).aggregate([
|
||||
|
||||
Reference in New Issue
Block a user