Refactor auth middleware to accept multiple auth modes

This commit is contained in:
Tuan Dang
2023-01-01 09:24:20 +07:00
parent 7e4bf7f44b
commit b8a64714d2
8 changed files with 138 additions and 67 deletions
@@ -48,7 +48,8 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
const expiresAt = new Date(); const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
serviceTokenData = await new ServiceTokenData({ // create service token data
serviceTokenData = new ServiceTokenData({
name, name,
workspace: workspaceId, workspace: workspaceId,
environment, environment,
@@ -59,7 +60,12 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
encryptedKey, encryptedKey,
iv, iv,
tag tag
}).save(); })
await serviceTokenData.save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
@@ -139,7 +139,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
environment environment
}); });
if (channel !== 'cli') { // TODO: fix frontend to get rid of this reformat bs if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets }); secrets = reformatPullSecrets({ secrets });
} }
+87 -29
View File
@@ -12,38 +12,62 @@ import {
JWT_REFRESH_SECRET, JWT_REFRESH_SECRET,
SALT_ROUNDS SALT_ROUNDS
} from '../config'; } 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 {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
}: { }: {
authTokenValue: string; authTokenValue: string;
}) => { }) => {
let serviceTokenHash, decodedToken; // intermediate variables let user;
let serviceTokenData, user; // payloads
try { try {
switch (authTokenValue.split('.', 1)[0]) { const decodedToken = <jwt.UserIDJwtPayload>(
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');
}
return serviceTokenData;
default:
// case: JWT token auth mode
decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, JWT_AUTH_SECRET) jwt.verify(authTokenValue, JWT_AUTH_SECRET)
); );
@@ -51,17 +75,49 @@ const attachAuthPayload = async ({
_id: decodedToken.userId _id: decodedToken.userId
}).select('+publicKey'); }).select('+publicKey');
if (!user) if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
throw new Error('Account not found error');
if (!user?.publicKey) if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
throw new Error('Unable to authenticate due to partially set up account');
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate JWT token'
});
}
return user; 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) { } catch (err) {
throw new Error('Failed to attach auth payload'); throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
} }
return serviceTokenData;
} }
/** /**
@@ -154,7 +210,9 @@ const createToken = ({
}; };
export { export {
attachAuthPayload, validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
createToken, createToken,
issueTokens, issueTokens,
clearTokens clearTokens
+15 -18
View File
@@ -2,7 +2,9 @@ import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { User, ServiceTokenData } from '../models'; import { User, ServiceTokenData } from '../models';
import { import {
attachAuthPayload validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload
} from '../helpers/auth'; } from '../helpers/auth';
import { JWT_AUTH_SECRET } from '../config'; import { JWT_AUTH_SECRET } from '../config';
import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors';
@@ -37,30 +39,25 @@ const requireAuth = ({
if(AUTH_TOKEN_VALUE === null) if(AUTH_TOKEN_VALUE === null)
return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
// validate auth mode // validate auth token against
let authMode; const authMode = validateAuthMode({
switch (AUTH_TOKEN_VALUE.split('.', 1)[0]) { authTokenValue: AUTH_TOKEN_VALUE,
case 'st': acceptedAuthModes
authMode = 'st'; });
break;
default:
authMode = 'jwt';
break;
}
if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode'); if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode');
// attach auth request payload // attach auth payloads
const payload = await attachAuthPayload({ switch (authMode) {
case 'serviceToken':
req.serviceTokenData = await getAuthSTDPayload({
authTokenValue: AUTH_TOKEN_VALUE authTokenValue: AUTH_TOKEN_VALUE
}); });
switch (authMode) {
case 'st':
req.serviceTokenData = payload;
break; break;
default: default:
req.user = payload; req.user = await getAuthUserPayload({
authTokenValue: AUTH_TOKEN_VALUE
});
break; break;
} }
+4 -4
View File
@@ -45,19 +45,19 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
type: String, type: String,
unique: true, unique: true,
required: true, required: true,
select: true select: false
}, },
encryptedKey: { encryptedKey: {
type: String, type: String,
select: true select: false
}, },
iv: { iv: {
type: String, type: String,
select: true select: false
}, },
tag: { tag: {
type: String, type: String,
select: true select: false
} }
}, },
{ {
+1 -1
View File
@@ -18,7 +18,7 @@ import { serviceTokenDataController } from '../../controllers/v1';
router.get( router.get(
'/', '/',
requireAuth({ requireAuth({
acceptedAuthModes: ['st'] acceptedAuthModes: ['serviceToken']
}), }),
param('serviceTokenDataId').exists().trim(), param('serviceTokenDataId').exists().trim(),
validateRequest, validateRequest,
+3 -3
View File
@@ -11,7 +11,7 @@ import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
import { membershipController } from '../../controllers/v1'; import { membershipController } from '../../controllers/v1';
import { workspaceController } from '../../controllers/v2'; import { workspaceController } from '../../controllers/v2';
router.post( // unfinished router.post(
'/:workspaceId/secrets', '/:workspaceId/secrets',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ['jwt']
@@ -29,10 +29,10 @@ router.post( // unfinished
workspaceController.pushWorkspaceSecrets workspaceController.pushWorkspaceSecrets
); );
router.get( // unfinished, check that it works with st router.get(
'/:workspaceId/secrets', '/:workspaceId/secrets',
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'st'] acceptedAuthModes: ['jwt', 'serviceToken']
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
+10
View File
@@ -113,4 +113,14 @@ export const AccountNotFoundError = (error?: Partial<RequestErrorContext>) => ne
stack: error?.stack stack: error?.stack
}) })
//* ----->[SERVICE TOKEN DATA ERRORS]<-----
export const ServiceTokenDataNotFoundError = (error?: Partial<RequestErrorContext>) => 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]<----- //* ----->[MISC ERRORS]<-----