diff --git a/server/controllers/authController.js b/server/controllers/authController.js new file mode 100644 index 0000000..be42555 --- /dev/null +++ b/server/controllers/authController.js @@ -0,0 +1,106 @@ +import User from "../models/User.js"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import asyncHandler from "express-async-handler"; + + +// @desc Login +// @route POST /auth +// @access Public +const login = asyncHandler(async (req, res) => { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ message: 'All fields are required' }); + }; + + const foundUser = await User.findOne({ username }).exec(); + + if (!foundUser || !foundUser.active) { + return res.status(401).json({ message: 'Unauthorized' }); + }; + + const match = await bcrypt.compare(password, foundUser.password); + + if (!match) return res.status(401).json({ message: 'Unauthorized' }); + + const accessToken = jwt.sign( + { + "UserInfo": { + "username": foundUser.username, + "roles": foundUser.roles + } + }, + process.env.ACCESS_TOKEN_SECRET, + { expiresIn: '15m' } + ); + + const refreshToken = jwt.sign( + { "username": foundUser.username }, + process.env.REFRESH_TOKEN_SECRET, + { expiresIn: '7d' } + ); + + // Create secure cookie with refresh token + res.cookie('jwt', refreshToken, { + httpOnly: true, //accessible only by web server + secure: true, //https + sameSite: 'None', //cross-site cookie + maxAge: 7 * 24 * 60 * 60 * 1000 //cookie expiry: set to match rT + }); + + // Send accessToken containing username and roles + res.json({ accessToken }) +}); + +// @desc Refresh +// @route GET /auth/refresh +// @access Public - because access token has expired +const refresh = (req, res) => { + const cookies = req.cookies + + if (!cookies?.jwt) return res.status(401).json({ message: 'Unauthorized' }); + + const refreshToken = cookies.jwt; + + jwt.verify( + refreshToken, + process.env.REFRESH_TOKEN_SECRET, + asyncHandler(async (err, decoded) => { + if (err) return res.status(403).json({ message: 'Forbidden' }) + + const foundUser = await User.findOne({ username: decoded.username }).exec() + + if (!foundUser) return res.status(401).json({ message: 'Unauthorized' }) + + const accessToken = jwt.sign( + { + "UserInfo": { + "username": foundUser.username, + "roles": foundUser.roles + } + }, + process.env.ACCESS_TOKEN_SECRET, + { expiresIn: '15m' } + ) + + res.json({ accessToken }) + }) + ); +}; + +// @desc Logout +// @route POST /auth/logout +// @access Public - just to clear cookie if exists +const logout = (req, res) => { + const cookies = req.cookies + if (!cookies?.jwt) return res.sendStatus(204) //No content + res.clearCookie('jwt', { httpOnly: true, sameSite: 'None', secure: true }) + res.json({ message: 'Cookie cleared' }) +}; + +export { + login, + refresh, + logout +}; \ No newline at end of file diff --git a/server/index.js b/server/index.js index 059e4b7..0eb3e02 100644 --- a/server/index.js +++ b/server/index.js @@ -11,13 +11,14 @@ import cors from 'cors'; import { corsOptions } from "./config/corsOptions.js"; // Router Imports import { router as rootRouter } from './routes/root.js'; +import { router as authRoutes } from './routes/authRoutes.js'; import { router as userRoutes } from './routes/userRoutes.js'; import { router as noteRoutes } from './routes/noteRoutes.js'; import { connectDB } from './config/dbConn.js'; import mongoose from 'mongoose'; -const __filename = fileURLToPath(import.meta.url); +const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Workaround to get the __dirname in ES modules // Create an express app @@ -41,6 +42,7 @@ app.use('/', express.static(`${__dirname}/public`)); // Any request to the root // Route Handling app.use('/', rootRouter); +app.use('/auth', authRoutes); app.use('/users', userRoutes); app.use('/notes', noteRoutes); @@ -50,7 +52,7 @@ app.all('*', (req, res) => { if (req.accepts('html')) { res.sendFile(`${__dirname}/views/404.html`); } else if (req.accepts('json')) { - res.json({message: '404 Not Found'}); + res.json({ message: '404 Not Found' }); } else { res.type('txt').send('404 Not Found'); } @@ -61,7 +63,7 @@ app.use(errorHandler); // Placed at the end to catch any errors that occur in th mongoose.connection.once('open', () => { // Starts server and listens on the specified port - app.listen(PORT, () => { + app.listen(PORT, () => { console.log('Connected to MongoDB'); console.log(`Server running on port ${PORT}`); }); diff --git a/server/middleware/loginLimiter.js b/server/middleware/loginLimiter.js new file mode 100644 index 0000000..7594758 --- /dev/null +++ b/server/middleware/loginLimiter.js @@ -0,0 +1,17 @@ +import rateLimit from "express-rate-limit"; +import { logEvents } from "./logger.js"; + +const loginLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 5, // Limit each IP to 5 login requests per `window` per minute + message: + { message: 'Too many login attempts from this IP, please try again after a 60 second pause' }, + handler: (req, res, next, options) => { + logEvents(`Too Many Requests: ${options.message.message}\t${req.method}\t${req.url}\t${req.headers.origin}`, 'errLog.log') + res.status(options.statusCode).send(options.message) + }, + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers +}) + +export { loginLimiter }; \ No newline at end of file diff --git a/server/middleware/verifyJWT.js b/server/middleware/verifyJWT.js index 16923c6..0ee07bc 100644 --- a/server/middleware/verifyJWT.js +++ b/server/middleware/verifyJWT.js @@ -31,4 +31,4 @@ const verifyJWT = (req, res, next) => { }; // Export the middleware function -export { verifyJWT }; \ No newline at end of file +export default verifyJWT; \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 7d6b428..b3c5078 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -16,6 +16,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "express-async-handler": "^1.2.0", + "express-rate-limit": "^7.3.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.4.1", "mongoose-sequence": "^6.0.1", @@ -568,6 +569,20 @@ "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==" }, + "node_modules/express-rate-limit": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.1.tgz", + "integrity": "sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "4 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", diff --git a/server/package.json b/server/package.json index a673be5..a96da4e 100644 --- a/server/package.json +++ b/server/package.json @@ -19,6 +19,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "express-async-handler": "^1.2.0", + "express-rate-limit": "^7.3.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.4.1", "mongoose-sequence": "^6.0.1", diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js new file mode 100644 index 0000000..6919d55 --- /dev/null +++ b/server/routes/authRoutes.js @@ -0,0 +1,17 @@ +import express from "express"; +import * as authController from "../controllers/authController.js"; +import { loginLimiter } from "../middleware/loginLimiter.js"; + +const router = express.Router(); + +router.route('/') + .post(loginLimiter, authController.login); + +router.route('/refresh') + .get(authController.refresh); + +router.route('/logout') + .post(authController.logout); + +// Export the router instance +export { router }; \ No newline at end of file diff --git a/server/routes/noteRoutes.js b/server/routes/noteRoutes.js index 2b564a1..ea359a4 100644 --- a/server/routes/noteRoutes.js +++ b/server/routes/noteRoutes.js @@ -1,10 +1,13 @@ // Import necessary modules import express from "express"; import * as notesController from '../controllers/notesController.js'; +import verifyJWT from "../middleware/verifyJWT.js"; // Create an instance of Express Router const router = express.Router(); +router.use(verifyJWT); + router.route('/') // we are at /notes .get(notesController.getAllNotes) // READ .post(notesController.createNewNote) // CREATE diff --git a/server/routes/userRoutes.js b/server/routes/userRoutes.js index 65e9746..5b2e5f7 100644 --- a/server/routes/userRoutes.js +++ b/server/routes/userRoutes.js @@ -1,10 +1,13 @@ // Import necessary modules import express from "express"; import * as usersController from '../controllers/usersController.js'; +import verifyJWT from "../middleware/verifyJWT.js"; // Create an instance of Express Router const router = express.Router(); +router.use(verifyJWT); + router.route('/') // we are at /users .get(usersController.getAllUsers) // READ .post(usersController.createNewUser) // CREATE