diff --git a/backend/src/app.ts b/backend/src/app.ts index ca6428447..461ede0c5 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -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)=>{ diff --git a/backend/src/controllers/v2/apiKeyDataController.ts b/backend/src/controllers/v2/apiKeyDataController.ts index e09504d49..f3fc957f1 100644 --- a/backend/src/controllers/v2/apiKeyDataController.ts +++ b/backend/src/controllers/v2/apiKeyDataController.ts @@ -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' }); } diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index ad63d41b4..2b972c09d 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -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 diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 8253cb64e..5d95883a5 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -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 diff --git a/backend/src/routes/v2/apiKeyData.ts b/backend/src/routes/v2/apiKeyData.ts index c6a6a1761..9544f6b0a 100644 --- a/backend/src/routes/v2/apiKeyData.ts +++ b/backend/src/routes/v2/apiKeyData.ts @@ -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; \ No newline at end of file diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index 52bc8bb8f..de3feb8bc 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -71,5 +71,4 @@ router.get( workspaceController.getWorkspaceServiceTokenData ); - export default router; diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 156800360..7b98d924f 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -16,6 +16,7 @@ declare global { serviceToken: any; accessToken: any; serviceTokenData: any; + apiKeyData: any; query?: any; } } diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index f7310d0be..9c8ac852b 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -143,4 +143,14 @@ export const ServiceTokenDataNotFoundError = (error?: Partial[API KEY DATA ERRORS]<----- +export const APIKeyDataNotFoundError = (error?: Partial) => 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]<-----