Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into secret-versioning
Browse files Browse the repository at this point in the history
  • Loading branch information
dangtony98 committed Dec 25, 2022
2 parents 9c76985 + 2513250 commit 890aff8
Show file tree
Hide file tree
Showing 35 changed files with 1,640 additions and 284 deletions.
2 changes: 2 additions & 0 deletions backend/environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ declare global {
JWT_SIGNUP_SECRET: string;
MONGO_URL: string;
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
VERBOSE_ERROR_OUTPUT: string;
LOKI_HOST: string;
CLIENT_ID_HEROKU: string;
CLIENT_ID_VERCEL: string;
CLIENT_ID_NETLIFY: string;
Expand Down
682 changes: 668 additions & 14 deletions backend/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"stripe": "^10.7.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3"
"typescript": "^4.9.3",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
},
"name": "infisical-api",
"version": "1.0.0",
Expand Down
21 changes: 19 additions & 2 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-console */

import { patchRouterParam } from './utils/patchAsyncRoutes';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
Expand Down Expand Up @@ -29,6 +29,12 @@ import {
integration as integrationRouter,
integrationAuth as integrationAuthRouter
} from './routes';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler';

//* Patch Async route params to handle Promise Rejections
patchRouterParam()

export const app = express();

Expand Down Expand Up @@ -69,6 +75,17 @@ app.use('/api/v1/stripe', stripeRouter);
app.use('/api/v1/integration', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter);


//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next)=>{
if(res.headersSent) return next();
next(RouteNotFoundError({message: `The requested source '(${req.method})${req.url}' was not found`}))
})

//* Error Handling Middleware (must be after all routing logic)
app.use(requestErrorHandler)


export const server = app.listen(PORT, () => {
console.log(`Listening on PORT ${[PORT]}`);
getLogger("backend-main").info(`Server started listening at port ${PORT}`)
});
4 changes: 4 additions & 0 deletions backend/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m';
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
const MONGO_URL = process.env.MONGO_URL!;
const NODE_ENV = process.env.NODE_ENV! || 'production';
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
const LOKI_HOST = process.env.LOKI_HOST || undefined;
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
Expand Down Expand Up @@ -53,6 +55,8 @@ export {
JWT_SIGNUP_SECRET,
MONGO_URL,
NODE_ENV,
VERBOSE_ERROR_OUTPUT,
LOKI_HOST,
CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY,
Expand Down
22 changes: 15 additions & 7 deletions backend/src/helpers/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import * as Sentry from '@sentry/node';
import {
Bot,
Integration,
IIntegration,
IntegrationAuth,
IIntegrationAuth
} from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService, IntegrationService } from '../services';
import { BotService } from '../services';
import {
ENV_DEV,
EVENT_PUSH_SECRETS,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables';
import { UnauthorizedRequestError } from '../utils/errors';
import RequestError from '../utils/requestError';

interface Update {
workspace: string;
Expand Down Expand Up @@ -176,12 +176,13 @@ const syncIntegrationsHelper = async ({
*/
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let refreshToken;

try {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');

if (!integrationAuth) throw new Error('Failed to find integration auth');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});

refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
Expand All @@ -193,7 +194,10 @@ const syncIntegrationsHelper = async ({
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration refresh token');
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration refresh token');
}

return refreshToken;
Expand All @@ -209,12 +213,13 @@ const syncIntegrationsHelper = async ({
*/
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let accessToken;

try {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext');

if (!integrationAuth) throw new Error('Failed to find integration auth');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});

accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
Expand All @@ -240,7 +245,10 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration access token');
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration access token');
}

return accessToken;
Expand Down
1 change: 1 addition & 0 deletions backend/src/helpers/membership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const validateMembership = async ({
}) => {

let membership;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
try {
membership = await Membership.findOne({
user: userId,
Expand Down
1 change: 1 addition & 0 deletions backend/src/integrations/refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const exchangeRefreshHeroku = async ({
refreshToken: string;
}) => {
let accessToken;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
try {
const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL,
Expand Down
29 changes: 29 additions & 0 deletions backend/src/middleware/requestErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ErrorRequestHandler } from "express";

import * as Sentry from '@sentry/node';
import { InternalServerError } from "../utils/errors";
import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError";


export const requestErrorHandler: ErrorRequestHandler = (error: RequestError|Error, req, res, next) => {
if(res.headersSent) return next();
//TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError
if(!(error instanceof RequestError)){
error = InternalServerError({context: {exception: error.message}, stack: error.stack})
getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
}

//* Set Sentry user identification if req.user is populated
if(req.user !== undefined && req.user !== null){
Sentry.setUser({ email: req.user.email })
}
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
if([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((<RequestError>error).level)){
Sentry.captureException(error)
}

res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
next()
}
39 changes: 16 additions & 23 deletions backend/src/middleware/requireAuth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../models';
import { JWT_AUTH_SECRET } from '../config';
import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors';

declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
Expand All @@ -20,32 +20,25 @@ declare module 'jsonwebtoken' {
*/
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
// JWT authentication middleware
try {
if (!req.headers?.authorization)
throw new Error('Failed to locate authorization header');
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))

const token = req.headers.authorization.split(' ')[1];
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(token, JWT_AUTH_SECRET)
);
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET)
);

const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');

if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account');
if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'}))
if (!user?.publicKey)
return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'}))

req.user = user;
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed to authenticate user. Try logging in'
});
}
req.user = user;
return next();
};

export default requireAuth;
40 changes: 16 additions & 24 deletions backend/src/middleware/requireBotAuth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Sentry from '@sentry/node';
import { Request, Response, NextFunction } from 'express';
import { Bot } from '../models';
import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError } from '../utils/errors';

type req = 'params' | 'body' | 'query';

Expand All @@ -15,30 +15,22 @@ const requireBotAuth = ({
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const bot = await Bot.findOne({ _id: req[location].botId });

if (!bot) {
throw new Error('Failed to find bot');
}

await validateMembership({
userId: req.user._id.toString(),
workspaceId: bot.workspace.toString(),
acceptedRoles,
acceptedStatuses
});

req.bot = bot;

next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed bot authorization'
});
const bot = await Bot.findOne({ _id: req[location].botId });

if (!bot) {
return next(AccountNotFoundError({message: 'Failed to locate Bot account'}))
}

await validateMembership({
userId: req.user._id.toString(),
workspaceId: bot.workspace.toString(),
acceptedRoles,
acceptedStatuses
});

req.bot = bot;

next();
}
}

Expand Down
Loading

0 comments on commit 890aff8

Please sign in to comment.