diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a41adc7..fb44521 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: swagger-ui-express: specifier: ^5.0.1 version: 5.0.1(express@4.19.2) + uuid: + specifier: ^10.0.0 + version: 10.0.0 devDependencies: '@eslint/js': specifier: ^9.6.0 @@ -126,6 +129,9 @@ importers: '@types/swagger-ui-express': specifier: ^4.1.6 version: 4.1.6 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 drizzle-kit: specifier: ^0.22.8 version: 0.22.8 @@ -1842,6 +1848,9 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@typescript-eslint/eslint-plugin@7.13.1': resolution: {integrity: sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4549,6 +4558,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -6399,6 +6412,8 @@ snapshots: '@types/trusted-types@2.0.7': {} + '@types/uuid@10.0.0': {} + '@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@7.13.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': dependencies: '@eslint-community/regexpp': 4.10.1 @@ -9258,6 +9273,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@10.0.0: {} + v8-compile-cache-lib@3.0.1: optional: true diff --git a/server/package.json b/server/package.json index 3cc283d..2364f37 100644 --- a/server/package.json +++ b/server/package.json @@ -37,7 +37,8 @@ "pg": "^8.12.0", "socket.io": "^4.7.5", "swagger-autogen": "^2.23.7", - "swagger-ui-express": "^5.0.1" + "swagger-ui-express": "^5.0.1", + "uuid": "^10.0.0" }, "devDependencies": { "@eslint/js": "^9.6.0", @@ -51,6 +52,7 @@ "@types/node": "^20.14.5", "@types/pg": "^8.11.6", "@types/swagger-ui-express": "^4.1.6", + "@types/uuid": "^10.0.0", "drizzle-kit": "^0.22.8", "eslint": "9.x", "globals": "^15.7.0", diff --git a/server/src/common/controllers.ts b/server/src/common/controllers.ts index 0ef1599..3db1090 100644 --- a/server/src/common/controllers.ts +++ b/server/src/common/controllers.ts @@ -35,9 +35,15 @@ export const recreateAccessToken: RequestHandler = async (req, res) => { } try { - const refreshPayload = verifyRefreshToken(refreshToken) as UserPayload + const refreshPayload = verifyRefreshToken(refreshToken) as UserPayload & { + tokenId: string + } - const isValid = await isRefreshTokenValid(refreshPayload.id, refreshToken) + const isValid = await isRefreshTokenValid( + refreshPayload.id, + refreshToken.tokenId, + refreshToken, + ) if (!isValid) { return notAuthorized(res) @@ -61,7 +67,12 @@ export const recreateAccessToken: RequestHandler = async (req, res) => { export const logout: RequestHandler = async (req, res, next) => { try { - await invalidateRefreshToken(req.user!.id, req.cookies[config.jwtKey]) + const refreshPayload = verifyRefreshToken( + req.cookies[config.jwtKey], + ) as UserPayload & { + tokenId: string + } + await invalidateRefreshToken(req.user!.id, refreshPayload.tokenId) clearRefreshTokenCookie(res) } catch (error) { next(error) diff --git a/server/src/redis/handlers.ts b/server/src/redis/handlers.ts index b9d7975..ab13f45 100644 --- a/server/src/redis/handlers.ts +++ b/server/src/redis/handlers.ts @@ -17,17 +17,28 @@ export const redisKeys = { // USER REFRESH TOKENS -export const addRefreshToken = (userId: number, token: string) => { - return redisClient.sadd(redisKeys.USER_TOKEN(userId), token) +export const addRefreshToken = ( + userId: number, + tokenId: string, + token: string, +) => { + return redisClient.hset(redisKeys.USER_TOKEN(userId), tokenId, token) } -export const invalidateRefreshToken = (userId: number, token: string) => { - return redisClient.srem(redisKeys.USER_TOKEN(userId), token) +export const invalidateRefreshToken = (userId: number, tokenId: string) => { + return redisClient.hdel(redisKeys.USER_TOKEN(userId), tokenId) } -export const isRefreshTokenValid = async (userId: number, token: string) => { - const value = await redisClient.sismember(redisKeys.USER_TOKEN(userId), token) - return value == 1 +export const isRefreshTokenValid = async ( + userId: number, + tokenId: string, + token: string, +) => { + const redisToken = await redisClient.hget( + redisKeys.USER_TOKEN(userId), + tokenId, + ) + return redisToken === token } // MEMBER ROLES diff --git a/server/src/utils/jwt.ts b/server/src/utils/jwt.ts index 2cd6f87..6f1c464 100644 --- a/server/src/utils/jwt.ts +++ b/server/src/utils/jwt.ts @@ -1,6 +1,7 @@ import { addRefreshToken } from '@/redis/handlers' import { CookieOptions, Response } from 'express' import jwt from 'jsonwebtoken' +import { v4 as uuidv4 } from 'uuid' import { config } from '../config' const refreshTokenCookieOptions: CookieOptions = { @@ -26,11 +27,16 @@ export const verifyRefreshToken = (token: string) => export const signTokens = async (res: Response, payload: UserPayload) => { const accessToken = signAccessToken(payload) - const refreshToken = jwt.sign(payload, config.refreshTokenSecret, { - expiresIn: config.refreshTokenExpiry, - }) - - await addRefreshToken(payload.id, refreshToken) + const tokenId = uuidv4() + const refreshToken = jwt.sign( + { ...payload, tokenId }, + config.refreshTokenSecret, + { + expiresIn: config.refreshTokenExpiry, + }, + ) + + await addRefreshToken(payload.id, tokenId, refreshToken) res.cookie(config.jwtKey, refreshToken, refreshTokenCookieOptions)