diff --git a/src/api/v1/auth/auth.service.ts b/src/api/v1/auth/auth.service.ts index 7e05cc7..b24c903 100644 --- a/src/api/v1/auth/auth.service.ts +++ b/src/api/v1/auth/auth.service.ts @@ -9,7 +9,7 @@ import { validateEmail, validateResetPasswordSchema, } from "@/api/v1/user/user.validation"; -import { ErrorTypeEnum } from "@/constants"; +import { ErrorTypeEnum, Permission } from "@/constants"; import { notificationService } from "@/services"; import { GoogleUser } from "@/types/passport-google"; import { comparePassword, generateAccessToken, generateRefreshToken, validateObjectId } from "@/utils"; @@ -117,10 +117,14 @@ export class AuthService { static async generateAccessAndRefreshToken(userId: string) { validateObjectId(userId); + const userRolePermissions = await UserDAL.getUserRolesAndPermissionsByUserId(userId); // Generate both tokens concurrently const [accessToken, refreshToken] = await Promise.all([ - generateAccessToken({ userId }), + generateAccessToken({ + userId, + permissions: userRolePermissions.allPermissions.map(({ name }) => name) as Permission[], + }), generateRefreshToken({ id: userId }), ]); diff --git a/src/api/v1/auth/auth.validation.ts b/src/api/v1/auth/auth.validation.ts index 243eea7..00e94f4 100644 --- a/src/api/v1/auth/auth.validation.ts +++ b/src/api/v1/auth/auth.validation.ts @@ -24,6 +24,11 @@ export const authenticateSchema = AuthSchema.pick({ export const jwtAccessTokenSchema = z.object({ userId: z.string(), + permissions: z.array(z.string()), + tokenVersion: z.number(), + issuedAt: z.number(), + issuer: z.string(), + audience: z.string(), }); export const jwtRefreshTokenSchema = z.object({ diff --git a/src/api/v1/permission/__test__/permission.test.ts b/src/api/v1/permission/__test__/permission.test.ts index 33f9645..8c4e4f6 100644 --- a/src/api/v1/permission/__test__/permission.test.ts +++ b/src/api/v1/permission/__test__/permission.test.ts @@ -6,9 +6,9 @@ import { getPermissions, } from "@/utils/test"; -import { PermissionCategory, createPermission } from "../permission.validation"; +import { CreatePermission, PermissionCategory } from "../permission.validation"; -const VALID_PERMISSION: createPermission = { +const VALID_PERMISSION: CreatePermission = { name: "create-permission", description: "this is crete permission creating first time", category: PermissionCategory.SYSTEM, diff --git a/src/api/v1/permission/permission.dal.ts b/src/api/v1/permission/permission.dal.ts index 6460fc2..fd30616 100644 --- a/src/api/v1/permission/permission.dal.ts +++ b/src/api/v1/permission/permission.dal.ts @@ -1,15 +1,15 @@ import { PermissionModel } from "./permission.model"; -import { Permission, createPermission } from "./permission.validation"; +import { CreatePermission, Permission } from "./permission.validation"; export class PermissionDAL { - static async createPermission(permission: createPermission): Promise { + static async createPermission(permission: CreatePermission): Promise { return await PermissionModel.create({ ...permission, updatedBy: permission.createdBy, }); } - static async createPermissions(permission: createPermission[]): Promise { + static async createPermissions(permission: CreatePermission[]): Promise { return await PermissionModel.insertMany( permission.map((permission) => ({ ...permission, diff --git a/src/api/v1/permission/permission.dto.ts b/src/api/v1/permission/permission.dto.ts index 9bf7ed7..67886ce 100644 --- a/src/api/v1/permission/permission.dto.ts +++ b/src/api/v1/permission/permission.dto.ts @@ -1,7 +1,7 @@ -import { Permission, getPermission } from "./permission.validation"; +import { GetPermission, Permission } from "./permission.validation"; export const PermissionDto = (permission: Permission) => ({ - getPermission: (): getPermission => ({ + getPermission: (): GetPermission => ({ id: permission.id, name: permission.name, description: permission.description, diff --git a/src/api/v1/permission/permission.service.ts b/src/api/v1/permission/permission.service.ts index b199087..d0fba2c 100644 --- a/src/api/v1/permission/permission.service.ts +++ b/src/api/v1/permission/permission.service.ts @@ -2,10 +2,10 @@ import { ErrorTypeEnum } from "@/constants"; import { PermissionDAL } from "./permission.dal"; import { PermissionDto } from "./permission.dto"; -import { createPermission, createPermissionSchema, getPermission } from "./permission.validation"; +import { CreatePermission, GetPermission, createPermissionSchema } from "./permission.validation"; export class PermissionService { - static async createPermission(createPermission: createPermission): Promise { + static async createPermission(createPermission: CreatePermission): Promise { const validPermissionData = createPermissionSchema.parse(createPermission); const isPermissionExist = await PermissionDAL.getPermissionByName(validPermissionData.name); @@ -17,7 +17,7 @@ export class PermissionService { return PermissionDto(createdPermission).getPermission(); } - static async getPermissions(): Promise { + static async getPermissions(): Promise { const permissions = await PermissionDAL.getPermissions(); return permissions.map((permission) => PermissionDto(permission).getPermission()); } diff --git a/src/api/v1/permission/permission.validation.ts b/src/api/v1/permission/permission.validation.ts index 96626b5..5c4ea1a 100644 --- a/src/api/v1/permission/permission.validation.ts +++ b/src/api/v1/permission/permission.validation.ts @@ -45,5 +45,5 @@ export const getPermissionSchema = permissionSchema.omit({ }); export type Permission = z.infer; -export type createPermission = z.infer; -export type getPermission = z.infer; +export type CreatePermission = z.infer; +export type GetPermission = z.infer; diff --git a/src/api/v1/rolePermission/__test__/role-permission.test.ts b/src/api/v1/rolePermission/__test__/role-permission.test.ts index cadd8fa..2687887 100644 --- a/src/api/v1/rolePermission/__test__/role-permission.test.ts +++ b/src/api/v1/rolePermission/__test__/role-permission.test.ts @@ -1,4 +1,4 @@ -import { PermissionCategory, createPermission } from "@/api/v1/permission/permission.validation"; +import { CreatePermission, PermissionCategory } from "@/api/v1/permission/permission.validation"; import { createRole } from "@/api/v1/role/role.validation"; import { defaultUsers } from "@/constants"; import { @@ -20,7 +20,7 @@ const VALID_ROLE: createRole = { isSystemRole: false, }; -const VALID_PERMISSION: createPermission = { +const VALID_PERMISSION: CreatePermission = { name: "new-permissions", description: "this is crete permission creating first time", createdBy: "65f6dac9156e93e7b6f1b88d", diff --git a/src/api/v1/token/token.service.ts b/src/api/v1/token/token.service.ts index 99ee7b8..e3109c7 100644 --- a/src/api/v1/token/token.service.ts +++ b/src/api/v1/token/token.service.ts @@ -1,7 +1,7 @@ import { isPast } from "date-fns"; import { ErrorTypeEnum, FIVE_MINUTES_IN_MS } from "@/constants"; -import { generateTokenForAction, verifyJWTToken } from "@/utils"; +import { generateTokenForAction, logger, verifyJWTToken } from "@/utils"; import { TokenDAL } from "./token.dal"; import { CreateToken, Token, TokenAction } from "./token.validation"; @@ -29,26 +29,27 @@ export class TokenService { } async verifyActionToken(token: string, action: TokenAction): Promise { - const verifiedToken = await verifyJWTToken(token, "action"); - - // Check if the token is valid for the intended action - // e.g. if the token is for changing subscription, then the action must be changeSubscription - if (verifiedToken.action !== action) { - throw new Error(ErrorTypeEnum.enum.INVALID_TOKEN_TYPE); - } - - const storedToken = await this.tokenDAL.getToken(token); - - if (!storedToken) { - throw new Error(ErrorTypeEnum.enum.INVALID_ACCESS); + try { + const verifiedToken = await verifyJWTToken(token, "action"); + + if (verifiedToken.action !== action) { + throw new Error(ErrorTypeEnum.enum.INVALID_TOKEN_TYPE); + } + + const storedToken = await this.tokenDAL.getToken(token); + if (!storedToken) { + throw new Error(ErrorTypeEnum.enum.INVALID_ACCESS); + } + + if (storedToken.isUsed || isPast(storedToken.expiryTime)) { + throw new Error(ErrorTypeEnum.enum.TOKEN_EXPIRED); + } + + await this.tokenDAL.updateToken(storedToken.id, { isUsed: true }); + return verifiedToken; + } catch (error) { + logger.error("Action token verification error:", error); + throw error; } - - if (storedToken.isUsed || isPast(storedToken.expiryTime)) { - throw new Error(ErrorTypeEnum.enum.TOKEN_EXPIRED); - } - - await this.tokenDAL.updateToken(storedToken.id, { isUsed: true }); - - return verifiedToken; } } diff --git a/src/constants/error-types.constant.ts b/src/constants/error-types.constant.ts index 2fa7143..95eca7d 100644 --- a/src/constants/error-types.constant.ts +++ b/src/constants/error-types.constant.ts @@ -34,6 +34,8 @@ const errorType = [ "INITIAL_SETUP_FAILED", "NOT_ENOUGH_PERMISSION", "TOO_MANY_REQUESTS", + "TOKEN_NOT_ACTIVE", + "INVALID_TOKEN_AUDIENCE", ] as const; export const ErrorTypeEnum = z.enum(errorType); @@ -198,6 +200,14 @@ export const errorMap = { httpStatusCode: STATUS_CODES.TOO_MANY_REQUESTS, body: { code: "too_many_requests", message: "Too many requests" }, }, + [ErrorTypeEnum.enum.TOKEN_NOT_ACTIVE]: { + httpStatusCode: STATUS_CODES.UNAUTHORIZED, + body: { code: "token_not_active", message: "Token not active" }, + }, + [ErrorTypeEnum.enum.INVALID_TOKEN_AUDIENCE]: { + httpStatusCode: STATUS_CODES.UNAUTHORIZED, + body: { code: "invalid_token_audience", message: "Invalid token audience" }, + }, }; export type ErrorTypeEnum = z.infer; diff --git a/src/constants/index.ts b/src/constants/index.ts index 8f46451..f6c40bd 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -2,4 +2,5 @@ export * from "./env.constant"; export * from "./error-types.constant"; export * from "./magic-numbers.constant"; export * from "./rbac.constants"; +export * from "./service.constant"; export * from "./status-code.constant"; diff --git a/src/constants/rbac.constants.ts b/src/constants/rbac.constants.ts index be5d663..f1bc50d 100644 --- a/src/constants/rbac.constants.ts +++ b/src/constants/rbac.constants.ts @@ -1,6 +1,6 @@ import { Types } from "mongoose"; -import { PermissionCategory, createPermission } from "@/api/v1/permission/permission.validation"; +import { CreatePermission, PermissionCategory } from "@/api/v1/permission/permission.validation"; import { createRole } from "@/api/v1/role/role.validation"; import { envConstants } from "./env.constant"; @@ -64,7 +64,7 @@ export const ROLES = { USER: "user", }; -export const defaultPermissions: createPermission[] = [ +export const defaultPermissions: CreatePermission[] = [ // User Management Permissions { name: PERMISSIONS.CREATE_USERS, diff --git a/src/constants/service.constant.ts b/src/constants/service.constant.ts new file mode 100644 index 0000000..7055489 --- /dev/null +++ b/src/constants/service.constant.ts @@ -0,0 +1,10 @@ +export enum ServiceEnum { + USER_SERVICE = "user-service", + NOTIFICATION_SERVICE = "notification-service", + CMS_SERVICE = "cms-service", + CHAT_SERVICE = "chat-service", +} + +export const SERVICE_AUDIENCE = Object.values(ServiceEnum); + +export const CURRENT_SERVICE = ServiceEnum.USER_SERVICE; diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index e1e834a..a74ec8d 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -8,23 +8,37 @@ import { extractTokenFromBearerString, verifyJWTToken } from "@/utils/jwt.util"; const parseUser = async (req: Request, _: Response, next: NextFunction, tokenType: "access" | "refresh") => { try { const authHeader = req.headers.authorization; - - if (authHeader == null || authHeader === "") throw new Error(ErrorTypeEnum.enum.NO_AUTH_HEADER); + if (authHeader == null || authHeader === "") { + throw new Error(ErrorTypeEnum.enum.NO_AUTH_HEADER); + } const token = extractTokenFromBearerString(authHeader); + if (!token) { + throw new Error(ErrorTypeEnum.enum.INVALID_ACCESS); + } + let user: Auth; + try { + if (tokenType === "refresh") { + const { id } = await verifyJWTToken(token, tokenType); + user = await AuthService.verifyToken(id); + } else { + const res = await verifyJWTToken(token, tokenType); + const { userId } = res; + user = await AuthService.verifyToken(userId); + } + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error(ErrorTypeEnum.enum.INVALID_TOKEN); + } - // TODO: instead of first verifying jwt token and then calling auth service to get user, call auth service directly and add logic to verify jwt token in auth service - if (tokenType === "refresh") { - const { id }: JwtRefreshToken = await verifyJWTToken(token, tokenType); - user = await AuthService.verifyToken(id); - } else { - const { userId }: JwtAccessToken = await verifyJWTToken(token, tokenType); - user = await AuthService.verifyToken(userId); + if (user === null) { + throw new Error(ErrorTypeEnum.enum.USER_NOT_FOUND); } req.body.loggedInUser = { ...user, userId: user.userId.toString() }; - next(); } catch (error) { next(error); diff --git a/src/utils/__test__/jwt.test.ts b/src/utils/__test__/jwt.test.ts index df95f6a..c64e923 100644 --- a/src/utils/__test__/jwt.test.ts +++ b/src/utils/__test__/jwt.test.ts @@ -1,4 +1,3 @@ -import { JwtAccessToken } from "@/api/v1/auth/auth.validation"; import { ErrorTypeEnum } from "@/constants"; import { extractTokenFromBearerString, generateAccessToken, generateRefreshToken } from "@/utils"; @@ -8,8 +7,9 @@ jest.mock("jsonwebtoken", () => ({ })); describe("Jwt token", () => { - const mockdata: JwtAccessToken = { + const mockdata = { userId: "123", + permissions: [], }; beforeEach(() => { diff --git a/src/utils/jwt.util.ts b/src/utils/jwt.util.ts index 56338d6..c9e82d5 100644 --- a/src/utils/jwt.util.ts +++ b/src/utils/jwt.util.ts @@ -2,15 +2,15 @@ import jwt from "jsonwebtoken"; import { AuthDAL } from "@/api/v1/auth/auth.dal"; import { - JwtAccessToken, JwtActionToken, JwtRefreshToken, jwtAccessTokenSchema, jwtActionTokenSchema, jwtRefreshTokenSchema, } from "@/api/v1/auth/auth.validation"; -import { ErrorTypeEnum, envConstants } from "@/constants"; +import { CURRENT_SERVICE, ErrorTypeEnum, Permission, ServiceEnum, envConstants } from "@/constants"; +import { logger } from "."; import { CryptoUtil } from "./crypto.util"; const { JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, JWT_TOKEN_FOR_ACTION_SECRET } = envConstants; @@ -21,15 +21,30 @@ export const tokenSecrets = { action: JWT_TOKEN_FOR_ACTION_SECRET, }; -export const generateAccessToken = async (payload: JwtAccessToken): Promise => { +export const generateAccessToken = async (payload: { userId: string; permissions: Permission[] }): Promise => { const cryptoUtil = CryptoUtil.getInstance(); const privateKey = cryptoUtil.getPrivateKey(); - const validPayload = jwtAccessTokenSchema.parse(payload); - return jwt.sign(validPayload, privateKey, { - algorithm: "RS256", - expiresIn: "1h", - }); + try { + const tokenPayload = jwtAccessTokenSchema.parse({ + userId: payload.userId, + permissions: payload.permissions, + tokenVersion: 1, + issuedAt: Date.now(), + issuer: CURRENT_SERVICE, + audience: CURRENT_SERVICE, + }); + + return jwt.sign(tokenPayload, privateKey, { + algorithm: "RS256", + expiresIn: "1h", + audience: CURRENT_SERVICE, + issuer: CURRENT_SERVICE, + }); + } catch (error) { + logger.error("Access token generation error:", error); + throw new Error(ErrorTypeEnum.enum.INTERNAL_SERVER_ERROR); + } }; export const generateRefreshToken = async (payload: JwtRefreshToken): Promise => { @@ -40,6 +55,8 @@ export const generateRefreshToken = async (payload: JwtRefreshToken): Promise { const validPayload = jwtActionTokenSchema.parse(payload); return jwt.sign(validPayload, JWT_TOKEN_FOR_ACTION_SECRET, { expiresIn: "5m", + audience: CURRENT_SERVICE, + issuer: CURRENT_SERVICE, }); }; -export const verifyJWTToken = async (token: string, tokenType: "access" | "refresh" | "action"): Promise => { +export const verifyJWTToken = async ( + token: string, + tokenType: "access" | "refresh" | "action", + serviceName: ServiceEnum = CURRENT_SERVICE +): Promise => { try { const cryptoUtil = CryptoUtil.getInstance(); const publicKey = cryptoUtil.getPublicKey(); - const verifiedToken = jwt.verify(token, publicKey, { - algorithms: ["RS256"], - }); + const secret = tokenType === "action" ? JWT_TOKEN_FOR_ACTION_SECRET : publicKey; + const algorithm = tokenType === "action" ? "HS256" : "RS256"; + + const verifyOptions = { + algorithms: [algorithm] as jwt.Algorithm[], + audience: serviceName, + issuer: CURRENT_SERVICE, + }; + + const verifiedToken = jwt.verify(token, secret, verifyOptions) as T; - // Only check refresh tokens in database if (tokenType === "refresh") { const storedToken = await AuthDAL.getAuthByRefreshToken(token); if (!storedToken) { @@ -67,17 +96,21 @@ export const verifyJWTToken = async (token: string, tokenType: "access" | "re } } - return verifiedToken as T; + return verifiedToken; } catch (error) { if (error instanceof jwt.TokenExpiredError) { throw new Error(ErrorTypeEnum.enum.TOKEN_EXPIRED); - } else if (error instanceof jwt.JsonWebTokenError) { + } + if (error instanceof jwt.JsonWebTokenError) { + if (error.message.includes("audience")) { + throw new Error(ErrorTypeEnum.enum.INVALID_TOKEN_AUDIENCE); + } throw new Error(ErrorTypeEnum.enum.INVALID_TOKEN); - } else if (error instanceof jwt.NotBeforeError) { - throw new Error(ErrorTypeEnum.enum.INTERNAL_SERVER_ERROR); - } else { - throw error; } + if (error instanceof jwt.NotBeforeError) { + throw new Error(ErrorTypeEnum.enum.TOKEN_NOT_ACTIVE); + } + throw new Error(ErrorTypeEnum.enum.INVALID_TOKEN); } }; diff --git a/src/utils/test/permission.utils.ts b/src/utils/test/permission.utils.ts index 12355d2..eaa7271 100644 --- a/src/utils/test/permission.utils.ts +++ b/src/utils/test/permission.utils.ts @@ -1,15 +1,15 @@ import supertest, { Response } from "supertest"; import { success } from "@/api/v1/permission/permission.constant"; -import { createPermission } from "@/api/v1/permission/permission.validation"; +import { CreatePermission } from "@/api/v1/permission/permission.validation"; import { app } from "@/app"; import { STATUS_CODES } from "@/constants"; -export const createPermissionRequest = async (permission: createPermission): Promise => { +export const createPermissionRequest = async (permission: CreatePermission): Promise => { return await supertest(app).post("/api/v1/permissions").send(permission); }; -export const expectCreatePermissionSuccess = (response: Response, permission: createPermission): void => { +export const expectCreatePermissionSuccess = (response: Response, permission: CreatePermission): void => { const { statusCode, body } = response; expect(statusCode).toBe(STATUS_CODES.CREATED);