make auth httponly

This commit is contained in:
Simon Larsen
2023-10-13 13:25:38 +01:00
parent e7682c826d
commit 725f41ef1b
17 changed files with 184 additions and 96 deletions

View File

@@ -16,9 +16,6 @@ export default abstract class LoginUtil {
User
) as User;
const token: string = value['token'] as string;
UserUtil.setAccessToken(token);
UserUtil.setEmail(user.email as Email);
UserUtil.setUserId(user.id as ObjectID);
UserUtil.setName(user.name || new Name(''));

View File

@@ -29,6 +29,7 @@ import SsoAuthorizationException from 'Common/Types/Exception/SsoAuthorizationEx
import JSONWebTokenData from 'Common/Types/JsonWebTokenData';
import logger from '../Utils/Logger';
import Exception from 'Common/Types/Exception/Exception';
import CookieUtil from '../Utils/Cookie';
export default class UserMiddleware {
/*
@@ -41,16 +42,8 @@ export default class UserMiddleware {
public static getAccessToken(req: ExpressRequest): string | null {
let accessToken: string | null = null;
if (req.headers['authorization']) {
accessToken = req.headers['authorization'] as string;
}
if (req.query['accessToken']) {
accessToken = req.query['accessToken'] as string;
}
if (accessToken?.includes(' ')) {
accessToken = accessToken.split(' ')[1] || '';
if(CookieUtil.getCookie(req, "user-token")){
accessToken = CookieUtil.getCookie(req, "user-token");
}
return accessToken;
@@ -59,10 +52,14 @@ export default class UserMiddleware {
public static getSsoTokens(req: ExpressRequest): Dictionary<string> {
const ssoTokens: Dictionary<string> = {};
for (const key of Object.keys(req.headers)) {
// get sso tokens from cookies.
const cookies: Dictionary<string> = CookieUtil.getAllCookies(req);
for (const key of Object.keys(cookies)) {
if (key.startsWith('sso-')) {
const value: string | undefined | Array<string> =
req.headers[key];
cookies[key];
let projectId: string | undefined = undefined;
try {
@@ -80,7 +77,7 @@ export default class UserMiddleware {
typeof value === 'string' &&
typeof projectId === 'string'
) {
ssoTokens[projectId] = req.headers[key] as string;
ssoTokens[projectId] = cookies[key] as string;
}
}
}

View File

@@ -0,0 +1,28 @@
import { CookieOptions } from "express";
import { ExpressRequest, ExpressResponse } from "./Express";
export default class CookieUtil {
// set cookie with express response
public static setCookie(res: ExpressResponse, name: string, value: string, options: CookieOptions) {
res.cookie(name, value, options);
}
// get cookie with express request
public static getCookie(req: ExpressRequest, name: string) {
return req.cookies[name];
}
// delete cookie with express response
public static removeCookie(res: ExpressResponse, name: string) {
res.clearCookie(name);
}
// get all cookies with express request
public static getAllCookies(req: ExpressRequest) {
return req.cookies;
}
}

View File

@@ -34,7 +34,7 @@ import HTTPResponse from 'Common/Types/API/HTTPResponse';
import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse';
import ServerException from 'Common/Types/Exception/ServerException';
import zlib from 'zlib';
// import OpenTelemetrySDK from "./OpenTelemetry";
import CookieParser from 'cookie-parser';
// Make sure we have stack trace for debugging.
Error.stackTraceLimit = Infinity;
@@ -44,6 +44,7 @@ const app: ExpressApplication = Express.getExpressApp();
app.disable('x-powered-by');
app.set('port', process.env['PORT']);
app.set('view engine', 'ejs');
app.use(CookieParser());
const jsonBodyParserMiddleware: Function = ExpressJson({
limit: '50mb',

View File

@@ -23,6 +23,7 @@
"axios": "^1.3.3",
"bullmq": "^3.6.6",
"Common": "file:../Common",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"cron-parser": "^4.8.1",
"dotenv": "^16.0.0",
@@ -49,6 +50,7 @@
},
"devDependencies": {
"@faker-js/faker": "^6.3.1",
"@types/cookie-parser": "^1.4.4",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/jest": "^27.4.1",
@@ -3660,6 +3662,15 @@
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"node_modules/@types/cookie-parser": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.4.tgz",
"integrity": "sha512-Var+aj5I6ZgIqsQ05N2V8q5OBrFfZXtIGWWDSrEYLIbMw758obagSwdGcLCjwh1Ga7M7+wj0SDIAaAC/WT7aaA==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/cookies": {
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz",
@@ -4935,6 +4946,26 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
"dependencies": {
"cookie": "0.4.1",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",

View File

@@ -25,6 +25,7 @@
"axios": "^1.3.3",
"bullmq": "^3.6.6",
"Common": "file:../Common",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"cron-parser": "^4.8.1",
"dotenv": "^16.0.0",
@@ -51,6 +52,7 @@
},
"devDependencies": {
"@faker-js/faker": "^6.3.1",
"@types/cookie-parser": "^1.4.4",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/jest": "^27.4.1",

View File

@@ -68,9 +68,6 @@ class BaseAPI extends API {
let defaultHeaders: Headers = this.getDefaultHeaders();
const headers: Headers = {};
if (User.isLoggedIn()) {
headers['Authorization'] = 'Basic ' + User.getAccessToken();
}
const globalPermissionsHash: string = LocalStorage.getItem(
'global-permissions-hash'

View File

@@ -0,0 +1,55 @@
import URL from 'Common/Types/API/URL';
import Email from 'Common/Types/Email';
import { JSONObject, JSONValue } from 'Common/Types/JSON';
import Typeof from 'Common/Types/Typeof';
import JSONFunctions from 'Common/Types/JSONFunctions';
import UniversalCookies from 'universal-cookie';
import Route from 'Common/Types/API/Route';
export default class Cookie {
public static setItem(key: string, value: JSONValue | Email | URL, options?: {
httpOnly?: boolean | undefined,
path: Route,
} | undefined): void {
if (typeof value === Typeof.Object) {
// if of type jsonobject.
value = JSON.stringify(
JSONFunctions.serializeValue(value as JSONValue) as JSONObject
);
}
const cookies: UniversalCookies = new UniversalCookies();
cookies.set(key, value as string, {
httpOnly: options?.httpOnly || false,
path: options?.path ? options.path.toString() : '/'
});
}
public static getItem(key: string): JSONValue {
const cookies: UniversalCookies = new UniversalCookies();
const value: JSONValue = cookies.get(key) as JSONValue;
try {
if (value) {
return JSONFunctions.deserializeValue(
JSONFunctions.parse(value?.toString())
);
}
return value;
} catch (err) {
return value;
}
}
public static removeItem(key: string): void {
const cookies: UniversalCookies = new UniversalCookies();
cookies.remove(key);
}
// check if cookie exists
public static exists(key: string): boolean {
const cookies: UniversalCookies = new UniversalCookies();
return Boolean(cookies.get(key));
}
}

View File

@@ -17,7 +17,6 @@ import Dictionary from 'Common/Types/Dictionary';
import ProjectUtil from '../Project';
import Sort from './Sort';
import Project from 'Model/Models/Project';
import User from '../User';
import Navigation from '../Navigation';
export class ModelAPIHttpResponse<
@@ -319,7 +318,6 @@ export default class ModelAPI {
headers = {
...headers,
...User.getAllSsoTokens(),
};
if (requestOptions && requestOptions.isMultiTenantRequest) {

View File

@@ -8,9 +8,6 @@ import BadDataException from 'Common/Types/Exception/BadDataException';
import Dictionary from 'Common/Types/Dictionary';
export default class User {
public static getAccessToken(): string {
return LocalStorage.getItem('access_token') as string;
}
public static setProfilePicId(id: ObjectID | null): void {
if (!id) {
@@ -31,20 +28,6 @@ export default class User {
);
}
public static setAccessToken(token: string): void {
LocalStorage.setItem('access_token', token);
}
public static setSsoToken(projectId: ObjectID, token: string): void {
LocalStorage.setItem('sso_' + projectId.toString(), token);
}
public static getSsoToken(projectId: ObjectID): string | null {
return LocalStorage.getItem('sso_' + projectId.toString()) as
| string
| null;
}
public static isCardRegistered(): boolean {
return Boolean(LocalStorage.getItem('cardRegistered'));
}
@@ -69,7 +52,11 @@ export default class User {
LocalStorage.setItem('user_name', name.toString());
}
public static getEmail(): Email {
public static getEmail(): Email | null {
if(!LocalStorage.getItem('user_email')){
return null;
}
return new Email(LocalStorage.getItem('user_email') as string);
}
@@ -94,29 +81,6 @@ export default class User {
LocalStorage.setItem('project', project);
}
public static getAllSsoTokens(): Dictionary<string> {
const localStorageItems: Dictionary<string> =
LocalStorage.getAllItems();
const result: Dictionary<string> = {};
let numberOfTokens: number = 1;
for (const key in localStorageItems) {
if (!localStorageItems[key]) {
continue;
}
if (key.startsWith('sso_')) {
result['sso-' + numberOfTokens] = localStorageItems[
key
] as string;
numberOfTokens++;
}
}
return result;
}
public static getProject(): JSONObject {
return LocalStorage.getItem('project') as JSONObject;
}
@@ -146,10 +110,11 @@ export default class User {
}
public static isLoggedIn(): boolean {
return LocalStorage.getItem('access_token') ? true : false;
return !!this.getEmail();
}
public static logout(): void {
public static async logout(): Promise<void> {
// TODO: Clear all cookies here.
LocalStorage.clear();
}

View File

@@ -49,7 +49,6 @@ const Init: FunctionComponent<ComponentProps> = (
return Navigation.navigate(RouteMap[PageMap.LOGOUT] as Route);
}
User.setSsoToken(decodedtoken.projectId, sso_token);
}
}, []);

View File

@@ -1,4 +1,4 @@
import React, { FunctionComponent, ReactElement, useEffect } from 'react';
import React, { FunctionComponent, ReactElement } from 'react';
import PageComponentProps from '../PageComponentProps';
import Page from 'CommonUI/src/Components/Page/Page';
import Route from 'Common/Types/API/Route';
@@ -9,13 +9,14 @@ import UserUtil from 'CommonUI/src/Utils/User';
import Navigation from 'CommonUI/src/Utils/Navigation';
import { ACCOUNTS_URL } from 'CommonUI/src/Config';
import UiAnalytics from 'CommonUI/src/Utils/Analytics';
import useAsyncEffect from 'use-async-effect';
const Logout: FunctionComponent<PageComponentProps> = (
_props: PageComponentProps
): ReactElement => {
useEffect(() => {
useAsyncEffect(async () => {
UiAnalytics.logout();
UserUtil.logout();
await UserUtil.logout();
Navigation.navigate(ACCOUNTS_URL);
}, []);

View File

@@ -35,6 +35,7 @@ import AccessTokenService from 'CommonServer/Services/AccessTokenService';
import Hostname from 'Common/Types/API/Hostname';
import Protocol from 'Common/Types/API/Protocol';
import DatabaseConfig from 'CommonServer/DatabaseConfig';
import CookieUtil from 'CommonServer/Utils/Cookie';
const router: ExpressRouter = Express.getRouter();
@@ -183,11 +184,15 @@ router.post(
OneUptimeDate.getSecondsInDays(new PositiveNumber(30))
);
return Response.sendEntityResponse(req, res, savedUser, User, {
miscData: {
token,
},
// Set a cookie with token.
CookieUtil.setCookie(res, 'user-token', token, {
maxAge: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30)
),
httpOnly: true,
});
return Response.sendEntityResponse(req, res, savedUser, User);
}
return Response.sendErrorResponse(
@@ -481,6 +486,24 @@ router.post(
}
);
router.post(
'/logout',
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction
): Promise<void> => {
try {
CookieUtil.removeCookie(res, 'user-token');
// remove all sso cookies as well.
return Response.sendEmptyResponse(req, res);
} catch (err) {
return next(err);
}
});
router.post(
'/login',
async (
@@ -553,16 +576,19 @@ router.post(
OneUptimeDate.getSecondsInDays(new PositiveNumber(30))
);
// Set a cookie with token.
CookieUtil.setCookie(res, 'user-token', token, {
maxAge: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30)
),
httpOnly: true,
});
return Response.sendEntityResponse(
req,
res,
alreadySavedUser,
User,
{
miscData: {
token,
},
}
User
);
}
}

View File

@@ -31,6 +31,7 @@ import Exception from 'Common/Types/Exception/Exception';
import Hostname from 'Common/Types/API/Hostname';
import Protocol from 'Common/Types/API/Protocol';
import DatabaseConfig from 'CommonServer/DatabaseConfig';
import CookieUtil from 'CommonServer/Utils/Cookie';
const router: ExpressRouter = Express.getRouter();
@@ -345,6 +346,13 @@ router.post(
const httpProtocol: Protocol =
await DatabaseConfig.getHttpProtocol();
CookieUtil.setCookie(res, 'sso-'+req.params['projectId'].toString(), token, {
maxAge: OneUptimeDate.getSecondsInDays(
new PositiveNumber(30)
),
httpOnly: true,
});
return Response.redirect(
req,
res,

View File

@@ -32,7 +32,6 @@ import Link from 'Common/Types/Link';
import JSONWebTokenData from 'Common/Types/JsonWebTokenData';
import JSONWebToken from 'CommonUI/src/Utils/JsonWebToken';
import Route from 'Common/Types/API/Route';
import User from '../../Utils/User';
import LoginUtil from '../../Utils/Login';
import StatusPageUtil from '../../Utils/StatusPage';
@@ -94,7 +93,6 @@ const DashboardMasterPage: FunctionComponent<ComponentProps> = (
}
LoginUtil.login({
token: sso_token,
user: { ...decodedtoken, _id: decodedtoken.userId },
});
@@ -105,8 +103,6 @@ const DashboardMasterPage: FunctionComponent<ComponentProps> = (
return Navigation.navigate(logoutRoute);
}
User.setAccessToken(decodedtoken.statusPageId!, sso_token);
if (Navigation.getQueryStringByName('redirectUrl')) {
Navigation.navigate(
new Route(Navigation.getQueryStringByName('redirectUrl')!)

View File

@@ -4,15 +4,6 @@ import ObjectID from 'Common/Types/ObjectID';
import Name from 'Common/Types/Name';
export default class User {
public static getAccessToken(statusPageId: ObjectID): string {
return LocalStorage.getItem(
statusPageId.toString() + 'access_token'
) as string;
}
public static setAccessToken(statusPageId: ObjectID, token: string): void {
LocalStorage.setItem(statusPageId.toString() + 'access_token', token);
}
public static setUserId(statusPageId: ObjectID, userId: ObjectID): void {
LocalStorage.setItem(

View File

@@ -127,10 +127,6 @@ INTERNAL_SMTP_DKIM_PUBLIC_KEY_AS_BASE64=
INTERNAL_SMTP_EMAIL=test@yourcompany.com
INTERNAL_SMTP_SENDING_DOMAIN=yourcompany.com
# Licensing Database
AIRTABLE_API_KEY=
AIRTABLE_BASE_ID=
# Plans
# This is in the format of PlanName,PlanIdFromBillingProvider,MonthlySubscriptionPlanAmountInUSD,YearlySubscriptionPlanAmountInUSD,Order,TrialPeriodInDays
# Enterprise plan will have -1 which means custom pricing.