Add Google OAuth authentication system #4
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: .
|
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"
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
26
server.js
26
server.js
@ -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([
|
||||||
|
|||||||
Reference in New Issue
Block a user