Skip to content

Commit

Permalink
Merge pull request #223 from Infisical/api-keys
Browse files Browse the repository at this point in the history
API Keys V1
  • Loading branch information
dangtony98 authored Jan 15, 2023
2 parents 564367d + 061a9c8 commit f19db53
Show file tree
Hide file tree
Showing 17 changed files with 571 additions and 144 deletions.
2 changes: 1 addition & 1 deletion backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
app.use('/api/v2/api-key', v2APIKeyDataRouter);

// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
Expand Down
4 changes: 2 additions & 2 deletions backend/src/ee/routes/v1/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ADMIN, MEMBER } from '../../../variables';
router.get(
'/:secretId/secret-versions',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
Expand All @@ -27,7 +27,7 @@ router.get(
router.post(
'/:secretId/secret-versions/rollback',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER]
Expand Down
6 changes: 3 additions & 3 deletions backend/src/ee/routes/v1/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { workspaceController } from '../../controllers/v1';
router.get(
'/:workspaceId/secret-snapshots',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
Expand Down Expand Up @@ -40,7 +40,7 @@ router.get(
router.post(
'/:workspaceId/secret-snapshots/rollback',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
Expand All @@ -54,7 +54,7 @@ router.post(
router.get(
'/:workspaceId/logs',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
Expand Down
73 changes: 45 additions & 28 deletions backend/src/helpers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,66 @@ import {
AccountNotFoundError,
ServiceTokenDataNotFoundError,
APIKeyDataNotFoundError,
UnauthorizedRequestError
UnauthorizedRequestError,
BadRequestError
} 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].
*
* @param {Object} obj
* @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
* @param {Object} obj.headers - HTTP request headers object
*/
const validateAuthMode = ({
authTokenValue,
headers,
acceptedAuthModes
}: {
authTokenValue: string;
acceptedAuthModes: string[];
headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: string[]
}) => {
let authMode;
try {
switch (authTokenValue.split('.', 1)[0]) {
// TODO: refactor middleware
const apiKey = headers['x-api-key'];
const authHeader = headers['authorization'];

let authTokenType, authTokenValue;
if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
}

if (typeof apiKey === 'string') {
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
authTokenType = 'apiKey';
authTokenValue = apiKey;
}

if (typeof authHeader === 'string') {
// case: treat request authentication type as via Authorization header (i.e. either JWT or service token)
const [tokenType, tokenValue] = <[string, string]>authHeader.split(' ', 2) ?? [null, null]
if (tokenType === null)
throw BadRequestError({ message: `Missing Authorization Header in the request header.` });
if (tokenType.toLowerCase() !== 'bearer')
throw BadRequestError({ message: `The provided authentication type '${tokenType}' is not supported.` });
if (tokenValue === null)
throw BadRequestError({ message: 'Missing Authorization Body in the request header.' });

switch (tokenValue.split('.', 1)[0]) {
case 'st':
authMode = 'serviceToken';
break;
case 'ak':
authMode = 'apiKey';
authTokenType = 'serviceToken';
break;
default:
authMode = 'jwt';
break;
authTokenType = 'jwt';
}

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

} catch (err) {
throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' });
authTokenValue = tokenValue;
}

return authMode;
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });

if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });

return ({
authTokenType,
authTokenValue
});
}

/**
Expand Down
26 changes: 8 additions & 18 deletions backend/src/middleware/requireAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
getAuthSTDPayload,
getAuthAPIKeyPayload
} from '../helpers/auth';
import { BadRequestError } from '../utils/errors';

declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
Expand All @@ -31,37 +30,28 @@ const requireAuth = ({
acceptedAuthModes: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if (AUTH_TOKEN_TYPE === null)
return next(BadRequestError({ message: `Missing Authorization Header in the request header.` }))
if (AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer')
return next(BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` }))
if (AUTH_TOKEN_VALUE === null)
return next(BadRequestError({ message: 'Missing Authorization Body in the request header' }))

// validate auth token against
const authMode = validateAuthMode({
authTokenValue: AUTH_TOKEN_VALUE,
// validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue]
const { authTokenType, authTokenValue } = validateAuthMode({
headers: req.headers,
acceptedAuthModes
});

if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode');

// attach auth payloads
switch (authMode) {
switch (authTokenType) {
case 'serviceToken':
req.serviceTokenData = await getAuthSTDPayload({
authTokenValue: AUTH_TOKEN_VALUE
authTokenValue
});
break;
case 'apiKey':
req.user = await getAuthAPIKeyPayload({
authTokenValue: AUTH_TOKEN_VALUE
authTokenValue
});
break;
default:
req.user = await getAuthUserPayload({
authTokenValue: AUTH_TOKEN_VALUE
authTokenValue
});
break;
}
Expand Down
8 changes: 4 additions & 4 deletions backend/src/routes/v2/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ router.post(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
Expand All @@ -76,7 +76,7 @@ router.get(
query('environment').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken']
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
Expand Down Expand Up @@ -115,7 +115,7 @@ router.patch(
}),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
Expand Down Expand Up @@ -143,7 +143,7 @@ router.delete(
.isEmpty(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER]
Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/v2/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { usersController } from '../../controllers/v2';
router.get(
'/me',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
usersController.getMe
);
Expand Down
8 changes: 4 additions & 4 deletions backend/src/routes/v2/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ router.get(
router.get(
'/:workspaceId/encrypted-key',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER]
Expand Down Expand Up @@ -75,7 +75,7 @@ router.get( // new - TODO: rewire dashboard to this route
param('workspaceId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
Expand All @@ -89,7 +89,7 @@ router.delete( // TODO - rewire dashboard to this route
param('membershipId').exists().trim(),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
Expand All @@ -107,7 +107,7 @@ router.patch( // TODO - rewire dashboard to this route
body('role').exists().isString().trim().isIn([ADMIN, MEMBER]),
validateRequest,
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
Expand Down
Loading

1 comment on commit f19db53

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for backend

St.
Category Percentage Covered / Total
🟡 Statements 75% 60/80
🔴 Branches 0% 0/5
🔴 Functions 50% 1/2
🟡 Lines 75.95% 60/79

Test suite run success

1 tests passing in 1 suite.

Report generated by 🧪jest coverage report action from f19db53

Please sign in to comment.