From 74b76eda7e9b5831647e092b12fb50a0e06fd649 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 15 Jan 2023 14:25:23 +0700 Subject: [PATCH] Complete v1 API Key --- README.md | 7 +- backend/src/app.ts | 2 +- backend/src/ee/routes/v1/secret.ts | 4 +- backend/src/ee/routes/v1/workspace.ts | 6 +- backend/src/helpers/auth.ts | 73 +++++++---- backend/src/middleware/requireAuth.ts | 26 ++-- backend/src/routes/v2/secrets.ts | 8 +- backend/src/routes/v2/users.ts | 2 +- backend/src/routes/v2/workspace.ts | 8 +- .../basic/dialog/AddApiKeyDialog.js | 121 ++++++------------ .../components/basic/table/ApiKeyTable.tsx | 22 ++-- frontend/pages/api/apiKey/addAPIKey.ts | 37 ++++++ frontend/pages/api/apiKey/deleteAPIKey.ts | 30 +++++ frontend/pages/api/apiKey/getAPIKeys.ts | 26 ++++ .../pages/api/serviceToken/addServiceToken.ts | 2 +- frontend/pages/settings/personal/[id].js | 40 +++--- .../public/locales/en/section-api-key.json | 13 ++ 17 files changed, 247 insertions(+), 180 deletions(-) create mode 100644 frontend/pages/api/apiKey/addAPIKey.ts create mode 100644 frontend/pages/api/apiKey/deleteAPIKey.ts create mode 100644 frontend/pages/api/apiKey/getAPIKeys.ts create mode 100644 frontend/public/locales/en/section-api-key.json diff --git a/README.md b/README.md index 3a3860d2c0..dd3710924d 100644 --- a/README.md +++ b/README.md @@ -335,13 +335,8 @@ Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot o - ## 🌎 Translations -<<<<<<< HEAD -Infisical is currently aviable in English and Korean. Help us translate Infisical to your language! -======= -Infisical is currently available in English and Korean. Help us translate Infisical to your language! ->>>>>>> 9ce4a52b8da0057c2450cd7af93a8c5758c2476b +Infisical is currently available in English and Korean. Help us translate Infisical to your language! You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181). diff --git a/backend/src/app.ts b/backend/src/app.ts index d32e83be77..e1ea949b96 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -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)) diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index ac3089e337..8e93919a02 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -12,7 +12,7 @@ import { ADMIN, MEMBER } from '../../../variables'; router.get( '/:secretId/secret-versions', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'apiKey'] }), requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER] @@ -27,7 +27,7 @@ router.get( router.post( '/:secretId/secret-versions/rollback', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'apiKey'] }), requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER] diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index c9da582614..a799d073b9 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -12,7 +12,7 @@ import { workspaceController } from '../../controllers/v1'; router.get( '/:workspaceId/secret-snapshots', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'apiKey'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] @@ -40,7 +40,7 @@ router.get( router.post( '/:workspaceId/secret-snapshots/rollback', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'apiKey'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] @@ -54,7 +54,7 @@ router.post( router.get( '/:workspaceId/logs', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'apiKey'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 2b972c09dd..fa1e50aa6f 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -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 + }); } /** diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 3be8418eed..24ef92f06c 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -7,7 +7,6 @@ import { getAuthSTDPayload, getAuthAPIKeyPayload } from '../helpers/auth'; -import { BadRequestError } from '../utils/errors'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -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; } diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index ccdce53215..45a273d351 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -61,7 +61,7 @@ router.post( }), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'apiKey'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -76,7 +76,7 @@ router.get( query('environment').exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'serviceToken'] + acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -115,7 +115,7 @@ router.patch( }), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'apiKey'] }), requireSecretsAuth({ acceptedRoles: [ADMIN, MEMBER] @@ -143,7 +143,7 @@ router.delete( .isEmpty(), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'apiKey'] }), requireSecretsAuth({ acceptedRoles: [ADMIN, MEMBER] diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index dba107b154..95b064a792 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -8,7 +8,7 @@ import { usersController } from '../../controllers/v2'; router.get( '/me', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'apiKey'] }), usersController.getMe ); diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index ca920e15ae..42c0f16a48 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -45,7 +45,7 @@ router.get( router.get( '/:workspaceId/encrypted-key', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'apiKey'] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER] @@ -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], @@ -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], @@ -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], diff --git a/frontend/components/basic/dialog/AddApiKeyDialog.js b/frontend/components/basic/dialog/AddApiKeyDialog.js index 3c9c359385..5adfd7dfd1 100644 --- a/frontend/components/basic/dialog/AddApiKeyDialog.js +++ b/frontend/components/basic/dialog/AddApiKeyDialog.js @@ -4,9 +4,10 @@ import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Dialog, Transition } from "@headlessui/react"; -import addServiceToken from "~/pages/api/serviceToken/addServiceToken"; -import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; +import addAPIKey from "~/pages/api/apiKey/addAPIKey"; +// import addServiceToken from "~/pages/api/serviceToken/addServiceToken"; +// import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; import { envMapping } from "../../../public/data/frequentConstants"; import { decryptAssymmetric, @@ -26,58 +27,33 @@ const expiryMapping = { const crypto = require('crypto'); +// TODO: convert to TS const AddApiKeyDialog = ({ isOpen, closeModal, - workspaceId, workspaceName, - serviceTokens, - setServiceTokens + apiKeys, + setApiKeys }) => { - const [serviceToken, setServiceToken] = useState(""); - const [serviceTokenName, setServiceTokenName] = useState(""); - const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); - const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); - const [serviceTokenCopied, setServiceTokenCopied] = useState(false); + const [apiKey, setApiKey] = useState(""); + const [apiKeyName, setApiKeyName] = useState(""); + const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState("1 day"); + const [apiKeyCopied, setApiKeyCopied] = useState(false); const { t } = useTranslation(); - const generateServiceToken = async () => { - const latestFileKey = await getLatestFileKey({ workspaceId }); - - const key = decryptAssymmetric({ - ciphertext: latestFileKey.latestKey.encryptedKey, - nonce: latestFileKey.latestKey.nonce, - publicKey: latestFileKey.latestKey.sender.publicKey, - privateKey: localStorage.getItem("PRIVATE_KEY"), - }); - - const randomBytes = crypto.randomBytes(16).toString('hex'); - const { - ciphertext, - iv, - tag, - } = encryptSymmetric({ - plaintext: key, - key: randomBytes, - }); - - let newServiceToken = await addServiceToken({ - name: serviceTokenName, - workspaceId, - environment: envMapping[serviceTokenEnv], - expiresIn: expiryMapping[serviceTokenExpiresIn], - encryptedKey: ciphertext, - iv, - tag + const generateAPIKey = async () => { + const newApiKey = await addAPIKey({ + name: apiKeyName, + expiresIn: expiryMapping[apiKeyExpiresIn] }); - setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData])); - setServiceToken(newServiceToken.serviceToken + "." + randomBytes); + setApiKeys([...apiKeys, newApiKey.apiKeyData]) + setApiKey(newApiKey.apiKey); }; function copyToClipboard() { // Get the text field - var copyText = document.getElementById("serviceToken"); + var copyText = document.getElementById("apiKey"); // Select the text field copyText.select(); @@ -86,16 +62,16 @@ const AddApiKeyDialog = ({ // Copy the text inside the text field navigator.clipboard.writeText(copyText.value); - setServiceTokenCopied(true); - setTimeout(() => setServiceTokenCopied(false), 2000); + setApiKeyCopied(true); + setTimeout(() => setApiKeyCopied(false), 2000); // Alert the copied text // alert("Copied the text: " + copyText.value); } - const closeAddServiceTokenModal = () => { + const closeAddApiKeyModal = () => { closeModal(); - setServiceTokenName(""); - setServiceToken(""); + setApiKeyName(""); + setApiKey(""); }; return ( @@ -125,51 +101,37 @@ const AddApiKeyDialog = ({ leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - {serviceToken == "" ? ( + {apiKey == "" ? ( - {t("section-token:add-dialog.title", { + {t("section-api-key:add-dialog.title", { target: workspaceName, })}

