From 0480f8b963df00155d67f8b5e79ed7bcda062c10 Mon Sep 17 00:00:00 2001 From: jydxh Date: Fri, 2 Aug 2024 10:11:30 -0400 Subject: [PATCH 1/2] fix the refreshToken never expire bug, also make sure refreshToken will be changed and saved in DB everytime user log in --- .../server/controllers/authController.js | 390 ++++++++++-------- .../final/server/middleware/authentication.js | 85 ++-- 11-auth-workflow/final/server/models/Token.js | 31 +- 11-auth-workflow/final/server/utils/jwt.js | 86 ++-- 4 files changed, 319 insertions(+), 273 deletions(-) diff --git a/11-auth-workflow/final/server/controllers/authController.js b/11-auth-workflow/final/server/controllers/authController.js index d8cf692d50..f64666ba27 100644 --- a/11-auth-workflow/final/server/controllers/authController.js +++ b/11-auth-workflow/final/server/controllers/authController.js @@ -1,200 +1,226 @@ -const User = require('../models/User'); -const Token = require('../models/Token'); -const { StatusCodes } = require('http-status-codes'); -const CustomError = require('../errors'); +const User = require('../models/User') +const Token = require('../models/Token') +const { StatusCodes } = require('http-status-codes') +const CustomError = require('../errors') const { - attachCookiesToResponse, - createTokenUser, - sendVerificationEmail, - sendResetPasswordEmail, - createHash, -} = require('../utils'); -const crypto = require('crypto'); + attachCookiesToResponse, + createTokenUser, + sendVerificationEmail, + sendResetPasswordEmail, + createHash, +} = require('../utils') +const crypto = require('crypto') const register = async (req, res) => { - const { email, name, password } = req.body; - - const emailAlreadyExists = await User.findOne({ email }); - if (emailAlreadyExists) { - throw new CustomError.BadRequestError('Email already exists'); - } - - // first registered user is an admin - const isFirstAccount = (await User.countDocuments({})) === 0; - const role = isFirstAccount ? 'admin' : 'user'; - - const verificationToken = crypto.randomBytes(40).toString('hex'); - - const user = await User.create({ - name, - email, - password, - role, - verificationToken, - }); - const origin = 'http://localhost:3000'; - // const newOrigin = 'https://react-node-user-workflow-front-end.netlify.app'; - - // const tempOrigin = req.get('origin'); - // const protocol = req.protocol; - // const host = req.get('host'); - // const forwardedHost = req.get('x-forwarded-host'); - // const forwardedProtocol = req.get('x-forwarded-proto'); - - await sendVerificationEmail({ - name: user.name, - email: user.email, - verificationToken: user.verificationToken, - origin, - }); - // send verification token back only while testing in postman!!! - res.status(StatusCodes.CREATED).json({ - msg: 'Success! Please check your email to verify account', - }); -}; + const { email, name, password } = req.body + + const emailAlreadyExists = await User.findOne({ email }) + if (emailAlreadyExists) { + throw new CustomError.BadRequestError('Email already exists') + } + + // first registered user is an admin + const isFirstAccount = (await User.countDocuments({})) === 0 + const role = isFirstAccount ? 'admin' : 'user' + + const verificationToken = crypto.randomBytes(40).toString('hex') + + const user = await User.create({ + name, + email, + password, + role, + verificationToken, + }) + const origin = 'http://localhost:3000' + // const newOrigin = 'https://react-node-user-workflow-front-end.netlify.app'; + + // const tempOrigin = req.get('origin'); + // const protocol = req.protocol; + // const host = req.get('host'); + // const forwardedHost = req.get('x-forwarded-host'); + // const forwardedProtocol = req.get('x-forwarded-proto'); + + await sendVerificationEmail({ + name: user.name, + email: user.email, + verificationToken: user.verificationToken, + origin, + }) + // send verification token back only while testing in postman!!! + res.status(StatusCodes.CREATED).json({ + msg: 'Success! Please check your email to verify account', + }) +} const verifyEmail = async (req, res) => { - const { verificationToken, email } = req.body; - const user = await User.findOne({ email }); + const { verificationToken, email } = req.body + const user = await User.findOne({ email }) - if (!user) { - throw new CustomError.UnauthenticatedError('Verification Failed'); - } + if (!user) { + throw new CustomError.UnauthenticatedError('Verification Failed') + } - if (user.verificationToken !== verificationToken) { - throw new CustomError.UnauthenticatedError('Verification Failed'); - } + if (user.verificationToken !== verificationToken) { + throw new CustomError.UnauthenticatedError('Verification Failed') + } - (user.isVerified = true), (user.verified = Date.now()); - user.verificationToken = ''; + ;(user.isVerified = true), (user.verified = Date.now()) + user.verificationToken = '' - await user.save(); + await user.save() - res.status(StatusCodes.OK).json({ msg: 'Email Verified' }); -}; + res.status(StatusCodes.OK).json({ msg: 'Email Verified' }) +} const login = async (req, res) => { - const { email, password } = req.body; - - if (!email || !password) { - throw new CustomError.BadRequestError('Please provide email and password'); - } - const user = await User.findOne({ email }); - - if (!user) { - throw new CustomError.UnauthenticatedError('Invalid Credentials'); - } - const isPasswordCorrect = await user.comparePassword(password); - - if (!isPasswordCorrect) { - throw new CustomError.UnauthenticatedError('Invalid Credentials'); - } - if (!user.isVerified) { - throw new CustomError.UnauthenticatedError('Please verify your email'); - } - const tokenUser = createTokenUser(user); - - // create refresh token - let refreshToken = ''; - // check for existing token - const existingToken = await Token.findOne({ user: user._id }); - - if (existingToken) { - const { isValid } = existingToken; - if (!isValid) { - throw new CustomError.UnauthenticatedError('Invalid Credentials'); - } - refreshToken = existingToken.refreshToken; - attachCookiesToResponse({ res, user: tokenUser, refreshToken }); - res.status(StatusCodes.OK).json({ user: tokenUser }); - return; - } - - refreshToken = crypto.randomBytes(40).toString('hex'); - const userAgent = req.headers['user-agent']; - const ip = req.ip; - const userToken = { refreshToken, ip, userAgent, user: user._id }; - - await Token.create(userToken); - - attachCookiesToResponse({ res, user: tokenUser, refreshToken }); - - res.status(StatusCodes.OK).json({ user: tokenUser }); -}; + const { email, password } = req.body + + if (!email || !password) { + throw new CustomError.BadRequestError('Please provide email and password') + } + const user = await User.findOne({ email }) + + if (!user) { + throw new CustomError.UnauthenticatedError('Invalid Credentials') + } + const isPasswordCorrect = await user.comparePassword(password) + + if (!isPasswordCorrect) { + throw new CustomError.UnauthenticatedError('Invalid Credentials') + } + if (!user.isVerified) { + throw new CustomError.UnauthenticatedError('Please verify your email') + } + const tokenUser = createTokenUser(user) + + // create refresh token + let refreshToken = '' + // check for existing token + const existingToken = await Token.findOne({ + user: user._id, + userAgent: req.headers['user-agent'], // add userAgent so, different devices will get different token + }) + + if (existingToken) { + const { isValid } = existingToken + if (!isValid) { + throw new CustomError.UnauthenticatedError('Invalid Credentials') + } + //since this is login, expired or not dosen't matter, so just renew the exiredIn + existingToken.expiredIn = Date.now() + 1000 * 60 * 60 * 24 * 30 //30days + //also update the refreshToken for safe + existingToken.refreshToken = crypto.randomBytes(40).toString('hex') + const token = await existingToken.save() + refreshToken = token.refreshToken + await attachCookiesToResponse({ + res, + user: tokenUser, + refreshToken, + expiresIn: token.expiredIn, + }) + return res.status(StatusCodes.OK).json({ user: tokenUser }) + } + + refreshToken = crypto.randomBytes(40).toString('hex') + const userAgent = req.headers['user-agent'] + const ip = req.ip + const userToken = { + refreshToken, + ip, + userAgent, + user: user._id, + expiredIn: Date.now() + 1000 * 60 * 60 * 24 * 30, + } + + const token = await Token.create(userToken) + + await attachCookiesToResponse({ + res, + user: tokenUser, + refreshToken, + expiresIn: token.expiredIn, + }) + res.status(StatusCodes.OK).json({ user: tokenUser }) +} + const logout = async (req, res) => { - await Token.findOneAndDelete({ user: req.user.userId }); - - res.cookie('accessToken', 'logout', { - httpOnly: true, - expires: new Date(Date.now()), - }); - res.cookie('refreshToken', 'logout', { - httpOnly: true, - expires: new Date(Date.now()), - }); - res.status(StatusCodes.OK).json({ msg: 'user logged out!' }); -}; + await Token.findOneAndDelete({ + user: req.user.userId, + userAgent: req.headers['user-agent'], + }) + + res.cookie('accessToken', 'logout', { + httpOnly: true, + expires: new Date(Date.now()), + }) + res.cookie('refreshToken', 'logout', { + httpOnly: true, + expires: new Date(Date.now()), + }) + res.status(StatusCodes.OK).json({ msg: 'user logged out!' }) +} const forgotPassword = async (req, res) => { - const { email } = req.body; - if (!email) { - throw new CustomError.BadRequestError('Please provide valid email'); - } - - const user = await User.findOne({ email }); - - if (user) { - const passwordToken = crypto.randomBytes(70).toString('hex'); - // send email - const origin = 'http://localhost:3000'; - await sendResetPasswordEmail({ - name: user.name, - email: user.email, - token: passwordToken, - origin, - }); - - const tenMinutes = 1000 * 60 * 10; - const passwordTokenExpirationDate = new Date(Date.now() + tenMinutes); - - user.passwordToken = createHash(passwordToken); - user.passwordTokenExpirationDate = passwordTokenExpirationDate; - await user.save(); - } - - res - .status(StatusCodes.OK) - .json({ msg: 'Please check your email for reset password link' }); -}; + const { email } = req.body + if (!email) { + throw new CustomError.BadRequestError('Please provide valid email') + } + + const user = await User.findOne({ email }) + + if (user) { + const passwordToken = crypto.randomBytes(70).toString('hex') + // send email + const origin = 'http://localhost:3000' + await sendResetPasswordEmail({ + name: user.name, + email: user.email, + token: passwordToken, + origin, + }) + + const tenMinutes = 1000 * 60 * 10 + const passwordTokenExpirationDate = new Date(Date.now() + tenMinutes) + + user.passwordToken = createHash(passwordToken) + user.passwordTokenExpirationDate = passwordTokenExpirationDate + await user.save() + } + + res + .status(StatusCodes.OK) + .json({ msg: 'Please check your email for reset password link' }) +} const resetPassword = async (req, res) => { - const { token, email, password } = req.body; - if (!token || !email || !password) { - throw new CustomError.BadRequestError('Please provide all values'); - } - const user = await User.findOne({ email }); - - if (user) { - const currentDate = new Date(); - - if ( - user.passwordToken === createHash(token) && - user.passwordTokenExpirationDate > currentDate - ) { - user.password = password; - user.passwordToken = null; - user.passwordTokenExpirationDate = null; - await user.save(); - } - } - - res.send('reset password'); -}; + const { token, email, password } = req.body + if (!token || !email || !password) { + throw new CustomError.BadRequestError('Please provide all values') + } + const user = await User.findOne({ email }) + + if (user) { + const currentDate = new Date() + + if ( + user.passwordToken === createHash(token) && + user.passwordTokenExpirationDate > currentDate + ) { + user.password = password + user.passwordToken = null + user.passwordTokenExpirationDate = null + await user.save() + } + } + + res.send('reset password') +} module.exports = { - register, - login, - logout, - verifyEmail, - forgotPassword, - resetPassword, -}; + register, + login, + logout, + verifyEmail, + forgotPassword, + resetPassword, +} diff --git a/11-auth-workflow/final/server/middleware/authentication.js b/11-auth-workflow/final/server/middleware/authentication.js index 2dc4121a21..bcae7e7a6d 100644 --- a/11-auth-workflow/final/server/middleware/authentication.js +++ b/11-auth-workflow/final/server/middleware/authentication.js @@ -1,52 +1,53 @@ -const CustomError = require('../errors'); -const { isTokenValid } = require('../utils'); -const Token = require('../models/Token'); -const { attachCookiesToResponse } = require('../utils'); +const CustomError = require('../errors') +const { isTokenValid } = require('../utils') +const Token = require('../models/Token') +const { attachCookiesToResponse } = require('../utils') const authenticateUser = async (req, res, next) => { - const { refreshToken, accessToken } = req.signedCookies; + const { refreshToken, accessToken } = req.signedCookies - try { - if (accessToken) { - const payload = isTokenValid(accessToken); - req.user = payload.user; - return next(); - } - const payload = isTokenValid(refreshToken); + try { + if (accessToken) { + const payload = await isTokenValid(accessToken) + req.user = payload.user + return next() + } + const payload = await isTokenValid(refreshToken) - const existingToken = await Token.findOne({ - user: payload.user.userId, - refreshToken: payload.refreshToken, - }); + const existingToken = await Token.findOne({ + user: payload.user.userId, + refreshToken: payload.refreshToken, + }) - if (!existingToken || !existingToken?.isValid) { - throw new CustomError.UnauthenticatedError('Authentication Invalid'); - } + if (!existingToken || !existingToken?.isValid) { + throw new CustomError.UnauthenticatedError('Authentication Invalid') + } - attachCookiesToResponse({ - res, - user: payload.user, - refreshToken: existingToken.refreshToken, - }); + await attachCookiesToResponse({ + res, + user: payload.user, + refreshToken: existingToken.refreshToken, + expiresIn: existingToken.expiredIn, + }) - req.user = payload.user; - next(); - } catch (error) { - throw new CustomError.UnauthenticatedError('Authentication Invalid'); - } -}; + req.user = payload.user + next() + } catch (error) { + throw new CustomError.UnauthenticatedError('Authentication Invalid') + } +} const authorizePermissions = (...roles) => { - return (req, res, next) => { - if (!roles.includes(req.user.role)) { - throw new CustomError.UnauthorizedError( - 'Unauthorized to access this route' - ); - } - next(); - }; -}; + return (req, res, next) => { + if (!roles.includes(req.user.role)) { + throw new CustomError.UnauthorizedError( + 'Unauthorized to access this route' + ) + } + next() + } +} module.exports = { - authenticateUser, - authorizePermissions, -}; + authenticateUser, + authorizePermissions, +} diff --git a/11-auth-workflow/final/server/models/Token.js b/11-auth-workflow/final/server/models/Token.js index afc40f1282..e0484dc729 100644 --- a/11-auth-workflow/final/server/models/Token.js +++ b/11-auth-workflow/final/server/models/Token.js @@ -1,18 +1,19 @@ -const mongoose = require('mongoose'); +const mongoose = require('mongoose') const TokenSchema = new mongoose.Schema( - { - refreshToken: { type: String, required: true }, - ip: { type: String, required: true }, - userAgent: { type: String, required: true }, - isValid: { type: Boolean, default: true }, - user: { - type: mongoose.Types.ObjectId, - ref: 'User', - required: true, - }, - }, - { timestamps: true } -); + { + refreshToken: { type: String, required: true }, + ip: { type: String, required: true }, + userAgent: { type: String, required: true }, + isValid: { type: Boolean, default: true }, + user: { + type: mongoose.Types.ObjectId, + ref: 'User', + required: true, + }, + expiredIn: { type: Date, required: true }, + }, + { timestamps: true } +) -module.exports = mongoose.model('Token', TokenSchema); +module.exports = mongoose.model('Token', TokenSchema) diff --git a/11-auth-workflow/final/server/utils/jwt.js b/11-auth-workflow/final/server/utils/jwt.js index c7c60783d9..142a556bce 100644 --- a/11-auth-workflow/final/server/utils/jwt.js +++ b/11-auth-workflow/final/server/utils/jwt.js @@ -1,33 +1,51 @@ -const jwt = require('jsonwebtoken'); - -const createJWT = ({ payload }) => { - const token = jwt.sign(payload, process.env.JWT_SECRET); - return token; -}; - -const isTokenValid = (token) => jwt.verify(token, process.env.JWT_SECRET); - -const attachCookiesToResponse = ({ res, user, refreshToken }) => { - const accessTokenJWT = createJWT({ payload: { user } }); - const refreshTokenJWT = createJWT({ payload: { user, refreshToken } }); - - const oneDay = 1000 * 60 * 60 * 24; - const longerExp = 1000 * 60 * 60 * 24 * 30; - - res.cookie('accessToken', accessTokenJWT, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - signed: true, - expires: new Date(Date.now() + oneDay), - }); - - res.cookie('refreshToken', refreshTokenJWT, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - signed: true, - expires: new Date(Date.now() + longerExp), - }); -}; +const jwt = require('jsonwebtoken') +const { promisify } = require('util') +const jwtVerify = promisify(jwt.verify) +const jwtSign = promisify(jwt.sign) + +const createJWT = async ({ payload }) => + await jwtSign(payload, process.env.JWT_SECRET, { + expiresIn: payload.expiresIn, + }) + +const isTokenValid = async token => + await jwtVerify(token, process.env.JWT_SECRET) + +const attachCookiesToResponse = async ({ + res, + user, + refreshToken, + expiresIn, +}) => { + const accessTokenJWT = await createJWT({ + payload: { user, expiresIn: 60 * 15 }, + }) //set accessJWT 15 mins valid + const refreshTokenJWT = await createJWT({ + payload: { + user, + refreshToken, + expiresIn: new Date(expiresIn).getTime() - Date.now() - 5 * 60, + // refreshJWT will be expired 5min ahead the expiredTime saved in DB + }, + }) + + const oneDay = 1000 * 60 * 60 * 24 + const longerExp = 1000 * 60 * 60 * 24 * 30 + + res.cookie('accessToken', accessTokenJWT, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + signed: true, + expires: new Date(Date.now() + 1000 * 60 * 15), //15min + }) + + res.cookie('refreshToken', refreshTokenJWT, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + signed: true, + expires: new Date(expiresIn - 1000 * 60 * 5), // if use Date.now() as reference, the refreshToken will never be expired, which is a bug, so add an expiredIn attribute in Token Schema, and use that as reference, whenever the refreshToken refresh, it's live can also be refreshed correct + }) +} // const attachSingleCookieToResponse = ({ res, user }) => { // const token = createJWT({ payload: user }); @@ -42,7 +60,7 @@ const attachCookiesToResponse = ({ res, user, refreshToken }) => { // }; module.exports = { - createJWT, - isTokenValid, - attachCookiesToResponse, -}; + createJWT, + isTokenValid, + attachCookiesToResponse, +} From ff2aaa980dea1de5b0748968278a824e8bc5704f Mon Sep 17 00:00:00 2001 From: jydxh Date: Fri, 2 Aug 2024 10:19:08 -0400 Subject: [PATCH 2/2] format --- .../server/controllers/authController.js | 162 +++++++++--------- .../final/server/middleware/authentication.js | 42 ++--- 11-auth-workflow/final/server/utils/jwt.js | 28 +-- 3 files changed, 116 insertions(+), 116 deletions(-) diff --git a/11-auth-workflow/final/server/controllers/authController.js b/11-auth-workflow/final/server/controllers/authController.js index f64666ba27..7e84e53acd 100644 --- a/11-auth-workflow/final/server/controllers/authController.js +++ b/11-auth-workflow/final/server/controllers/authController.js @@ -1,29 +1,29 @@ -const User = require('../models/User') -const Token = require('../models/Token') -const { StatusCodes } = require('http-status-codes') -const CustomError = require('../errors') +const User = require('../models/User'); +const Token = require('../models/Token'); +const { StatusCodes } = require('http-status-codes'); +const CustomError = require('../errors'); const { attachCookiesToResponse, createTokenUser, sendVerificationEmail, sendResetPasswordEmail, createHash, -} = require('../utils') -const crypto = require('crypto') +} = require('../utils'); +const crypto = require('crypto'); const register = async (req, res) => { - const { email, name, password } = req.body + const { email, name, password } = req.body; - const emailAlreadyExists = await User.findOne({ email }) + const emailAlreadyExists = await User.findOne({ email }); if (emailAlreadyExists) { - throw new CustomError.BadRequestError('Email already exists') + throw new CustomError.BadRequestError('Email already exists'); } // first registered user is an admin - const isFirstAccount = (await User.countDocuments({})) === 0 - const role = isFirstAccount ? 'admin' : 'user' + const isFirstAccount = (await User.countDocuments({})) === 0; + const role = isFirstAccount ? 'admin' : 'user'; - const verificationToken = crypto.randomBytes(40).toString('hex') + const verificationToken = crypto.randomBytes(40).toString('hex'); const user = await User.create({ name, @@ -31,8 +31,8 @@ const register = async (req, res) => { password, role, verificationToken, - }) - const origin = 'http://localhost:3000' + }); + const origin = 'http://localhost:3000'; // const newOrigin = 'https://react-node-user-workflow-front-end.netlify.app'; // const tempOrigin = req.get('origin'); @@ -46,175 +46,175 @@ const register = async (req, res) => { email: user.email, verificationToken: user.verificationToken, origin, - }) + }); // send verification token back only while testing in postman!!! res.status(StatusCodes.CREATED).json({ msg: 'Success! Please check your email to verify account', - }) -} + }); +}; const verifyEmail = async (req, res) => { - const { verificationToken, email } = req.body - const user = await User.findOne({ email }) + const { verificationToken, email } = req.body; + const user = await User.findOne({ email }); if (!user) { - throw new CustomError.UnauthenticatedError('Verification Failed') + throw new CustomError.UnauthenticatedError('Verification Failed'); } if (user.verificationToken !== verificationToken) { - throw new CustomError.UnauthenticatedError('Verification Failed') + throw new CustomError.UnauthenticatedError('Verification Failed'); } - ;(user.isVerified = true), (user.verified = Date.now()) - user.verificationToken = '' + (user.isVerified = true), (user.verified = Date.now()); + user.verificationToken = ''; - await user.save() + await user.save(); - res.status(StatusCodes.OK).json({ msg: 'Email Verified' }) -} + res.status(StatusCodes.OK).json({ msg: 'Email Verified' }); +}; const login = async (req, res) => { - const { email, password } = req.body + const { email, password } = req.body; if (!email || !password) { - throw new CustomError.BadRequestError('Please provide email and password') + throw new CustomError.BadRequestError('Please provide email and password'); } - const user = await User.findOne({ email }) + const user = await User.findOne({ email }); if (!user) { - throw new CustomError.UnauthenticatedError('Invalid Credentials') + throw new CustomError.UnauthenticatedError('Invalid Credentials'); } - const isPasswordCorrect = await user.comparePassword(password) + const isPasswordCorrect = await user.comparePassword(password); if (!isPasswordCorrect) { - throw new CustomError.UnauthenticatedError('Invalid Credentials') + throw new CustomError.UnauthenticatedError('Invalid Credentials'); } if (!user.isVerified) { - throw new CustomError.UnauthenticatedError('Please verify your email') + throw new CustomError.UnauthenticatedError('Please verify your email'); } - const tokenUser = createTokenUser(user) + const tokenUser = createTokenUser(user); // create refresh token - let refreshToken = '' + let refreshToken = ''; // check for existing token const existingToken = await Token.findOne({ user: user._id, userAgent: req.headers['user-agent'], // add userAgent so, different devices will get different token - }) + }); if (existingToken) { - const { isValid } = existingToken + const { isValid } = existingToken; if (!isValid) { - throw new CustomError.UnauthenticatedError('Invalid Credentials') + throw new CustomError.UnauthenticatedError('Invalid Credentials'); } //since this is login, expired or not dosen't matter, so just renew the exiredIn - existingToken.expiredIn = Date.now() + 1000 * 60 * 60 * 24 * 30 //30days + existingToken.expiredIn = Date.now() + 1000 * 60 * 60 * 24 * 30; //30days //also update the refreshToken for safe - existingToken.refreshToken = crypto.randomBytes(40).toString('hex') - const token = await existingToken.save() - refreshToken = token.refreshToken + existingToken.refreshToken = crypto.randomBytes(40).toString('hex'); + const token = await existingToken.save(); + refreshToken = token.refreshToken; await attachCookiesToResponse({ res, user: tokenUser, refreshToken, expiresIn: token.expiredIn, - }) - return res.status(StatusCodes.OK).json({ user: tokenUser }) + }); + return res.status(StatusCodes.OK).json({ user: tokenUser }); } - refreshToken = crypto.randomBytes(40).toString('hex') - const userAgent = req.headers['user-agent'] - const ip = req.ip + refreshToken = crypto.randomBytes(40).toString('hex'); + const userAgent = req.headers['user-agent']; + const ip = req.ip; const userToken = { refreshToken, ip, userAgent, user: user._id, expiredIn: Date.now() + 1000 * 60 * 60 * 24 * 30, - } + }; - const token = await Token.create(userToken) + const token = await Token.create(userToken); await attachCookiesToResponse({ res, user: tokenUser, refreshToken, expiresIn: token.expiredIn, - }) - res.status(StatusCodes.OK).json({ user: tokenUser }) -} + }); + res.status(StatusCodes.OK).json({ user: tokenUser }); +}; const logout = async (req, res) => { await Token.findOneAndDelete({ user: req.user.userId, userAgent: req.headers['user-agent'], - }) + }); res.cookie('accessToken', 'logout', { httpOnly: true, expires: new Date(Date.now()), - }) + }); res.cookie('refreshToken', 'logout', { httpOnly: true, expires: new Date(Date.now()), - }) - res.status(StatusCodes.OK).json({ msg: 'user logged out!' }) -} + }); + res.status(StatusCodes.OK).json({ msg: 'user logged out!' }); +}; const forgotPassword = async (req, res) => { - const { email } = req.body + const { email } = req.body; if (!email) { - throw new CustomError.BadRequestError('Please provide valid email') + throw new CustomError.BadRequestError('Please provide valid email'); } - const user = await User.findOne({ email }) + const user = await User.findOne({ email }); if (user) { - const passwordToken = crypto.randomBytes(70).toString('hex') + const passwordToken = crypto.randomBytes(70).toString('hex'); // send email - const origin = 'http://localhost:3000' + const origin = 'http://localhost:3000'; await sendResetPasswordEmail({ name: user.name, email: user.email, token: passwordToken, origin, - }) + }); - const tenMinutes = 1000 * 60 * 10 - const passwordTokenExpirationDate = new Date(Date.now() + tenMinutes) + const tenMinutes = 1000 * 60 * 10; + const passwordTokenExpirationDate = new Date(Date.now() + tenMinutes); - user.passwordToken = createHash(passwordToken) - user.passwordTokenExpirationDate = passwordTokenExpirationDate - await user.save() + user.passwordToken = createHash(passwordToken); + user.passwordTokenExpirationDate = passwordTokenExpirationDate; + await user.save(); } res .status(StatusCodes.OK) - .json({ msg: 'Please check your email for reset password link' }) -} + .json({ msg: 'Please check your email for reset password link' }); +}; const resetPassword = async (req, res) => { - const { token, email, password } = req.body + const { token, email, password } = req.body; if (!token || !email || !password) { - throw new CustomError.BadRequestError('Please provide all values') + throw new CustomError.BadRequestError('Please provide all values'); } - const user = await User.findOne({ email }) + const user = await User.findOne({ email }); if (user) { - const currentDate = new Date() + const currentDate = new Date(); if ( user.passwordToken === createHash(token) && user.passwordTokenExpirationDate > currentDate ) { - user.password = password - user.passwordToken = null - user.passwordTokenExpirationDate = null - await user.save() + user.password = password; + user.passwordToken = null; + user.passwordTokenExpirationDate = null; + await user.save(); } } - res.send('reset password') -} + res.send('reset password'); +}; module.exports = { register, @@ -223,4 +223,4 @@ module.exports = { verifyEmail, forgotPassword, resetPassword, -} +}; diff --git a/11-auth-workflow/final/server/middleware/authentication.js b/11-auth-workflow/final/server/middleware/authentication.js index bcae7e7a6d..5c0e78f703 100644 --- a/11-auth-workflow/final/server/middleware/authentication.js +++ b/11-auth-workflow/final/server/middleware/authentication.js @@ -1,25 +1,25 @@ -const CustomError = require('../errors') -const { isTokenValid } = require('../utils') -const Token = require('../models/Token') -const { attachCookiesToResponse } = require('../utils') +const CustomError = require('../errors'); +const { isTokenValid } = require('../utils'); +const Token = require('../models/Token'); +const { attachCookiesToResponse } = require('../utils'); const authenticateUser = async (req, res, next) => { - const { refreshToken, accessToken } = req.signedCookies + const { refreshToken, accessToken } = req.signedCookies; try { if (accessToken) { - const payload = await isTokenValid(accessToken) - req.user = payload.user - return next() + const payload = await isTokenValid(accessToken); + req.user = payload.user; + return next(); } - const payload = await isTokenValid(refreshToken) + const payload = await isTokenValid(refreshToken); const existingToken = await Token.findOne({ user: payload.user.userId, refreshToken: payload.refreshToken, - }) + }); if (!existingToken || !existingToken?.isValid) { - throw new CustomError.UnauthenticatedError('Authentication Invalid') + throw new CustomError.UnauthenticatedError('Authentication Invalid'); } await attachCookiesToResponse({ @@ -27,27 +27,27 @@ const authenticateUser = async (req, res, next) => { user: payload.user, refreshToken: existingToken.refreshToken, expiresIn: existingToken.expiredIn, - }) + }); - req.user = payload.user - next() + req.user = payload.user; + next(); } catch (error) { - throw new CustomError.UnauthenticatedError('Authentication Invalid') + throw new CustomError.UnauthenticatedError('Authentication Invalid'); } -} +}; const authorizePermissions = (...roles) => { return (req, res, next) => { if (!roles.includes(req.user.role)) { throw new CustomError.UnauthorizedError( 'Unauthorized to access this route' - ) + ); } - next() - } -} + next(); + }; +}; module.exports = { authenticateUser, authorizePermissions, -} +}; diff --git a/11-auth-workflow/final/server/utils/jwt.js b/11-auth-workflow/final/server/utils/jwt.js index 142a556bce..79f84cff52 100644 --- a/11-auth-workflow/final/server/utils/jwt.js +++ b/11-auth-workflow/final/server/utils/jwt.js @@ -1,15 +1,15 @@ -const jwt = require('jsonwebtoken') -const { promisify } = require('util') -const jwtVerify = promisify(jwt.verify) -const jwtSign = promisify(jwt.sign) +const jwt = require('jsonwebtoken'); +const { promisify } = require('util'); +const jwtVerify = promisify(jwt.verify); +const jwtSign = promisify(jwt.sign); const createJWT = async ({ payload }) => await jwtSign(payload, process.env.JWT_SECRET, { expiresIn: payload.expiresIn, - }) + }); const isTokenValid = async token => - await jwtVerify(token, process.env.JWT_SECRET) + await jwtVerify(token, process.env.JWT_SECRET); const attachCookiesToResponse = async ({ res, @@ -19,7 +19,7 @@ const attachCookiesToResponse = async ({ }) => { const accessTokenJWT = await createJWT({ payload: { user, expiresIn: 60 * 15 }, - }) //set accessJWT 15 mins valid + }); //set accessJWT 15 mins valid const refreshTokenJWT = await createJWT({ payload: { user, @@ -27,25 +27,25 @@ const attachCookiesToResponse = async ({ expiresIn: new Date(expiresIn).getTime() - Date.now() - 5 * 60, // refreshJWT will be expired 5min ahead the expiredTime saved in DB }, - }) + }); - const oneDay = 1000 * 60 * 60 * 24 - const longerExp = 1000 * 60 * 60 * 24 * 30 + const oneDay = 1000 * 60 * 60 * 24; + const longerExp = 1000 * 60 * 60 * 24 * 30; res.cookie('accessToken', accessTokenJWT, { httpOnly: true, secure: process.env.NODE_ENV === 'production', signed: true, expires: new Date(Date.now() + 1000 * 60 * 15), //15min - }) + }); res.cookie('refreshToken', refreshTokenJWT, { httpOnly: true, secure: process.env.NODE_ENV === 'production', signed: true, expires: new Date(expiresIn - 1000 * 60 * 5), // if use Date.now() as reference, the refreshToken will never be expired, which is a bug, so add an expiredIn attribute in Token Schema, and use that as reference, whenever the refreshToken refresh, it's live can also be refreshed correct - }) -} + }); +}; // const attachSingleCookieToResponse = ({ res, user }) => { // const token = createJWT({ payload: user }); @@ -63,4 +63,4 @@ module.exports = { createJWT, isTokenValid, attachCookiesToResponse, -} +};