Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions config/custom-environment-variables.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,6 @@ module.exports = {
__name: "USER_TOKEN_REFRESH_TTL",
__format: "number",
},
impersonationTtl: {
__name: "USER_TOKEN_IMPERSONATION_TTL",
__format: "number",
},
publicKey: "PUBLIC_KEY",
privateKey: "PRIVATE_KEY",
},
Expand All @@ -100,10 +96,6 @@ module.exports = {
botPublicKey: "BOT_PUBLIC_KEY",
},

discordService: {
publicKey: "DISCORD_SERVICE_PUBLIC_KEY",
},

cronJobHandler: {
publicKey: "CRON_JOB_PUBLIC_KEY",
},
Expand Down
5 changes: 0 additions & 5 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ module.exports = {
cookieV2Name: `rds-session-v2-${NODE_ENV}`,
ttl: 30 * 24 * 60 * 60, // in seconds
refreshTtl: 180 * 24 * 60 * 60, // in seconds
impersonationTtl: 15 * 60, // in seconds
publicKey: "<publicKey>",
privateKey: "<privateKey>",
},
Expand All @@ -95,10 +94,6 @@ module.exports = {
botPublicKey: "<botpublicKey>",
},

discordService: {
publicKey: "DISCORD_SERVICE_PUBLIC_KEY",
},

// Cloudinary keys
cloudinary: {
cloud_name: "Cloud_name",
Expand Down
7 changes: 1 addition & 6 deletions constants/bot.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
const CLOUDFLARE_WORKER = "Cloudflare Worker";
const BAD_TOKEN = "BAD.JWT.TOKEN";
const CRON_JOB_HANDLER = "Cron Job Handler";
const DISCORD_SERVICE = "Discord Service";

const Services = {
CLOUDFLARE_WORKER: CLOUDFLARE_WORKER,
CRON_JOB_HANDLER: CRON_JOB_HANDLER,
};

const DiscordServiceHeader = {
name: "x-service-name"
}

module.exports = { CLOUDFLARE_WORKER, BAD_TOKEN, CRON_JOB_HANDLER, Services, DISCORD_SERVICE, DiscordServiceHeader };
module.exports = { CLOUDFLARE_WORKER, BAD_TOKEN, CRON_JOB_HANDLER, Services };
15 changes: 4 additions & 11 deletions constants/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const REQUEST_LOG_TYPE = {
REQUEST_CANCELLED: "REQUEST_CANCELLED",
REQUEST_UPDATED: "REQUEST_UPDATED",
PENDING_REQUEST_FOUND: "PENDING_REQUEST_FOUND",
REQUEST_ALREADY_APPROVED: "REQUEST_ALREADY_APPROVED",
REQUEST_ALREADY_REJECTED: "REQUEST_ALREADY_REJECTED",
};

export const REQUEST_CREATED_SUCCESSFULLY = "Request created successfully";
Expand All @@ -39,7 +41,9 @@ export const REQUEST_ALREADY_REJECTED = "Request already rejected";
export const ERROR_WHILE_FETCHING_REQUEST = "Error while fetching request";
export const ERROR_WHILE_CREATING_REQUEST = "Error while creating request";
export const ERROR_WHILE_UPDATING_REQUEST = "Error while updating request";
export const ERROR_WHILE_ACKNOWLEDGING_REQUEST = "Error while acknowledging request";

export const REQUEST_ID_REQUIRED = "Request id is required";
export const REQUEST_DOES_NOT_EXIST = "Request does not exist";
export const REQUEST_ALREADY_PENDING = "Request already exists please wait for approval or rejection";
export const UNAUTHORIZED_TO_CREATE_OOO_REQUEST = "Unauthorized to create OOO request";
Expand Down Expand Up @@ -68,14 +72,3 @@ export const INVALID_REQUEST_TYPE = "Invalid request type";
export const INVALID_REQUEST_DEADLINE = "New deadline of the request must be greater than old deadline";
export const REQUEST_UPDATED_SUCCESSFULLY = "Request updated successfully";
export const UNAUTHORIZED_TO_UPDATE_REQUEST = "Unauthorized to update request";

export const FEATURE_NOT_IMPLEMENTED = "Feature not implemented";

export const INVALID_ACTION_PARAM = "Invalid 'action' parameter: must be either 'START' or 'STOP'";

export const OPERATION_NOT_ALLOWED = "You are not allowed for this operation at the moment";

export const IMPERSONATION_LOG_TYPE = {
SESSION_STARTED:"SESSION_STARTED",
SESSION_STOPPED:"SESSION_STOPPED"
}
50 changes: 49 additions & 1 deletion controllers/oooRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
REQUEST_ALREADY_PENDING,
USER_STATUS_NOT_FOUND,
OOO_STATUS_ALREADY_EXIST,
UNAUTHORIZED_TO_UPDATE_REQUEST,
ERROR_WHILE_ACKNOWLEDGING_REQUEST,
REQUEST_ID_REQUIRED,
} from "../constants/requests";
import { statusState } from "../constants/userStatus";
import { logType } from "../constants/logs";
Expand All @@ -20,9 +23,11 @@ import { getRequestByKeyValues, getRequests, updateRequest } from "../models/req
import { createUserFutureStatus } from "../models/userFutureStatus";
import { getUserStatus, addFutureStatus } from "../models/userStatus";
import { createOooRequest, validateUserStatus } from "../services/oooRequest";
import * as oooRequestService from "../services/oooRequest";
import { CustomResponse } from "../typeDefinitions/global";
import { OooRequestCreateRequest, OooRequestResponse, OooStatusRequest } from "../types/oooRequest";
import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse, OooStatusRequest } from "../types/oooRequest";
import { UpdateRequest } from "../types/requests";
import { NextFunction } from "express";