- {t("section-token:add-dialog.description")} + {t("section-api-key:add-dialog.description")}

-
- -
@@ -200,13 +162,13 @@ const AddApiKeyDialog = ({ as="h3" className="text-lg font-medium leading-6 text-gray-400 z-50" > - {t("section-token:add-dialog.copy-service-token")} + {t("section-api-key:add-dialog.copy-service-token")}

{t( - "section-token:add-dialog.copy-service-token-description" + "section-api-key:add-dialog.copy-service-token-description" )}

@@ -215,19 +177,20 @@ const AddApiKeyDialog = ({
- {serviceToken} + {apiKey}
@@ -365,4 +366,5 @@ export const getServerSideProps = getTranslatedServerSideProps([ "settings", "settings-personal", "section-password", + "section-api-key" ]); diff --git a/frontend/public/locales/en/section-api-key.json b/frontend/public/locales/en/section-api-key.json new file mode 100644 index 0000000000..3419744fe9 --- /dev/null +++ b/frontend/public/locales/en/section-api-key.json @@ -0,0 +1,13 @@ +{ + "api-keys": "Service Tokens", + "api-keys-description": "Every service token is specific to you, a certain project and a certain environment within this project.", + "add-new": "Add New Token", + "add-dialog": { + "title": "Add an API Key", + "description": "Specify the name and expiry period. When an API key is generated, you will only be able to see it once before it disappears. Make sure to save it somewhere.", + "name": "API Key Name", + "add": "Add API Key", + "copy-service-token": "Copy your API key", + "copy-service-token-description": "Once you close this popup, you will never see your API key again" + } +}