Complete v1 support for API key auth mode

This commit is contained in:
Tuan Dang
2023-01-04 20:27:16 +07:00
parent 8c7c41e091
commit c7fb9209c4
8 changed files with 119 additions and 16 deletions

View File

@ -38,6 +38,7 @@ import {
secret as v2SecretRouter,
workspace as v2WorkspaceRouter,
serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter,
} from './routes/v2';
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/secret', v2SecretRouter);
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
app.use((req, res, next)=>{

View File

@ -10,11 +10,36 @@ import {
} from '../../config';
/**
* Create new API key for user with id [req.user._id]
* 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 createAPIKey = async (req: Request, res: Response) => {
export const createAPIKeyData = async (req: Request, res: Response) => {
let apiKey, apiKeyData;
try {
const { name, expiresIn } = req.body;
@ -30,7 +55,7 @@ export const createAPIKey = async (req: Request, res: Response) => {
expiresAt,
user: req.user._id,
secretHash
});
}).save();
// return api key data without sensitive data
apiKeyData = await APIKeyData.findById(apiKeyData._id);
@ -40,10 +65,11 @@ export const createAPIKey = async (req: Request, res: Response) => {
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 create service token data'
message: 'Failed to API key data'
});
}

View File

@ -3,22 +3,25 @@ import * as Sentry from '@sentry/node';
import bcrypt from 'bcrypt';
import {
User,
ServiceTokenData
ServiceTokenData,
APIKeyData
} from '../models';
import {
JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET,
JWT_REFRESH_LIFETIME,
JWT_REFRESH_SECRET,
SALT_ROUNDS
JWT_REFRESH_SECRET
} from '../config';
import {
AccountNotFoundError,
ServiceTokenDataNotFoundError,
UnauthorizedRequestError,
BadRequestError
APIKeyDataNotFoundError,
UnauthorizedRequestError
} from '../utils/errors';
// TODO 1: check if API key works
// TODO 2: optimize middleware
/**
* Validate that auth token value [authTokenValue] falls under one of
* accepted auth modes [acceptedAuthModes].
@ -40,6 +43,9 @@ const validateAuthMode = ({
case 'st':
authMode = 'serviceToken';
break;
case 'ak':
authMode = 'apiKey';
break;
default:
authMode = 'jwt';
break;
@ -106,9 +112,11 @@ const getAuthSTDPayload = async ({
// TODO: optimize double query
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
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
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);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
@ -136,6 +142,50 @@ const getAuthSTDPayload = async ({
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]
* @param {Object} obj
@ -229,6 +279,7 @@ export {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthAPIKeyPayload,
createToken,
issueTokens,
clearTokens

View File

@ -4,7 +4,8 @@ import { User, ServiceTokenData } from '../models';
import {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload
getAuthSTDPayload,
getAuthAPIKeyPayload
} from '../helpers/auth';
import { BadRequestError } from '../utils/errors';
@ -53,6 +54,11 @@ const requireAuth = ({
authTokenValue: AUTH_TOKEN_VALUE
});
break;
case 'apiKey':
req.user = await getAuthAPIKeyPayload({
authTokenValue: AUTH_TOKEN_VALUE
});
break;
default:
req.user = await getAuthUserPayload({
authTokenValue: AUTH_TOKEN_VALUE

View File

@ -7,6 +7,14 @@ import {
import { body } from 'express-validator';
import { apiKeyDataController } from '../../controllers/v2';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt']
}),
apiKeyDataController.getAPIKeyData
);
router.post(
'/',
requireAuth({
@ -15,7 +23,7 @@ router.post(
body('name').exists().trim(),
body('expiresIn'), // measured in ms
validateRequest,
apiKeyDataController.createAPIKey
apiKeyDataController.createAPIKeyData
);
export default router;

View File

@ -71,5 +71,4 @@ router.get(
workspaceController.getWorkspaceServiceTokenData
);
export default router;

View File

@ -16,6 +16,7 @@ declare global {
serviceToken: any;
accessToken: any;
serviceTokenData: any;
apiKeyData: any;
query?: any;
}
}

View File

@ -143,4 +143,14 @@ export const ServiceTokenDataNotFoundError = (error?: Partial<RequestErrorContex
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]<-----