Merge remote-tracking branch 'origin' into cleaning

This commit is contained in:
Tuan Dang
2023-01-04 21:24:38 +07:00
13 changed files with 273 additions and 17 deletions
+2
View File
@@ -38,6 +38,7 @@ import {
secret as v2SecretRouter, secret as v2SecretRouter,
workspace as v2WorkspaceRouter, workspace as v2WorkspaceRouter,
serviceTokenData as v2ServiceTokenDataRouter, serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
} from './routes/v2'; } from './routes/v2';
import { getLogger } from './utils/logger'; import { getLogger } from './utils/logger';
@@ -94,6 +95,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); app.use('/api/v2/secret', v2SecretRouter);
app.use('/api/v2/service-token-data', v2ServiceTokenDataRouter); app.use('/api/v2/service-token-data', v2ServiceTokenDataRouter);
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
//* Handle unrouted requests and respond with proper error message as well as status code //* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next)=>{ app.use((req, res, next)=>{
@@ -0,0 +1,106 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
APIKeyData
} from '../../models';
import {
SALT_ROUNDS
} from '../../config';
/**
* Return API key data for user with id [req.user_id]
* @param req
* @param res
* @returns
*/
export const getAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData;
try {
apiKeyData = await APIKeyData.find({
user: req.user._id
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get API key data'
});
}
return res.status(200).send({
apiKeyData
});
}
/**
* Create new API key data for user with id [req.user._id]
* @param req
* @param res
*/
export const createAPIKeyData = async (req: Request, res: Response) => {
let apiKey, apiKeyData;
try {
const { name, expiresIn } = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS);
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
apiKeyData = await new APIKeyData({
name,
expiresAt,
user: req.user._id,
secretHash
}).save();
// return api key data without sensitive data
apiKeyData = await APIKeyData.findById(apiKeyData._id);
if (!apiKeyData) throw new Error('Failed to find API key data');
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
} catch (err) {
console.error(err);
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to API key data'
});
}
return res.status(200).send({
apiKey,
apiKeyData
});
}
/**
* Delete API key data with id [apiKeyDataId].
* @param req
* @param res
* @returns
*/
export const deleteAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData;
try {
const { apiKeyDataId } = req.params;
apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete API key data'
});
}
return res.status(200).send({
apiKeyData
});
}
+3 -1
View File
@@ -1,7 +1,9 @@
import * as workspaceController from './workspaceController'; import * as workspaceController from './workspaceController';
import * as serviceTokenDataController from './serviceTokenDataController'; import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController';
export { export {
workspaceController, workspaceController,
serviceTokenDataController serviceTokenDataController,
apiKeyDataController
} }
+60 -9
View File
@@ -3,22 +3,25 @@ import * as Sentry from '@sentry/node';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { import {
User, User,
ServiceTokenData ServiceTokenData,
APIKeyData
} from '../models'; } from '../models';
import { import {
JWT_AUTH_LIFETIME, JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET, JWT_AUTH_SECRET,
JWT_REFRESH_LIFETIME, JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET, JWT_REFRESH_SECRET
SALT_ROUNDS
} from '../config'; } from '../config';
import { import {
AccountNotFoundError, AccountNotFoundError,
ServiceTokenDataNotFoundError, ServiceTokenDataNotFoundError,
UnauthorizedRequestError, APIKeyDataNotFoundError,
BadRequestError UnauthorizedRequestError
} from '../utils/errors'; } from '../utils/errors';
// TODO 1: check if API key works
// TODO 2: optimize middleware
/** /**
* Validate that auth token value [authTokenValue] falls under one of * Validate that auth token value [authTokenValue] falls under one of
* accepted auth modes [acceptedAuthModes]. * accepted auth modes [acceptedAuthModes].
@@ -40,6 +43,9 @@ const validateAuthMode = ({
case 'st': case 'st':
authMode = 'serviceToken'; authMode = 'serviceToken';
break; break;
case 'ak':
authMode = 'apiKey';
break;
default: default:
authMode = 'jwt'; authMode = 'jwt';
break; break;
@@ -106,9 +112,11 @@ const getAuthSTDPayload = async ({
// TODO: optimize double query // TODO: optimize double query
serviceTokenData = await ServiceTokenData serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, 'secretHash expiresAt'); .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired // case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
@@ -116,8 +124,6 @@ const getAuthSTDPayload = async ({
}); });
} }
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash); const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({ if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token' message: 'Failed to authenticate service token'
@@ -136,6 +142,50 @@ const getAuthSTDPayload = async ({
return serviceTokenData; return serviceTokenData;
} }
/**
* Return API key data payload corresponding to API key [authTokenValue]
* @param {Object} obj
* @param {String} obj.authTokenValue - API key value
* @returns {APIKeyData} apiKeyData - API key data
*/
const getAuthAPIKeyPayload = async ({
authTokenValue
}: {
authTokenValue: string;
}) => {
let user;
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate('user', '+publicKey');
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired API key'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
user = apiKeyData.user;
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
}
return user;
}
/** /**
* Return newly issued (JWT) auth and refresh tokens to user with id [userId] * Return newly issued (JWT) auth and refresh tokens to user with id [userId]
* @param {Object} obj * @param {Object} obj
@@ -229,6 +279,7 @@ export {
validateAuthMode, validateAuthMode,
getAuthUserPayload, getAuthUserPayload,
getAuthSTDPayload, getAuthSTDPayload,
getAuthAPIKeyPayload,
createToken, createToken,
issueTokens, issueTokens,
clearTokens clearTokens
+7 -1
View File
@@ -4,7 +4,8 @@ import { User, ServiceTokenData } from '../models';
import { import {
validateAuthMode, validateAuthMode,
getAuthUserPayload, getAuthUserPayload,
getAuthSTDPayload getAuthSTDPayload,
getAuthAPIKeyPayload
} from '../helpers/auth'; } from '../helpers/auth';
import { BadRequestError } from '../utils/errors'; import { BadRequestError } from '../utils/errors';
@@ -53,6 +54,11 @@ const requireAuth = ({
authTokenValue: AUTH_TOKEN_VALUE authTokenValue: AUTH_TOKEN_VALUE
}); });
break; break;
case 'apiKey':
req.user = await getAuthAPIKeyPayload({
authTokenValue: AUTH_TOKEN_VALUE
});
break;
default: default:
req.user = await getAuthUserPayload({ req.user = await getAuthUserPayload({
authTokenValue: AUTH_TOKEN_VALUE authTokenValue: AUTH_TOKEN_VALUE
+37
View File
@@ -0,0 +1,37 @@
import { Schema, model, Types } from 'mongoose';
export interface IAPIKeyData {
name: string;
user: Types.ObjectId;
expiresAt: Date;
secretHash: string;
}
const apiKeyDataSchema = new Schema<IAPIKeyData>(
{
name: {
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
expiresAt: {
type: Date
},
secretHash: {
type: String,
required: true,
select: false
}
},
{
timestamps: true
}
);
const APIKeyData = model<IAPIKeyData>('APIKeyData', apiKeyDataSchema);
export default APIKeyData;
+5 -2
View File
@@ -14,7 +14,8 @@ import Token, { IToken } from './token';
import User, { IUser } from './user'; import User, { IUser } from './user';
import UserAction, { IUserAction } from './userAction'; import UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace'; import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData '; import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
import APIKeyData, { IAPIKeyData } from './apiKeyData';
export { export {
BackupPrivateKey, BackupPrivateKey,
@@ -50,5 +51,7 @@ export {
Workspace, Workspace,
IWorkspace, IWorkspace,
ServiceTokenData, ServiceTokenData,
IServiceTokenData IServiceTokenData,
APIKeyData,
IAPIKeyData
}; };
@@ -1,5 +1,4 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
export interface IServiceTokenData { export interface IServiceTokenData {
name: string; name: string;
@@ -38,7 +37,6 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
}, },
secretHash: { secretHash: {
type: String, type: String,
unique: true,
required: true, required: true,
select: false select: false
}, },
+39
View File
@@ -0,0 +1,39 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
validateRequest
} from '../../middleware';
import { param, body } from 'express-validator';
import { apiKeyDataController } from '../../controllers/v2';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
apiKeyDataController.getAPIKeyData
);
router.post(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
body('name').exists().trim(),
body('expiresIn'), // measured in ms
validateRequest,
apiKeyDataController.createAPIKeyData
);
router.delete(
'/:apiKeyDataId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('apiKeyDataId').exists().trim(),
validateRequest,
apiKeyDataController.deleteAPIKeyData
);
export default router;
+3 -1
View File
@@ -1,9 +1,11 @@
import secret from './secret'; import secret from './secret';
import workspace from './workspace'; import workspace from './workspace';
import serviceTokenData from './serviceTokenData'; import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData';
export { export {
secret, secret,
workspace, workspace,
serviceTokenData serviceTokenData,
apiKeyData
} }
-1
View File
@@ -67,5 +67,4 @@ router.get(
workspaceController.getWorkspaceServiceTokenData workspaceController.getWorkspaceServiceTokenData
); );
export default router; export default router;
+1
View File
@@ -16,6 +16,7 @@ declare global {
serviceToken: any; serviceToken: any;
accessToken: any; accessToken: any;
serviceTokenData: any; serviceTokenData: any;
apiKeyData: any;
query?: any; query?: any;
} }
} }
+10
View File
@@ -143,4 +143,14 @@ export const ServiceTokenDataNotFoundError = (error?: Partial<RequestErrorContex
stack: error?.stack stack: error?.stack
}) })
//* ----->[API KEY DATA ERRORS]<-----
export const APIKeyDataNotFoundError = (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]<-----