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

View File

@ -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 });

View File

@ -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 });
}

View File

@ -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.UserIDJwtPayload>(
jwt.verify(authTokenValue, JWT_AUTH_SECRET)
);
return serviceTokenData;
default:
// case: JWT token auth mode
decodedToken = <jwt.UserIDJwtPayload>(
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

View File

@ -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;
}

View File

@ -45,19 +45,19 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
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
}
},
{

View File

@ -18,7 +18,7 @@ import { serviceTokenDataController } from '../../controllers/v1';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['st']
acceptedAuthModes: ['serviceToken']
}),
param('serviceTokenDataId').exists().trim(),
validateRequest,

View File

@ -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],

View File

@ -113,4 +113,14 @@ export const AccountNotFoundError = (error?: Partial<RequestErrorContext>) => ne
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]<-----