Add Google OAuth authentication infrastructure
- Add passport, passport-google-oauth20, jsonwebtoken, cookie-parser, uuid deps - Create config/auth.js with JWT, cookie, and OAuth configuration - Create models/user.js with MongoDB user model and indexes - Create middleware/passport.js with Google OAuth strategy - Create middleware/auth.js with requireAuth middleware and sliding window refresh - Create routes/auth.js with OAuth flow endpoints - Update server.js to integrate auth, protect all data endpoints (except /health) - Configure CORS for cookie-based authentication
This commit is contained in:
111
middleware/auth.js
Normal file
111
middleware/auth.js
Normal file
@ -0,0 +1,111 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const authConfig = require('../config/auth');
|
||||
const { findById } = require('../models/user');
|
||||
|
||||
/**
|
||||
* Check if a token should be refreshed based on its age
|
||||
* @param {Object} decoded - Decoded JWT payload
|
||||
* @returns {boolean} True if token should be refreshed
|
||||
*/
|
||||
const shouldRefreshToken = (decoded) => {
|
||||
const tokenAge = Math.floor(Date.now() / 1000) - decoded.iat;
|
||||
return tokenAge > authConfig.jwt.refreshThreshold;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a new JWT token for a user
|
||||
* @param {string|ObjectId} userId - User's MongoDB _id
|
||||
* @returns {string} JWT token
|
||||
*/
|
||||
const generateToken = (userId) => {
|
||||
return jwt.sign(
|
||||
{ userId: userId.toString() },
|
||||
authConfig.jwt.secret,
|
||||
{ expiresIn: authConfig.jwt.expiresIn }
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Authentication middleware
|
||||
* Validates JWT token from cookie and attaches user to request
|
||||
* Implements sliding window token refresh
|
||||
*
|
||||
* @param {Request} req - Express request object
|
||||
* @param {Response} res - Express response object
|
||||
* @param {Function} next - Express next function
|
||||
*/
|
||||
const requireAuth = async (req, res, next) => {
|
||||
const token = req.cookies[authConfig.cookie.name];
|
||||
|
||||
// No token present
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify the token
|
||||
const decoded = jwt.verify(token, authConfig.jwt.secret);
|
||||
|
||||
// Get database instance
|
||||
const db = req.app.locals.db;
|
||||
|
||||
// Fetch user from database
|
||||
const user = await findById(db, decoded.userId);
|
||||
|
||||
// User not found
|
||||
if (!user) {
|
||||
res.clearCookie(authConfig.cookie.name);
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
// User is disabled (silent logout)
|
||||
if (!user.isActive) {
|
||||
res.clearCookie(authConfig.cookie.name);
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
// Attach user to request
|
||||
req.user = user;
|
||||
|
||||
// Sliding window token refresh
|
||||
if (shouldRefreshToken(decoded)) {
|
||||
const newToken = generateToken(user._id);
|
||||
res.cookie(authConfig.cookie.name, newToken, authConfig.cookie.options);
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
// Token verification failed (invalid or expired)
|
||||
res.clearCookie(authConfig.cookie.name);
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Admin authorization middleware
|
||||
* Must be used after requireAuth middleware
|
||||
* Checks if authenticated user has admin role
|
||||
*
|
||||
* @param {Request} req - Express request object
|
||||
* @param {Response} res - Express response object
|
||||
* @param {Function} next - Express next function
|
||||
*/
|
||||
const requireAdmin = (req, res, next) => {
|
||||
// Check if user is attached (requireAuth should be called first)
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
// Check if user has admin role
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
requireAuth,
|
||||
requireAdmin,
|
||||
generateToken
|
||||
};
|
||||
29
middleware/passport.js
Normal file
29
middleware/passport.js
Normal file
@ -0,0 +1,29 @@
|
||||
const passport = require('passport');
|
||||
const GoogleStrategy = require('passport-google-oauth20').Strategy;
|
||||
const authConfig = require('../config/auth');
|
||||
const { findOrCreateUser } = require('../models/user');
|
||||
|
||||
const configurePassport = (passport, db) => {
|
||||
passport.use(new GoogleStrategy({
|
||||
clientID: authConfig.google.clientID,
|
||||
clientSecret: authConfig.google.clientSecret,
|
||||
callbackURL: authConfig.google.callbackURL
|
||||
},
|
||||
async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
const userProfile = {
|
||||
googleId: profile.id,
|
||||
email: profile.emails[0].value,
|
||||
name: profile.displayName,
|
||||
picture: profile.photos?.[0]?.value || null
|
||||
};
|
||||
|
||||
const user = await findOrCreateUser(db, userProfile);
|
||||
done(null, user);
|
||||
} catch (error) {
|
||||
done(error, null);
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
module.exports = { configurePassport };
|
||||
Reference in New Issue
Block a user