From b8a64714d25970acde15bafe833f6185bbed6c02 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 1 Jan 2023 09:24:20 +0700 Subject: [PATCH] Refactor auth middleware to accept multiple auth modes --- .../v1/serviceTokenDataController.ts | 10 +- .../src/controllers/v2/workspaceController.ts | 2 +- backend/src/helpers/auth.ts | 130 +++++++++++++----- backend/src/middleware/requireAuth.ts | 37 +++-- backend/src/models/serviceTokenData .ts | 8 +- backend/src/routes/v1/serviceTokenData.ts | 2 +- backend/src/routes/v2/workspace.ts | 6 +- backend/src/utils/errors.ts | 10 ++ 8 files changed, 138 insertions(+), 67 deletions(-) diff --git a/backend/src/controllers/v1/serviceTokenDataController.ts b/backend/src/controllers/v1/serviceTokenDataController.ts index 83701abcd..3a4b94a83 100644 --- a/backend/src/controllers/v1/serviceTokenDataController.ts +++ b/backend/src/controllers/v1/serviceTokenDataController.ts @@ -48,7 +48,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => { const expiresAt = new Date(); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - serviceTokenData = await new ServiceTokenData({ + // create service token data + serviceTokenData = new ServiceTokenData({ name, workspace: workspaceId, environment, @@ -59,7 +60,12 @@ export const createServiceTokenData = async (req: Request, res: Response) => { encryptedKey, iv, tag - }).save(); + }) + + await serviceTokenData.save(); + + // return service token data without sensitive data + serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index fa0ffa5af..1b10ebccd 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -139,7 +139,7 @@ export const pullSecrets = async (req: Request, res: Response) => { environment }); - if (channel !== 'cli') { // TODO: fix frontend to get rid of this reformat bs + if (channel !== 'cli') { secrets = reformatPullSecrets({ secrets }); } diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 9845e9c74..c3b212916 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -12,56 +12,112 @@ import { JWT_REFRESH_SECRET, SALT_ROUNDS } from '../config'; +import { + AccountNotFoundError, + ServiceTokenDataNotFoundError, + UnauthorizedRequestError +} from '../utils/errors'; /** - * Attach auth payload + * Validate that auth token value [authTokenValue] falls under one of + * accepted auth modes [acceptedAuthModes]. * @param {Object} obj - * @param {String} obj.authTokenValue + * @param {String} obj.authTokenValue - auth token value (e.g. JWT or service token value) + * @param {String[]} obj.acceptedAuthModes - accepted auth modes (e.g. jwt, serviceToken) + * @returns {String} authMode - auth mode */ -const attachAuthPayload = async ({ +const validateAuthMode = ({ + authTokenValue, + acceptedAuthModes +}: { + authTokenValue: string; + acceptedAuthModes: string[]; +}) => { + let authMode; + try { + switch (authTokenValue.split('.', 1)[0]) { + case 'st': + authMode = 'serviceToken'; + break; + default: + authMode = 'jwt'; + break; + } + + if (!acceptedAuthModes.includes(authMode)) + throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' }); + + } catch (err) { + throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' }); + } + + return authMode; +} + +/** + * Return user payload corresponding to JWT token [authTokenValue] + * @param {Object} obj + * @param {String} obj.authTokenValue - JWT token value + * @returns {User} user - user corresponding to JWT token + */ +const getAuthUserPayload = async ({ authTokenValue }: { authTokenValue: string; }) => { - let serviceTokenHash, decodedToken; // intermediate variables - let serviceTokenData, user; // payloads + let user; try { - switch (authTokenValue.split('.', 1)[0]) { - case 'st': - // case: service token auth mode - serviceTokenHash = await bcrypt.hash(authTokenValue, SALT_ROUNDS); - serviceTokenData = await ServiceTokenData - .findOne({ - serviceTokenHash - }) - .select('+encryptedKey +iv +tag'); - - if (!serviceTokenData) { - throw new Error('Account not found error'); - } + const decodedToken = ( + jwt.verify(authTokenValue, JWT_AUTH_SECRET) + ); - return serviceTokenData; - default: - // case: JWT token auth mode - decodedToken = ( - jwt.verify(authTokenValue, JWT_AUTH_SECRET) - ); - - user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); - if (!user) - throw new Error('Account not found error'); + if (!user) throw AccountNotFoundError({ message: 'Failed to find User' }); - if (!user?.publicKey) - throw new Error('Unable to authenticate due to partially set up account'); + if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' }); - return user; - } } catch (err) { - throw new Error('Failed to attach auth payload'); + throw UnauthorizedRequestError({ + message: 'Failed to authenticate JWT token' + }); } + + return user; +} + +/** + * Return service token data payload corresponding to service token [authTokenValue] + * @param {Object} obj + * @param {String} obj.authTokenValue - service token value + * @returns {ServiceTokenData} serviceTokenData - service token data + */ +const getAuthSTDPayload = async ({ + authTokenValue +}: { + authTokenValue: string; +}) => { + let serviceTokenData; + try { + const serviceTokenHash = await bcrypt.hash(authTokenValue, SALT_ROUNDS); + + serviceTokenData = await ServiceTokenData + .findOne({ + serviceTokenHash + }) + .select('+encryptedKey +iv +tag'); + + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + + } catch (err) { + throw UnauthorizedRequestError({ + message: 'Failed to authenticate service token' + }); + } + + return serviceTokenData; } /** @@ -154,7 +210,9 @@ const createToken = ({ }; export { - attachAuthPayload, + validateAuthMode, + getAuthUserPayload, + getAuthSTDPayload, createToken, issueTokens, clearTokens diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 52f69d260..b91d2cd09 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -2,7 +2,9 @@ import jwt from 'jsonwebtoken'; import { Request, Response, NextFunction } from 'express'; import { User, ServiceTokenData } from '../models'; import { - attachAuthPayload + validateAuthMode, + getAuthUserPayload, + getAuthSTDPayload } from '../helpers/auth'; import { JWT_AUTH_SECRET } from '../config'; import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; @@ -37,30 +39,25 @@ const requireAuth = ({ if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) - // validate auth mode - let authMode; - switch (AUTH_TOKEN_VALUE.split('.', 1)[0]) { - case 'st': - authMode = 'st'; - break; - default: - authMode = 'jwt'; - break; - } - - if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode'); - - // attach auth request payload - const payload = await attachAuthPayload({ - authTokenValue: AUTH_TOKEN_VALUE + // validate auth token against + const authMode = validateAuthMode({ + authTokenValue: AUTH_TOKEN_VALUE, + acceptedAuthModes }); + if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode'); + + // attach auth payloads switch (authMode) { - case 'st': - req.serviceTokenData = payload; + case 'serviceToken': + req.serviceTokenData = await getAuthSTDPayload({ + authTokenValue: AUTH_TOKEN_VALUE + }); break; default: - req.user = payload; + req.user = await getAuthUserPayload({ + authTokenValue: AUTH_TOKEN_VALUE + }); break; } diff --git a/backend/src/models/serviceTokenData .ts b/backend/src/models/serviceTokenData .ts index 32520dda9..1faecfaff 100644 --- a/backend/src/models/serviceTokenData .ts +++ b/backend/src/models/serviceTokenData .ts @@ -45,19 +45,19 @@ const serviceTokenDataSchema = new Schema( type: String, unique: true, required: true, - select: true + select: false }, encryptedKey: { type: String, - select: true + select: false }, iv: { type: String, - select: true + select: false }, tag: { type: String, - select: true + select: false } }, { diff --git a/backend/src/routes/v1/serviceTokenData.ts b/backend/src/routes/v1/serviceTokenData.ts index fa9f4fdbd..4223172d3 100644 --- a/backend/src/routes/v1/serviceTokenData.ts +++ b/backend/src/routes/v1/serviceTokenData.ts @@ -18,7 +18,7 @@ import { serviceTokenDataController } from '../../controllers/v1'; router.get( '/', requireAuth({ - acceptedAuthModes: ['st'] + acceptedAuthModes: ['serviceToken'] }), param('serviceTokenDataId').exists().trim(), validateRequest, diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index 6fc81bbc5..df26749f3 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -11,7 +11,7 @@ import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; import { membershipController } from '../../controllers/v1'; import { workspaceController } from '../../controllers/v2'; -router.post( // unfinished +router.post( '/:workspaceId/secrets', requireAuth({ acceptedAuthModes: ['jwt'] @@ -29,10 +29,10 @@ router.post( // unfinished workspaceController.pushWorkspaceSecrets ); -router.get( // unfinished, check that it works with st +router.get( '/:workspaceId/secrets', requireAuth({ - acceptedAuthModes: ['jwt', 'st'] + acceptedAuthModes: ['jwt', 'serviceToken'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index 40c467131..2b2ce59a6 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -113,4 +113,14 @@ export const AccountNotFoundError = (error?: Partial) => ne stack: error?.stack }) +//* ----->[SERVICE TOKEN DATA ERRORS]<----- +export const ServiceTokenDataNotFoundError = (error?: Partial) => new RequestError({ + logLevel: error?.logLevel ?? LogLevel.ERROR, + statusCode: error?.statusCode ?? 404, + type: error?.type ?? 'service_token_data_not_found_error', + message: error?.message ?? 'The requested service token data was not found', + context: error?.context, + stack: error?.stack +}) + //* ----->[MISC ERRORS]<-----