From 7c0bb94b8cbc2a0f17e1ced182f688dfc9387300 Mon Sep 17 00:00:00 2001 From: Dillepa Mabulage Date: Tue, 3 Sep 2024 16:21:04 +0530 Subject: [PATCH 1/9] add refresh jwt secret --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index 8eac7fbd..9d6078d5 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,4 @@ SMTP_PASSWORD=your_smtp_password LINKEDIN_CLIENT_ID=your_linkedin_client_id LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret LINKEDIN_REDIRECT_URL=http://localhost:${SERVER_PORT}/api/auth/linkedin/callback +REFRESH_JWT_SECRET=your_refresh_jwt_secret_key From 75096244731e7c3d3892ae8e5f5ac0680f580153 Mon Sep 17 00:00:00 2001 From: Dillepa Mabulage Date: Tue, 3 Sep 2024 16:21:39 +0530 Subject: [PATCH 2/9] implement middleware to attach the new tokens if the existing ones are expired --- src/controllers/auth.controller.ts | 42 +++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 81f0b4ad..5ce2238f 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,14 +1,14 @@ -import type { Request, Response, NextFunction } from 'express' +import type { NextFunction, Request, Response } from 'express' +import jwt from 'jsonwebtoken' +import passport from 'passport' +import { JWT_SECRET, REFRESH_JWT_SECRET } from '../configs/envConfig' +import type Profile from '../entities/profile.entity' import { - registerUser, + generateResetToken, loginUser, - resetPassword, - generateResetToken + registerUser, + resetPassword } from '../services/auth.service' -import passport from 'passport' -import type Profile from '../entities/profile.entity' -import jwt from 'jsonwebtoken' -import { JWT_SECRET } from '../configs/envConfig' import type { ApiResponse } from '../types' import { signAndSetCookie } from '../utils' @@ -136,6 +136,7 @@ export const logout = async ( ): Promise> => { try { res.clearCookie('jwt', { httpOnly: true }) + res.clearCookie('refreshToken', { httpOnly: true }) return res.status(200).json({ message: 'Logged out successfully' }) } catch (err) { if (err instanceof Error) { @@ -164,17 +165,32 @@ export const requireAuth = ( } const token = req.cookies.jwt + const refreshToken = req.cookies.refreshToken - if (!token) { - return res.status(401).json({ error: 'User is not authenticated' }) + if (!token && !refreshToken) { + return res + .status(401) + .json({ error: 'Access Denied. No token provided.' }) } try { jwt.verify(token, JWT_SECRET) } catch (err) { - return res - .status(401) - .json({ error: 'Invalid token, please log in again' }) + if (!refreshToken) { + return res.status(401).send('Access Denied. Please Login again.') + } + + try { + const decoded = jwt.verify(refreshToken, REFRESH_JWT_SECRET) as { + userId: string + } + + signAndSetCookie(res, decoded.userId) + } catch (error) { + return res + .status(401) + .json({ error: 'Invalid token, please log in again' }) + } } if (!user) { From 02a6b2a6fdda6cf24e67259bdd2354bfbf24b96e Mon Sep 17 00:00:00 2001 From: Dillepa Mabulage Date: Tue, 3 Sep 2024 16:21:55 +0530 Subject: [PATCH 3/9] update the cookie set method --- src/utils.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index a7f30f66..a1179ec1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,24 +1,33 @@ -import { JWT_SECRET, CLIENT_URL } from './configs/envConfig' -import jwt from 'jsonwebtoken' +import { randomUUID } from 'crypto' +import ejs from 'ejs' import type { Response } from 'express' -import type Mentor from './entities/mentor.entity' -import path from 'path' +import jwt from 'jsonwebtoken' import multer from 'multer' -import ejs from 'ejs' -import { MenteeApplicationStatus, MentorApplicationStatus } from './enums' -import { generateCertificate } from './services/admin/generateCertificate' -import { randomUUID } from 'crypto' +import path from 'path' import { certificatesDir } from './app' +import { CLIENT_URL, JWT_SECRET, REFRESH_JWT_SECRET } from './configs/envConfig' import type Mentee from './entities/mentee.entity' +import type Mentor from './entities/mentor.entity' +import { MenteeApplicationStatus, MentorApplicationStatus } from './enums' +import { generateCertificate } from './services/admin/generateCertificate' export const signAndSetCookie = (res: Response, uuid: string): void => { const token = jwt.sign({ userId: uuid }, JWT_SECRET ?? '') + const refreshToken = jwt.sign({ userId: uuid }, REFRESH_JWT_SECRET ?? '', { + expiresIn: '10d' + }) res.cookie('jwt', token, { httpOnly: true, maxAge: 5 * 24 * 60 * 60 * 1000, secure: false // TODO: Set to true when using HTTPS }) + + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + maxAge: 10 * 24 * 60 * 60 * 1000, + secure: false // TODO: Set to true when using HTTPS + }) } export const getMentorPublicData = (mentor: Mentor): Mentor => { From 1cccb5b2186c79347dbea8d54ea042d407fed32b Mon Sep 17 00:00:00 2001 From: Dillepa Mabulage Date: Tue, 3 Sep 2024 16:22:19 +0530 Subject: [PATCH 4/9] update env config --- src/configs/envConfig.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/configs/envConfig.ts b/src/configs/envConfig.ts index 451eebde..fd1cf954 100644 --- a/src/configs/envConfig.ts +++ b/src/configs/envConfig.ts @@ -19,3 +19,4 @@ export const SMTP_PASS = process.env.SMTP_PASS ?? '' export const LINKEDIN_CLIENT_ID = process.env.LINKEDIN_CLIENT_ID ?? '' export const LINKEDIN_CLIENT_SECRET = process.env.LINKEDIN_CLIENT_SECRET ?? '' export const LINKEDIN_REDIRECT_URL = process.env.LINKEDIN_REDIRECT_URL ?? '' +export const REFRESH_JWT_SECRET = process.env.REFRESH_JWT_SECRET ?? '' From ce1502d4701a7b0d141a5720bae720003133c7e8 Mon Sep 17 00:00:00 2001 From: Dillepa Mabulage Date: Tue, 3 Sep 2024 21:15:41 +0530 Subject: [PATCH 5/9] update variable name --- src/configs/google-passport.ts | 2 +- src/configs/linkedin-passport.ts | 2 +- src/controllers/auth.controller.ts | 4 ++-- src/utils.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/configs/google-passport.ts b/src/configs/google-passport.ts index 8ed3c011..a1aee5f3 100644 --- a/src/configs/google-passport.ts +++ b/src/configs/google-passport.ts @@ -67,7 +67,7 @@ passport.deserializeUser(async (primary_email: string, done) => { const cookieExtractor = (req: Request): string => { let token = null if (req?.cookies) { - token = req.cookies.jwt + token = req.cookies.accessToken } return token } diff --git a/src/configs/linkedin-passport.ts b/src/configs/linkedin-passport.ts index 416276d4..b4dd4c85 100644 --- a/src/configs/linkedin-passport.ts +++ b/src/configs/linkedin-passport.ts @@ -68,7 +68,7 @@ passport.deserializeUser(async (primary_email: string, done) => { const cookieExtractor = (req: Request): string => { let token = null if (req?.cookies) { - token = req.cookies.jwt + token = req.cookies.accessToken } return token } diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 5ce2238f..96eb91d4 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -135,7 +135,7 @@ export const logout = async ( res: Response ): Promise> => { try { - res.clearCookie('jwt', { httpOnly: true }) + res.clearCookie('accessToken', { httpOnly: true }) res.clearCookie('refreshToken', { httpOnly: true }) return res.status(200).json({ message: 'Logged out successfully' }) } catch (err) { @@ -164,7 +164,7 @@ export const requireAuth = ( return } - const token = req.cookies.jwt + const token = req.cookies.accessToken const refreshToken = req.cookies.refreshToken if (!token && !refreshToken) { diff --git a/src/utils.ts b/src/utils.ts index a1179ec1..fa095332 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,12 +12,12 @@ import { MenteeApplicationStatus, MentorApplicationStatus } from './enums' import { generateCertificate } from './services/admin/generateCertificate' export const signAndSetCookie = (res: Response, uuid: string): void => { - const token = jwt.sign({ userId: uuid }, JWT_SECRET ?? '') + const accessToken = jwt.sign({ userId: uuid }, JWT_SECRET ?? '') const refreshToken = jwt.sign({ userId: uuid }, REFRESH_JWT_SECRET ?? '', { expiresIn: '10d' }) - res.cookie('jwt', token, { + res.cookie('accessToken', accessToken, { httpOnly: true, maxAge: 5 * 24 * 60 * 60 * 1000, secure: false // TODO: Set to true when using HTTPS From c30396ad6654c72aa95fdcb09f329d4a424db501 Mon Sep 17 00:00:00 2001 From: Dillepa Mabulage Date: Wed, 4 Sep 2024 14:27:24 +0530 Subject: [PATCH 6/9] add refresh token endpoint --- src/controllers/auth.controller.ts | 18 ++++++++++++++++++ src/routes/auth/auth.route.ts | 2 ++ 2 files changed, 20 insertions(+) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 96eb91d4..256586e9 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -246,3 +246,21 @@ export const passwordReset = async ( }) } } + +export const refresh = async (req: Request, res: Response) => { + const refreshToken = req.cookies.refreshToken + + if (!refreshToken) { + return res.status(401).json({ error: 'Access Denied. No token provided.' }) + } + + try { + const decoded = jwt.verify(refreshToken, REFRESH_JWT_SECRET) as { + userId: string + } + + signAndSetCookie(res, decoded.userId) + } catch (error) { + return res.status(401).json({ error: 'Invalid token, please log in again' }) + } +} diff --git a/src/routes/auth/auth.route.ts b/src/routes/auth/auth.route.ts index a50bb233..7e3d5ffd 100644 --- a/src/routes/auth/auth.route.ts +++ b/src/routes/auth/auth.route.ts @@ -7,6 +7,7 @@ import { logout, passwordReset, passwordResetRequest, + refresh, register } from '../../controllers/auth.controller' import { requestBodyValidator } from '../../middlewares/requestValidator' @@ -15,6 +16,7 @@ import { loginSchema, registerSchema } from '../../schemas/auth-routes.schems' const authRouter = express.Router() authRouter.post('/register', requestBodyValidator(registerSchema), register) +authRouter.post('/refresh', refresh) authRouter.post('/login', requestBodyValidator(loginSchema), login) authRouter.get('/logout', logout) From c68c7c30c36fad4a26f59cde5df65c28afe51107 Mon Sep 17 00:00:00 2001 From: Dillepa Mabulage Date: Wed, 4 Sep 2024 14:30:43 +0530 Subject: [PATCH 7/9] update return type --- src/controllers/auth.controller.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 256586e9..99838ab2 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -247,11 +247,12 @@ export const passwordReset = async ( } } -export const refresh = async (req: Request, res: Response) => { +export const refresh = async (req: Request, res: Response): Promise => { const refreshToken = req.cookies.refreshToken if (!refreshToken) { - return res.status(401).json({ error: 'Access Denied. No token provided.' }) + res.status(401).json({ error: 'Access Denied. No token provided.' }) + return } try { @@ -261,6 +262,6 @@ export const refresh = async (req: Request, res: Response) => { signAndSetCookie(res, decoded.userId) } catch (error) { - return res.status(401).json({ error: 'Invalid token, please log in again' }) + res.status(401).json({ error: 'Invalid token, please log in again' }) } } From 4d9f2fe71c3aa5bed1ac4889606a9f846419c498 Mon Sep 17 00:00:00 2001 From: Dillepa Mabulage Date: Mon, 16 Sep 2024 18:44:10 +0530 Subject: [PATCH 8/9] add token generate methods and update /refresh endpoint and middleware --- src/controllers/auth.controller.ts | 16 ++++++---------- src/utils.ts | 24 +++++++++++++++++++++--- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 99838ab2..5ee109df 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -10,7 +10,7 @@ import { resetPassword } from '../services/auth.service' import type { ApiResponse } from '../types' -import { signAndSetCookie } from '../utils' +import { setAccessToken, signAndSetCookie } from '../utils' export const googleRedirect = async ( req: Request, @@ -169,23 +169,19 @@ export const requireAuth = ( if (!token && !refreshToken) { return res - .status(401) - .json({ error: 'Access Denied. No token provided.' }) + .status(403) + .json({ error: 'Forbidden. No token provided.' }) } try { jwt.verify(token, JWT_SECRET) - } catch (err) { - if (!refreshToken) { - return res.status(401).send('Access Denied. Please Login again.') - } - + } catch (err) { try { const decoded = jwt.verify(refreshToken, REFRESH_JWT_SECRET) as { userId: string } - signAndSetCookie(res, decoded.userId) + setAccessToken(res, decoded.userId) } catch (error) { return res .status(401) @@ -260,7 +256,7 @@ export const refresh = async (req: Request, res: Response): Promise => { userId: string } - signAndSetCookie(res, decoded.userId) + setAccessToken(res, decoded.userId) } catch (error) { res.status(401).json({ error: 'Invalid token, please log in again' }) } diff --git a/src/utils.ts b/src/utils.ts index fa095332..51314d6a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,11 +11,19 @@ import type Mentor from './entities/mentor.entity' import { MenteeApplicationStatus, MentorApplicationStatus } from './enums' import { generateCertificate } from './services/admin/generateCertificate' -export const signAndSetCookie = (res: Response, uuid: string): void => { - const accessToken = jwt.sign({ userId: uuid }, JWT_SECRET ?? '') - const refreshToken = jwt.sign({ userId: uuid }, REFRESH_JWT_SECRET ?? '', { +const generateAccessToken = (uuid: string): string => { + return jwt.sign({ userId: uuid }, JWT_SECRET ?? '') +} + +const generateRefreshToken = (uuid: string): string => { + return jwt.sign({ userId: uuid }, REFRESH_JWT_SECRET ?? '', { expiresIn: '10d' }) +} + +export const signAndSetCookie = (res: Response, uuid: string): void => { + const accessToken = generateAccessToken(uuid) + const refreshToken = generateRefreshToken(uuid) res.cookie('accessToken', accessToken, { httpOnly: true, @@ -30,6 +38,16 @@ export const signAndSetCookie = (res: Response, uuid: string): void => { }) } +export const setAccessToken = (res: Response, uuid: string): void => { + const accessToken = generateAccessToken(uuid) + + res.cookie('accessToken', accessToken, { + httpOnly: true, + maxAge: 5 * 24 * 60 * 60 * 1000, + secure: false // TODO: Set to true when using HTTPS + }) +} + export const getMentorPublicData = (mentor: Mentor): Mentor => { const { application, profile } = mentor From a98c398ea8f86fd59dab907e8523533d1232cb92 Mon Sep 17 00:00:00 2001 From: Dillepa Mabulage Date: Mon, 16 Sep 2024 18:50:38 +0530 Subject: [PATCH 9/9] fix formatting --- src/controllers/auth.controller.ts | 6 ++---- src/utils.ts | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 5ee109df..d42649ca 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -168,14 +168,12 @@ export const requireAuth = ( const refreshToken = req.cookies.refreshToken if (!token && !refreshToken) { - return res - .status(403) - .json({ error: 'Forbidden. No token provided.' }) + return res.status(403).json({ error: 'Forbidden. No token provided.' }) } try { jwt.verify(token, JWT_SECRET) - } catch (err) { + } catch (err) { try { const decoded = jwt.verify(refreshToken, REFRESH_JWT_SECRET) as { userId: string diff --git a/src/utils.ts b/src/utils.ts index 51314d6a..d2d338b3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -39,14 +39,14 @@ export const signAndSetCookie = (res: Response, uuid: string): void => { } export const setAccessToken = (res: Response, uuid: string): void => { - const accessToken = generateAccessToken(uuid) + const accessToken = generateAccessToken(uuid) res.cookie('accessToken', accessToken, { httpOnly: true, maxAge: 5 * 24 * 60 * 60 * 1000, secure: false // TODO: Set to true when using HTTPS }) -} +} export const getMentorPublicData = (mentor: Mentor): Mentor => { const { application, profile } = mentor