mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 16:27:46 +00:00
Complete v1 support for API key auth mode
This commit is contained in:
@@ -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)=>{
|
||||||
|
|||||||
@@ -10,11 +10,36 @@ import {
|
|||||||
} from '../../config';
|
} 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 req
|
||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const createAPIKey = async (req: Request, res: Response) => {
|
export const createAPIKeyData = async (req: Request, res: Response) => {
|
||||||
let apiKey, apiKeyData;
|
let apiKey, apiKeyData;
|
||||||
try {
|
try {
|
||||||
const { name, expiresIn } = req.body;
|
const { name, expiresIn } = req.body;
|
||||||
@@ -30,7 +55,7 @@ export const createAPIKey = async (req: Request, res: Response) => {
|
|||||||
expiresAt,
|
expiresAt,
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
secretHash
|
secretHash
|
||||||
});
|
}).save();
|
||||||
|
|
||||||
// return api key data without sensitive data
|
// return api key data without sensitive data
|
||||||
apiKeyData = await APIKeyData.findById(apiKeyData._id);
|
apiKeyData = await APIKeyData.findById(apiKeyData._id);
|
||||||
@@ -40,10 +65,11 @@ export const createAPIKey = async (req: Request, res: Response) => {
|
|||||||
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to create service token data'
|
message: 'Failed to API key data'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import {
|
|||||||
import { body } from 'express-validator';
|
import { body } from 'express-validator';
|
||||||
import { apiKeyDataController } from '../../controllers/v2';
|
import { apiKeyDataController } from '../../controllers/v2';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
apiKeyDataController.getAPIKeyData
|
||||||
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/',
|
'/',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
@@ -15,7 +23,7 @@ router.post(
|
|||||||
body('name').exists().trim(),
|
body('name').exists().trim(),
|
||||||
body('expiresIn'), // measured in ms
|
body('expiresIn'), // measured in ms
|
||||||
validateRequest,
|
validateRequest,
|
||||||
apiKeyDataController.createAPIKey
|
apiKeyDataController.createAPIKeyData
|
||||||
);
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -71,5 +71,4 @@ router.get(
|
|||||||
workspaceController.getWorkspaceServiceTokenData
|
workspaceController.getWorkspaceServiceTokenData
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
Vendored
+1
@@ -16,6 +16,7 @@ declare global {
|
|||||||
serviceToken: any;
|
serviceToken: any;
|
||||||
accessToken: any;
|
accessToken: any;
|
||||||
serviceTokenData: any;
|
serviceTokenData: any;
|
||||||
|
apiKeyData: any;
|
||||||
query?: any;
|
query?: any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]<-----
|
||||||
|
|||||||
Reference in New Issue
Block a user