/**
* Controller to handle the creation of OOO requests.
Expand Down Expand Up @@ -148,3 +153,46 @@ export const updateOooRequestController = async (req: UpdateRequest, res: Custom
return res.boom.badImplementation(ERROR_WHILE_UPDATING_REQUEST);
}
};

/**
* Acknowledges an Out-of-Office (OOO) request
*
* @param {AcknowledgeOooRequest} req - The request object.
* @param {OooRequestResponse} res - The response object.
* @returns {Promise<OooRequestResponse>} Resolves with success or failure.
*/
export const acknowledgeOooRequest = async (
req: AcknowledgeOooRequest,
res: OooRequestResponse,
next: NextFunction
)
: Promise<OooRequestResponse> => {
try {
const dev = req.query.dev === "true";
if(!dev) return res.boom.notImplemented("Feature not implemented");

const isSuperuser = req.userData?.roles?.super_user;
if (!isSuperuser) {
return res.boom.forbidden(UNAUTHORIZED_TO_UPDATE_REQUEST);
}

const requestBody = req.body;
const superUserId = req.userData.id;
const requestId = req.params.id;

if (!requestId) {
return res.boom.badRequest(REQUEST_ID_REQUIRED);
}

const response = await oooRequestService.acknowledgeOooRequest(requestId, requestBody, superUserId);

return res.status(200).json({
message: response.message,
});
}
catch(error){
logger.error(ERROR_WHILE_ACKNOWLEDGING_REQUEST, error);
next(error);
return res;
}
};
13 changes: 8 additions & 5 deletions controllers/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
} from "../constants/requests";
import { getRequests } from "../models/requests";
import { getPaginatedLink } from "../utils/helper";
import { createOooRequestController, updateOooRequestController } from "./oooRequests";
import { OooRequestCreateRequest, OooRequestResponse } from "../types/oooRequest";
import { acknowledgeOooRequest, createOooRequestController, updateOooRequestController } from "./oooRequests";
import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse } from "../types/oooRequest";
import { CustomResponse } from "../typeDefinitions/global";
import { ExtensionRequestRequest, ExtensionRequestResponse } from "../types/extensionRequests";
import { createTaskExtensionRequest, updateTaskExtensionRequest } from "./extensionRequestsv2";
Expand All @@ -16,8 +16,7 @@ import { createTaskRequestController } from "./taskRequestsv2";
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse, UpdateOnboardingExtensionStateRequest } from "../types/onboardingExtension";
import { createOnboardingExtensionRequestController, updateOnboardingExtensionRequestController, updateOnboardingExtensionRequestState } from "./onboardingExtension";
import { UpdateOnboardingExtensionRequest } from "../types/onboardingExtension";

