Skip to content

Commit

Permalink
Implemented authentication and authorization features
Browse files Browse the repository at this point in the history
  • Loading branch information
absolute-xero7 committed Jun 9, 2024
1 parent 8c471df commit cec7f3c
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 4 deletions.
106 changes: 106 additions & 0 deletions server/controllers/authController.js
Original file line number Diff line number Diff line change
@@ -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
};
8 changes: 5 additions & 3 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand All @@ -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');
}
Expand All @@ -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}`);
});
Expand Down
17 changes: 17 additions & 0 deletions server/middleware/loginLimiter.js
Original file line number Diff line number Diff line change
@@ -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 };
2 changes: 1 addition & 1 deletion server/middleware/verifyJWT.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ const verifyJWT = (req, res, next) => {
};

// Export the middleware function
export { verifyJWT };
export default verifyJWT;
15 changes: 15 additions & 0 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions server/routes/authRoutes.js
Original file line number Diff line number Diff line change
@@ -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 };
3 changes: 3 additions & 0 deletions server/routes/noteRoutes.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions server/routes/userRoutes.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit cec7f3c

Please sign in to comment.