Skip to content

Commit

Permalink
Refactor auth middleware to accept multiple auth modes
Browse files Browse the repository at this point in the history
  • Loading branch information
dangtony98 committed Jan 1, 2023
1 parent 7e4bf7f commit b8a6471
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 67 deletions.
10 changes: 8 additions & 2 deletions backend/src/controllers/v1/serviceTokenDataController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 });
Expand Down
2 changes: 1 addition & 1 deletion backend/src/controllers/v2/workspaceController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}

Expand Down
132 changes: 95 additions & 37 deletions backend/src/helpers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ({
authTokenValue
const validateAuthMode = ({
authTokenValue,
acceptedAuthModes
}: {
authTokenValue: string;
acceptedAuthModes: string[];
}) => {
let serviceTokenHash, decodedToken; // intermediate variables
let serviceTokenData, user; // payloads
let authMode;
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');
}

return serviceTokenData;
authMode = 'serviceToken';
break;
default:
// case: JWT token auth mode
decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, JWT_AUTH_SECRET)
);

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

if (!user)
throw new Error('Account not found error');

if (!user?.publicKey)
throw new Error('Unable to authenticate due to partially set up account');

return user;
authMode = 'jwt';
break;
}

if (!acceptedAuthModes.includes(authMode))
throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' });

} catch (err) {
throw new Error('Failed to attach auth payload');
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 user;
try {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, JWT_AUTH_SECRET)
);

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

if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });

if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });

} catch (err) {
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;
}

/**
Expand Down Expand Up @@ -154,7 +210,9 @@ const createToken = ({
};

export {
attachAuthPayload,
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
createToken,
issueTokens,
clearTokens
Expand Down
35 changes: 16 additions & 19 deletions backend/src/middleware/requireAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
// 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 request payload
const payload = await attachAuthPayload({
authTokenValue: AUTH_TOKEN_VALUE
});

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

Expand Down
8 changes: 4 additions & 4 deletions backend/src/models/serviceTokenData .ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/v1/serviceTokenData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { serviceTokenDataController } from '../../controllers/v1';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['st']
acceptedAuthModes: ['serviceToken']
}),
param('serviceTokenDataId').exists().trim(),
validateRequest,
Expand Down
6 changes: 3 additions & 3 deletions backend/src/routes/v2/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -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],
Expand Down
10 changes: 10 additions & 0 deletions backend/src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]<-----

0 comments on commit b8a6471

Please sign in to comment.