Files
oneuptime/Common/Server/Utils/Cookie.ts
Nawaz Dhandala a62ba231be feat: Add DashboardDomain model and associated API services
- Introduced DashboardDomain model with comprehensive fields for managing custom domains for dashboards.
- Implemented DashboardDomainAPI for handling CNAME verification and SSL provisioning.
- Created DashboardDomainService to manage domain-related operations, including SSL ordering and CNAME validation.
- Added master password handling in DashboardAPI for enhanced security.
- Defined constants for master password messages and cookie management.
2026-03-26 11:32:06 +00:00

428 lines
11 KiB
TypeScript

import { ExpressRequest, ExpressResponse } from "./Express";
import Dictionary from "../../Types/Dictionary";
import ObjectID from "../../Types/ObjectID";
import { CookieOptions } from "express";
import JSONWebToken from "./JsonWebToken";
import User from "../../Models/DatabaseModels/User";
import StatusPagePrivateUser from "../../Models/DatabaseModels/StatusPagePrivateUser";
import OneUptimeDate from "../../Types/Date";
import PositiveNumber from "../../Types/PositiveNumber";
import CookieName from "../../Types/CookieName";
import {
MASTER_PASSWORD_COOKIE_IDENTIFIER,
MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
} from "../../Types/StatusPage/MasterPassword";
import {
DASHBOARD_MASTER_PASSWORD_COOKIE_IDENTIFIER,
DASHBOARD_MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
} from "../../Types/Dashboard/MasterPassword";
import CaptureSpan from "./Telemetry/CaptureSpan";
export default class CookieUtil {
// set cookie with express response
private static readonly DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS: number = 15 * 60;
@CaptureSpan()
public static getCookiesFromCookieString(
cookieString: string,
): Dictionary<string> {
const cookies: Dictionary<string> = {};
cookieString.split(";").forEach((cookie: string) => {
const parts: string[] = cookie.split("=");
const key: string = (parts[0] as string).trim() as string;
const value: string = parts[1] as string;
cookies[key] = value;
});
return cookies;
}
@CaptureSpan()
public static setSSOCookie(data: {
user: User;
projectId: ObjectID;
expressResponse: ExpressResponse;
}): void {
const { user, projectId, expressResponse: res } = data;
const ssoToken: string = JSONWebToken.sign({
data: {
userId: user.id!,
projectId: projectId,
name: user.name!,
email: user.email,
isMasterAdmin: false,
isGeneralLogin: false,
},
expiresInSeconds: OneUptimeDate.getSecondsInDays(new PositiveNumber(30)),
});
CookieUtil.setCookie(res, CookieUtil.getUserSSOKey(projectId), ssoToken, {
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
httpOnly: true,
});
}
@CaptureSpan()
public static setUserCookie(data: {
expressResponse: ExpressResponse;
user: User;
isGlobalLogin: boolean;
sessionId: ObjectID;
refreshToken: string;
refreshTokenExpiresAt: Date;
accessTokenExpiresInSeconds?: number;
}): void {
const {
expressResponse: res,
user,
isGlobalLogin,
sessionId,
refreshToken,
refreshTokenExpiresAt,
} = data;
const accessTokenExpiresInSeconds: number =
data.accessTokenExpiresInSeconds ||
CookieUtil.DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS;
const token: string = JSONWebToken.signUserLoginToken({
tokenData: {
userId: user.id!,
email: user.email!,
name: user.name!,
timezone: user.timezone || null,
isMasterAdmin: user.isMasterAdmin!,
isGlobalLogin: isGlobalLogin, // This is a general login without SSO. So, we will set this to true. This will give access to all the projects that dont require SSO.
sessionId: sessionId,
},
expiresInSeconds: accessTokenExpiresInSeconds,
});
// Set a cookie with token.
CookieUtil.setCookie(res, CookieUtil.getUserTokenKey(), token, {
maxAge: accessTokenExpiresInSeconds * 1000,
httpOnly: true,
});
const refreshTokenTtl: number = Math.max(
refreshTokenExpiresAt.getTime() - Date.now(),
0,
);
CookieUtil.setCookie(res, CookieUtil.getRefreshTokenKey(), refreshToken, {
maxAge: refreshTokenTtl,
httpOnly: true,
});
if (user.id) {
// set user id cookie
CookieUtil.setCookie(res, CookieName.UserID, user.id!.toString(), {
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
httpOnly: false,
});
}
if (user.email) {
// set user email cookie
CookieUtil.setCookie(
res,
CookieName.Email,
user.email?.toString() || "",
{
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
httpOnly: false,
},
);
}
if (user.name) {
// set user name cookie
CookieUtil.setCookie(res, CookieName.Name, user.name?.toString() || "", {
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
httpOnly: false,
});
}
if (user.timezone) {
// set user timezone cookie
CookieUtil.setCookie(
res,
CookieName.Timezone,
user.timezone?.toString() || "",
{
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
httpOnly: false,
},
);
}
if (user.isMasterAdmin) {
// set user isMasterAdmin cookie
CookieUtil.setCookie(
res,
CookieName.IsMasterAdmin,
user.isMasterAdmin?.toString() || "",
{
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
httpOnly: false,
},
);
}
if (user.profilePictureId) {
// set user profile picture id cookie
CookieUtil.setCookie(
res,
CookieName.ProfilePicID,
user.profilePictureId?.toString() || "",
{
maxAge: OneUptimeDate.getMillisecondsInDays(new PositiveNumber(30)),
httpOnly: false,
},
);
}
}
@CaptureSpan()
public static setStatusPagePrivateUserCookie(data: {
expressResponse: ExpressResponse;
user: StatusPagePrivateUser;
statusPageId: ObjectID;
sessionId: ObjectID;
refreshToken: string;
refreshTokenExpiresAt: Date;
accessTokenExpiresInSeconds?: number;
}): string {
const {
expressResponse: res,
user,
statusPageId,
sessionId,
refreshToken,
refreshTokenExpiresAt,
} = data;
const accessTokenExpiresInSeconds: number =
data.accessTokenExpiresInSeconds ||
CookieUtil.DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS;
const token: string = JSONWebToken.sign({
data: {
userId: user.id!,
email: user.email!,
statusPageId: statusPageId,
sessionId: sessionId,
},
expiresInSeconds: accessTokenExpiresInSeconds,
});
CookieUtil.setCookie(res, CookieUtil.getUserTokenKey(statusPageId), token, {
maxAge: accessTokenExpiresInSeconds * 1000,
httpOnly: true,
});
const refreshTokenTtl: number = Math.max(
refreshTokenExpiresAt.getTime() - Date.now(),
0,
);
CookieUtil.setCookie(
res,
CookieUtil.getRefreshTokenKey(statusPageId),
refreshToken,
{
maxAge: refreshTokenTtl,
httpOnly: true,
},
);
return token;
}
@CaptureSpan()
public static setStatusPageMasterPasswordCookie(data: {
expressResponse: ExpressResponse;
statusPageId: ObjectID;
}): void {
const expiresInDays: PositiveNumber = new PositiveNumber(
MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
);
const token: string = JSONWebToken.signJsonPayload(
{
statusPageId: data.statusPageId.toString(),
type: MASTER_PASSWORD_COOKIE_IDENTIFIER,
},
OneUptimeDate.getSecondsInDays(expiresInDays),
);
CookieUtil.setCookie(
data.expressResponse,
CookieUtil.getStatusPageMasterPasswordKey(data.statusPageId),
token,
{
maxAge: OneUptimeDate.getMillisecondsInDays(expiresInDays),
httpOnly: true,
},
);
}
@CaptureSpan()
public static setCookie(
res: ExpressResponse,
name: string | CookieName,
value: string,
options: CookieOptions,
): void {
const cookieOptions: CookieOptions = {
path: "/",
sameSite: "lax",
...options,
};
res.cookie(name, value, cookieOptions);
}
// get cookie with express request
@CaptureSpan()
public static getCookieFromExpressRequest(
req: ExpressRequest,
name: string,
): string | undefined {
return req.cookies[name];
}
@CaptureSpan()
public static getRefreshTokenFromExpressRequest(
req: ExpressRequest,
id?: ObjectID,
): string | undefined {
return CookieUtil.getCookieFromExpressRequest(
req,
CookieUtil.getRefreshTokenKey(id),
);
}
// delete cookie with express response
@CaptureSpan()
public static removeCookie(res: ExpressResponse, name: string): void {
res.clearCookie(name, {
path: "/",
sameSite: "lax",
});
}
@CaptureSpan()
public static removeStatusPageMasterPasswordCookie(
res: ExpressResponse,
statusPageId: ObjectID,
): void {
CookieUtil.removeCookie(
res,
CookieUtil.getStatusPageMasterPasswordKey(statusPageId),
);
}
@CaptureSpan()
public static setDashboardMasterPasswordCookie(data: {
expressResponse: ExpressResponse;
dashboardId: ObjectID;
}): void {
const expiresInDays: PositiveNumber = new PositiveNumber(
DASHBOARD_MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
);
const token: string = JSONWebToken.signJsonPayload(
{
dashboardId: data.dashboardId.toString(),
type: DASHBOARD_MASTER_PASSWORD_COOKIE_IDENTIFIER,
},
OneUptimeDate.getSecondsInDays(expiresInDays),
);
CookieUtil.setCookie(
data.expressResponse,
CookieUtil.getDashboardMasterPasswordKey(data.dashboardId),
token,
{
maxAge: OneUptimeDate.getMillisecondsInDays(expiresInDays),
httpOnly: true,
},
);
}
@CaptureSpan()
public static removeDashboardMasterPasswordCookie(
res: ExpressResponse,
dashboardId: ObjectID,
): void {
CookieUtil.removeCookie(
res,
CookieUtil.getDashboardMasterPasswordKey(dashboardId),
);
}
@CaptureSpan()
public static getDashboardMasterPasswordKey(id: ObjectID): string {
return `${CookieName.DashboardMasterPassword}-${id.toString()}`;
}
// get all cookies with express request
@CaptureSpan()
public static getAllCookies(req: ExpressRequest): Dictionary<string> {
return req.cookies || {};
}
@CaptureSpan()
public static getUserTokenKey(id?: ObjectID): string {
if (!id) {
return CookieName.Token;
}
return `${CookieName.Token}-${id.toString()}`;
}
@CaptureSpan()
public static getRefreshTokenKey(id?: ObjectID): string {
if (!id) {
return CookieName.RefreshToken;
}
return `${CookieName.RefreshToken}-${id.toString()}`;
}
@CaptureSpan()
public static getStatusPageMasterPasswordKey(id: ObjectID): string {
return `${CookieName.StatusPageMasterPassword}-${id.toString()}`;
}
@CaptureSpan()
public static getUserSSOKey(id: ObjectID): string {
return `${this.getSSOKey()}${id.toString()}`;
}
@CaptureSpan()
public static getSSOKey(): string {
return `sso-`;
}
// delete all cookies.
@CaptureSpan()
public static removeAllCookies(
req: ExpressRequest,
res: ExpressResponse,
): void {
const cookies: Dictionary<string> = this.getAllCookies(req);
for (const key in cookies) {
this.removeCookie(res, key);
}
// Always attempt to remove refresh token cookie even if not parsed.
this.removeCookie(res, this.getRefreshTokenKey());
}
}