Add Google OAuth authentication system #4

Merged
stephen merged 6 commits from dev into main 2026-01-21 19:15:22 -07:00
12 changed files with 1141 additions and 34 deletions
Showing only changes of commit af52c33617 - Show all commits

20
.env.example Normal file
View 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

View File

@ -3,10 +3,18 @@ services:
build: . build: .
container_name: apartment-api container_name: apartment-api
restart: unless-stopped restart: unless-stopped
env_file:
- .env
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- PORT=8080 # Changed from 3000 to 8080 - PORT=8080
- MONGO_URI=mongodb://admin:password123@mongodb:27017 - 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: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.docker.network=traefik" - "traefik.docker.network=traefik"

View File

@ -1,6 +1,7 @@
const express = require('express'); const express = require('express');
const passport = require('passport'); const passport = require('passport');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const authConfig = require('../config/auth'); const authConfig = require('../config/auth');
const { requireAuth, generateToken } = require('../middleware/auth'); const { requireAuth, generateToken } = require('../middleware/auth');
const { logActivity, ACTIONS } = require('../services/activityLogger'); const { logActivity, ACTIONS } = require('../services/activityLogger');
@ -9,25 +10,53 @@ const router = express.Router();
/** /**
* GET /auth/google * GET /auth/google
* Initiates Google OAuth flow * Initiates Google OAuth flow with state parameter for CSRF protection
*/ */
router.get('/google', passport.authenticate('google', { 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, scope: authConfig.google.scope,
session: false session: false,
})); state: state
})(req, res, next);
});
/** /**
* GET /auth/google/callback * GET /auth/google/callback
* Handles OAuth callback from Google * Handles OAuth callback from Google
* Validates state parameter for CSRF protection
* On success: generates JWT, sets cookie, redirects to frontend * On success: generates JWT, sets cookie, redirects to frontend
* On failure: redirects to login with error * 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', { passport.authenticate('google', {
session: false, session: false,
failureRedirect: `${process.env.FRONTEND_URL}/login?error=auth_failed` failureRedirect: `${process.env.FRONTEND_URL}/login?error=auth_failed`
}), })(req, res, next);
async (req, res) => { }, async (req, res) => {
try { try {
const user = req.user; const user = req.user;

View File

@ -15,7 +15,11 @@ const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
// Configuration // 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 DB_NAME = "apartments";
const UNITS_COLLECTION = "units_migration_test"; const UNITS_COLLECTION = "units_migration_test";
const PRICES_COLLECTION = "unit_prices_migration_test"; const PRICES_COLLECTION = "unit_prices_migration_test";
@ -80,6 +84,15 @@ const getYesterdayDate = () => {
return yesterday.toISOString().split('T')[0]; 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 // Mount auth routes
app.use('/auth', authRoutes); app.use('/auth', authRoutes);
@ -374,6 +387,11 @@ app.get('/unit/:unitCode/price-history', requireAuth, async (req, res) => {
try { try {
const { unitCode } = req.params; 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) const priceHistory = await db.collection(PRICES_COLLECTION)
.find({ unit_code: unitCode }) .find({ unit_code: unitCode })
.sort({ date_checked: 1 }) .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) => { app.get('/unit/:unitCode', requireAuth, async (req, res) => {
try { try {
const { unitCode } = req.params; 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 today = getTodayDate();
const unit = await db.collection(UNITS_COLLECTION).aggregate([ const unit = await db.collection(UNITS_COLLECTION).aggregate([