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 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 ?? '' 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 81f0b4ad..d42649ca 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,16 +1,16 @@ -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' +import { setAccessToken, signAndSetCookie } from '../utils' export const googleRedirect = async ( req: Request, @@ -135,7 +135,8 @@ 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) { if (err instanceof Error) { @@ -163,18 +164,27 @@ export const requireAuth = ( return } - const token = req.cookies.jwt + const token = req.cookies.accessToken + const refreshToken = req.cookies.refreshToken - if (!token) { - return res.status(401).json({ error: 'User is not authenticated' }) + if (!token && !refreshToken) { + return res.status(403).json({ error: 'Forbidden. No token provided.' }) } try { jwt.verify(token, JWT_SECRET) } catch (err) { - return res - .status(401) - .json({ error: 'Invalid token, please log in again' }) + try { + const decoded = jwt.verify(refreshToken, REFRESH_JWT_SECRET) as { + userId: string + } + + setAccessToken(res, decoded.userId) + } catch (error) { + return res + .status(401) + .json({ error: 'Invalid token, please log in again' }) + } } if (!user) { @@ -230,3 +240,22 @@ export const passwordReset = async ( }) } } + +export const refresh = async (req: Request, res: Response): Promise => { + const refreshToken = req.cookies.refreshToken + + if (!refreshToken) { + res.status(401).json({ error: 'Access Denied. No token provided.' }) + return + } + + try { + const decoded = jwt.verify(refreshToken, REFRESH_JWT_SECRET) as { + userId: string + } + + setAccessToken(res, decoded.userId) + } catch (error) { + 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) diff --git a/src/utils.ts b/src/utils.ts index 51d1a0ff..95678374 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,21 +1,48 @@ -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' import { type ZodError } from 'zod' +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 token = jwt.sign({ userId: uuid }, JWT_SECRET ?? '') + const accessToken = generateAccessToken(uuid) + const refreshToken = generateRefreshToken(uuid) + + res.cookie('accessToken', accessToken, { + 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 setAccessToken = (res: Response, uuid: string): void => { + const accessToken = generateAccessToken(uuid) - 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