import { Request } from "express";
import { NextFunction, Request } from "express";

export const createRequestController = async (
req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest,
Expand Down Expand Up @@ -121,9 +120,13 @@ export const getRequestsController = async (req: any, res: any) => {
* @param {CustomResponse} res - The response object.
* @returns {Promise<void>} Resolves or sends an error for invalid types.
*/
export const updateRequestBeforeAcknowledgedController = async (req: Request, res: CustomResponse) => {
export const updateRequestBeforeAcknowledgedController = async (req: Request, res: CustomResponse, next: NextFunction) => {
const type = req.body.type;

switch(type){
case REQUEST_TYPE.OOO:
await acknowledgeOooRequest(req as AcknowledgeOooRequest, res as OooRequestResponse, next);
break;
case REQUEST_TYPE.ONBOARDING:
await updateOnboardingExtensionRequestController(req as UpdateOnboardingExtensionRequest, res as OnboardingExtensionResponse);
break;
Expand Down
81 changes: 31 additions & 50 deletions middlewares/authenticate.js
Original file line number Diff line number Diff line change
@@ -1,82 +1,62 @@
const authService = require("../services/authService");
const dataAccess = require("../services/dataAccessLayer");
const logger = require("../utils/logger");

/**
* Middleware to check if the user is restricted or in an impersonation session.
*
* - If the user is impersonating, only GET requests and the STOP impersonation route are allowed.
* - If the user is restricted (based on roles), only GET requests are permitted.
* Middleware to check if the user has been restricted. If user is restricted,
* then only allow read requests and do not allow to any edit/create requests.
*
* Note: This requires that user is authenticated hence must be called after
* the user authentication middleware. We are calling it from within the
* `authenticate` middleware itself to avoid explicitly adding this middleware
* while defining routes.
*
* @async
* @function checkRestricted
* @param {import('express').Request} req - Express request object
* @param {import('express').Response} res - Express response object
* @param {Function} next - Express next middleware function
* @returns {void}
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express middleware function
* @returns {Object} - Returns unauthorized object if user has been restricted.
*/
const checkRestricted = async (req, res, next) => {
const { roles } = req.userData;

if (req.isImpersonating) {
const isStopImpersonationRoute =
req.method === "PATCH" &&
req.baseUrl === "/impersonation" &&
/^\/[a-zA-Z0-9_-]+$/.test(req.path) &&
req.query.action === "STOP";

if (req.method !== "GET" && !isStopImpersonationRoute) {
return res.boom.forbidden("Only viewing is permitted during impersonation");
}
}

if (roles && roles.restricted && req.method !== "GET") {
return res.boom.forbidden("You are restricted from performing this action");
}

return next();
};

/**
* Authentication middleware that:
* 1. Verifies JWT token from cookies (or headers in non-production).
* 2. Handles impersonation if applicable.
* 3. Refreshes token if it's expired but still within the refresh TTL window.
* 4. Attaches user data to `req.userData` for downstream use.
* Middleware to validate the authenticated routes
* 1] Verifies the token and adds user info to `req.userData` for further use
* 2] In case of JWT expiry, adds a new JWT to the response if `currTime - tokenInitialisationTime <= refreshTtl`
*
* @async
* @function
* @param {import('express').Request} req - Express request object
* @param {import('express').Response} res - Express response object
* @param {Function} next - Express next middleware function
* @returns {Promise<void>} - Calls `next()` on successful authentication or returns an error response.
* The currently implemented mechanism satisfies the current use case.
* Authentication with JWT and a refreshToken to be added once we have user permissions and authorizations to be handled
*
* @todo: Add tests to assert on refreshed JWT generation by modifying the TTL values for the specific test. Currently not possible in the absence of a test-suite.
*
*
* @param req {Object} - Express request object
* @param res {Object} - Express response object
* @param next {Function} - Express middleware function
* @return {Object} - Returns unauthenticated object if token is invalid
*/
module.exports = async (req, res, next) => {
try {
let token = req.cookies[config.get("userToken.cookieName")];

/**
* Enable Bearer Token authentication for NON-PRODUCTION environments.
* Useful for Swagger or manual testing where cookies are not easily managed.
* Enable Bearer Token authentication for NON-PRODUCTION environments
* This is enabled as Swagger UI does not support cookie authe
*/
Comment on lines 46 to 49
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete Security Feature Documentation category Documentation

Tell me more
What is the issue?

The comment contains a typo ('authe' instead of 'auth') and doesn't explain the security implications of this feature.

Why this matters

Unclear documentation of security-related features can lead to misuse or security vulnerabilities.

Suggested change ∙ Feature Preview

/**

  • Enable Bearer Token authentication for NON-PRODUCTION environments only.
  • This is enabled as Swagger UI does not support cookie auth.
  • Note: This is less secure than cookie-based auth and should never be enabled in production.
    */
Provide feedback to improve future suggestions

Nice Catch Incorrect Not in Scope Not in coding standard Other

💬 Looking for more details? Reply to this comment to chat with Korbit.

if (process.env.NODE_ENV !== "production" && !token) {
token = req.headers.authorization?.split(" ")[1];
token = req.headers.authorization.split(" ")[1];
}
Comment on lines 50 to 52
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsafe Authorization Header Access category Error Handling

Tell me more
What is the issue?

The code assumes req.headers.authorization exists when accessing it, which could cause a runtime error if authorization header is missing.

Why this matters

If authorization header is undefined, attempting to call split() on it will throw a TypeError and crash the application.

Suggested change ∙ Feature Preview

Add a check for the existence of the authorization header before accessing it:

if (process.env.NODE_ENV !== 'production' && !token) {
      token = req.headers.authorization?.split(' ')[1] || null;
}
Provide feedback to improve future suggestions

Nice Catch Incorrect Not in Scope Not in coding standard Other

💬 Looking for more details? Reply to this comment to chat with Korbit.


const { userId, impersonatedUserId } = authService.verifyAuthToken(token);
// `req.isImpersonating` keeps track of the impersonation session
req.isImpersonating = Boolean(impersonatedUserId);

const userData = impersonatedUserId
? await dataAccess.retrieveUsers({ id: impersonatedUserId })
: await dataAccess.retrieveUsers({ id: userId });
const { userId } = authService.verifyAuthToken(token);

// add user data to `req.userData` for further use
const userData = await dataAccess.retrieveUsers({ id: userId });
req.userData = userData.user;

return checkRestricted(req, res, next);
} catch (err) {
logger.error(err);
Expand All @@ -98,13 +78,14 @@ module.exports = async (req, res, next) => {
sameSite: "lax",
});

const userData = await dataAccess.retrieveUsers({ id: userId });
req.userData = userData.user;

// add user data to `req.userData` for further use
req.userData = await dataAccess.retrieveUsers({ id: userId });
return checkRestricted(req, res, next);
} else {
return res.boom.unauthorized("Unauthenticated User");
}
} else {
return res.boom.unauthorized("Unauthenticated User");
}
return res.boom.unauthorized("Unauthenticated User");
}
};
16 changes: 6 additions & 10 deletions middlewares/authorizeBot.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const botVerifcation = require("../services/botVerificationService");
const { CLOUDFLARE_WORKER, CRON_JOB_HANDLER, DISCORD_SERVICE, DiscordServiceHeader } = require("../constants/bot");
const { CLOUDFLARE_WORKER, CRON_JOB_HANDLER } = require("../constants/bot");

const verifyCronJob = async (req, res, next) => {
try {
Expand All @@ -18,23 +18,19 @@ const verifyCronJob = async (req, res, next) => {
const verifyDiscordBot = async (req, res, next) => {
try {
const token = req.headers.authorization.split(" ")[1];
const serviceName = req.headers[DiscordServiceHeader.name] || "";

if (serviceName === DISCORD_SERVICE && botVerifcation.verifyDiscordService(token).name === DISCORD_SERVICE) {
return next();
}

const data = botVerifcation.verifyToken(token);
if (data.name === CLOUDFLARE_WORKER) {
return next();

if (data.name !== CLOUDFLARE_WORKER) {
return res.boom.unauthorized("Unauthorized Bot");
}

return res.boom.unauthorized("Unauthorized Bot");
return next();
} catch (error) {
if (error.message === "invalid token") {
return res.boom.unauthorized("Unauthorized Bot");
}
return res.boom.badRequest("Invalid Request");
}
};

module.exports = { verifyDiscordBot, verifyCronJob };
Loading
Loading