You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
306 lines
8.5 KiB
306 lines
8.5 KiB
import { Types } from "mongoose";
|
|
import { Bot, IntegrationAuth } from "../models";
|
|
import { exchangeCode, exchangeRefresh } from "../integrations";
|
|
import { BotService } from "../services";
|
|
import {
|
|
ALGORITHM_AES_256_GCM,
|
|
ENCODING_SCHEME_UTF8,
|
|
INTEGRATION_NETLIFY,
|
|
INTEGRATION_VERCEL
|
|
} from "../variables";
|
|
import { UnauthorizedRequestError } from "../utils/errors";
|
|
import { syncSecretsToActiveIntegrationsQueue } from "../queues/integrations/syncSecretsToThirdPartyServices"
|
|
|
|
interface Update {
|
|
workspace: string;
|
|
integration: string;
|
|
teamId?: string;
|
|
accountId?: string;
|
|
}
|
|
|
|
/**
|
|
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
|
* named [integration]
|
|
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
|
* - Add placeholder inactive integration
|
|
* - Create bot sequence for integration
|
|
* @param {Object} obj
|
|
* @param {String} obj.workspaceId - id of workspace
|
|
* @param {String} obj.integration - name of integration
|
|
* @param {String} obj.code - code
|
|
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
|
|
*/
|
|
export const handleOAuthExchangeHelper = async ({
|
|
workspaceId,
|
|
integration,
|
|
code,
|
|
environment
|
|
}: {
|
|
workspaceId: string;
|
|
integration: string;
|
|
code: string;
|
|
environment: string;
|
|
}) => {
|
|
const bot = await Bot.findOne({
|
|
workspace: workspaceId,
|
|
isActive: true
|
|
});
|
|
|
|
if (!bot) throw new Error("Bot must be enabled for OAuth2 code-token exchange");
|
|
|
|
// exchange code for access and refresh tokens
|
|
const res = await exchangeCode({
|
|
integration,
|
|
code
|
|
});
|
|
|
|
const update: Update = {
|
|
workspace: workspaceId,
|
|
integration
|
|
};
|
|
|
|
switch (integration) {
|
|
case INTEGRATION_VERCEL:
|
|
update.teamId = res.teamId;
|
|
break;
|
|
case INTEGRATION_NETLIFY:
|
|
update.accountId = res.accountId;
|
|
break;
|
|
}
|
|
|
|
const integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
|
{
|
|
workspace: workspaceId,
|
|
integration
|
|
},
|
|
update,
|
|
{
|
|
new: true,
|
|
upsert: true
|
|
}
|
|
);
|
|
|
|
if (res.refreshToken) {
|
|
// case: refresh token returned from exchange
|
|
// set integration auth refresh token
|
|
await setIntegrationAuthRefreshHelper({
|
|
integrationAuthId: integrationAuth._id.toString(),
|
|
refreshToken: res.refreshToken
|
|
});
|
|
}
|
|
|
|
if (res.accessToken) {
|
|
// case: access token returned from exchange
|
|
// set integration auth access token
|
|
await setIntegrationAuthAccessHelper({
|
|
integrationAuthId: integrationAuth._id.toString(),
|
|
accessId: null,
|
|
accessToken: res.accessToken,
|
|
accessExpiresAt: res.accessExpiresAt
|
|
});
|
|
}
|
|
|
|
return integrationAuth;
|
|
};
|
|
|
|
/**
|
|
* Return decrypted refresh token using the bot's copy
|
|
* of the workspace key for workspace belonging to integration auth
|
|
* with id [integrationAuthId]
|
|
* @param {Object} obj
|
|
* @param {String} obj.integrationAuthId - id of integration auth
|
|
* @param {String} refreshToken - decrypted refresh token
|
|
*/
|
|
export const getIntegrationAuthRefreshHelper = async ({
|
|
integrationAuthId
|
|
}: {
|
|
integrationAuthId: Types.ObjectId;
|
|
}) => {
|
|
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select(
|
|
"+refreshCiphertext +refreshIV +refreshTag"
|
|
);
|
|
|
|
if (!integrationAuth)
|
|
throw UnauthorizedRequestError({
|
|
message: "Failed to locate Integration Authentication credentials"
|
|
});
|
|
|
|
const refreshToken = await BotService.decryptSymmetric({
|
|
workspaceId: integrationAuth.workspace,
|
|
ciphertext: integrationAuth.refreshCiphertext as string,
|
|
iv: integrationAuth.refreshIV as string,
|
|
tag: integrationAuth.refreshTag as string
|
|
});
|
|
|
|
return refreshToken;
|
|
};
|
|
|
|
/**
|
|
* Return decrypted access token using the bot's copy
|
|
* of the workspace key for workspace belonging to integration auth
|
|
* with id [integrationAuthId]
|
|
* @param {Object} obj
|
|
* @param {String} obj.integrationAuthId - id of integration auth
|
|
* @returns {String} accessToken - decrypted access token
|
|
*/
|
|
export const getIntegrationAuthAccessHelper = async ({
|
|
integrationAuthId
|
|
}: {
|
|
integrationAuthId: Types.ObjectId;
|
|
}) => {
|
|
let accessId;
|
|
let accessToken;
|
|
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select(
|
|
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag"
|
|
);
|
|
|
|
if (!integrationAuth)
|
|
throw UnauthorizedRequestError({
|
|
message: "Failed to locate Integration Authentication credentials"
|
|
});
|
|
|
|
accessToken = await BotService.decryptSymmetric({
|
|
workspaceId: integrationAuth.workspace,
|
|
ciphertext: integrationAuth.accessCiphertext as string,
|
|
iv: integrationAuth.accessIV as string,
|
|
tag: integrationAuth.accessTag as string
|
|
});
|
|
|
|
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
|
// there is a access token expiration date
|
|
// and refresh token to exchange with the OAuth2 server
|
|
|
|
if (integrationAuth.accessExpiresAt < new Date()) {
|
|
// access token is expired
|
|
const refreshToken = await getIntegrationAuthRefreshHelper({
|
|
integrationAuthId
|
|
});
|
|
accessToken = await exchangeRefresh({
|
|
integrationAuth,
|
|
refreshToken
|
|
});
|
|
}
|
|
}
|
|
|
|
if (
|
|
integrationAuth?.accessIdCiphertext &&
|
|
integrationAuth?.accessIdIV &&
|
|
integrationAuth?.accessIdTag
|
|
) {
|
|
accessId = await BotService.decryptSymmetric({
|
|
workspaceId: integrationAuth.workspace,
|
|
ciphertext: integrationAuth.accessIdCiphertext as string,
|
|
iv: integrationAuth.accessIdIV as string,
|
|
tag: integrationAuth.accessIdTag as string
|
|
});
|
|
}
|
|
|
|
return {
|
|
accessId,
|
|
accessToken
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Encrypt refresh token [refreshToken] using the bot's copy
|
|
* of the workspace key for workspace belonging to integration auth
|
|
* with id [integrationAuthId] and store it
|
|
* @param {Object} obj
|
|
* @param {String} obj.integrationAuthId - id of integration auth
|
|
* @param {String} obj.refreshToken - refresh token
|
|
*/
|
|
export const setIntegrationAuthRefreshHelper = async ({
|
|
integrationAuthId,
|
|
refreshToken
|
|
}: {
|
|
integrationAuthId: string;
|
|
refreshToken: string;
|
|
}) => {
|
|
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
|
|
|
if (!integrationAuth) throw new Error("Failed to find integration auth");
|
|
|
|
const obj = await BotService.encryptSymmetric({
|
|
workspaceId: integrationAuth.workspace,
|
|
plaintext: refreshToken
|
|
});
|
|
|
|
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
|
{
|
|
_id: integrationAuthId
|
|
},
|
|
{
|
|
refreshCiphertext: obj.ciphertext,
|
|
refreshIV: obj.iv,
|
|
refreshTag: obj.tag,
|
|
algorithm: ALGORITHM_AES_256_GCM,
|
|
keyEncoding: ENCODING_SCHEME_UTF8
|
|
},
|
|
{
|
|
new: true
|
|
}
|
|
);
|
|
|
|
return integrationAuth;
|
|
};
|
|
|
|
/**
|
|
* Encrypt access token [accessToken] and (optionally) access id [accessId]
|
|
* using the bot's copy of the workspace key for workspace belonging to
|
|
* integration auth with id [integrationAuthId] and store it along with [accessExpiresAt]
|
|
* @param {Object} obj
|
|
* @param {String} obj.integrationAuthId - id of integration auth
|
|
* @param {String} obj.accessToken - access token
|
|
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
|
*/
|
|
export const setIntegrationAuthAccessHelper = async ({
|
|
integrationAuthId,
|
|
accessId,
|
|
accessToken,
|
|
accessExpiresAt
|
|
}: {
|
|
integrationAuthId: string;
|
|
accessId: string | null;
|
|
accessToken: string;
|
|
accessExpiresAt: Date | undefined;
|
|
}) => {
|
|
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
|
|
|
if (!integrationAuth) throw new Error("Failed to find integration auth");
|
|
|
|
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
|
|
workspaceId: integrationAuth.workspace,
|
|
plaintext: accessToken
|
|
});
|
|
|
|
let encryptedAccessIdObj;
|
|
if (accessId) {
|
|
encryptedAccessIdObj = await BotService.encryptSymmetric({
|
|
workspaceId: integrationAuth.workspace,
|
|
plaintext: accessId
|
|
});
|
|
}
|
|
|
|
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
|
{
|
|
_id: integrationAuthId
|
|
},
|
|
{
|
|
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
|
|
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
|
|
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
|
|
accessCiphertext: encryptedAccessTokenObj.ciphertext,
|
|
accessIV: encryptedAccessTokenObj.iv,
|
|
accessTag: encryptedAccessTokenObj.tag,
|
|
accessExpiresAt,
|
|
algorithm: ALGORITHM_AES_256_GCM,
|
|
keyEncoding: ENCODING_SCHEME_UTF8
|
|
},
|
|
{
|
|
new: true
|
|
}
|
|
);
|
|
|
|
return integrationAuth;
|
|
};
|