feat(microsoft-teams): better authentication flow and doc references

This commit is contained in:
Daniel Hougaard
2025-04-26 09:46:23 +04:00
parent 8987938642
commit 0323d152da
17 changed files with 477 additions and 103 deletions
@@ -15,6 +15,51 @@ import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow
import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
export const verifyTenantFromCode = async (
tenantId: string,
redirectUri: string,
clientId: string,
clientSecret: string
) => {
const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
const params = new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
scope: "https://graph.microsoft.com/.default",
redirect_uri: redirectUri,
grant_type: "client_credentials"
});
const response = await axios
.post<{ access_token: string }>(tokenEndpoint, params, {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
})
.catch((err) => {
if (axios.isAxiosError(err)) {
logger.error(err.response?.data, "Error fetching Microsoft Teams access token");
}
throw err;
});
const accessToken = response.data.access_token;
const tokenParts = accessToken.split(".");
const tokenPayload = JSON.parse(Buffer.from(tokenParts[1], "base64").toString()) as { tid: string };
// the 'tid' claim in the token contains the tenant ID
const tenantIdFromToken = tokenPayload.tid;
if (tenantIdFromToken !== tenantId) {
throw new BadRequestError({
message: `Invalid tenant state ID. Expected ${tenantId}, got ${tenantIdFromToken}`
});
}
return tenantIdFromToken;
};
export const getMicrosoftTeamsAccessToken = async (
{
orgId,
@@ -219,9 +264,6 @@ export const isBotInstalledInTenant = async (
tx
).catch(() => null);
console.log("botAccessToken", botAccessToken);
console.log("accessToken", accessToken);
if (!botAccessToken || !accessToken) {
return {
accessToken: null,
@@ -256,12 +298,6 @@ export const isBotInstalledInTenant = async (
const botInstalledInTenant = appsResponse.data.value.find((a) => a.externalId === botId);
for (const app of appsResponse.data.value) {
if (app.displayName.toLowerCase().includes("infisical")) {
console.log(`${app.displayName} - ${app.externalId}`);
}
}
if (!botInstalledInTenant) {
return {
installed: false,
@@ -10,7 +10,7 @@ import { FastifyReply, FastifyRequest } from "fastify";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "../kms/kms-service";
@@ -18,12 +18,18 @@ import { KmsDataKey } from "../kms/kms-types";
import { TSuperAdminDALFactory } from "../super-admin/super-admin-dal";
import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow-integration-dal";
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
import { getMicrosoftTeamsAccessToken, isBotInstalledInTenant, TeamsBot } from "./microsoft-teams-fns";
import {
getMicrosoftTeamsAccessToken,
isBotInstalledInTenant,
TeamsBot,
verifyTenantFromCode
} from "./microsoft-teams-fns";
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
import {
TCheckInstallationStatusDTO,
TCreateMicrosoftTeamsIntegrationDTO,
TDeleteMicrosoftTeamsIntegrationDTO,
TGetClientIdDTO,
TGetMicrosoftTeamsIntegrationByIdDTO,
TGetMicrosoftTeamsIntegrationByOrgDTO,
TGetTeamsDTO,
@@ -202,14 +208,15 @@ export const microsoftTeamsServiceFactory = ({
}
};
const createMicrosoftTeamsIntegration = async ({
actorId,
const completeMicrosoftTeamsIntegration = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
tenantId,
slug,
description,
tenantId
redirectUri
}: TCreateMicrosoftTeamsIntegrationDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
@@ -237,7 +244,14 @@ export const microsoftTeamsServiceFactory = ({
});
}
const integration = await workflowIntegrationDAL.transaction(async (tx) => {
const decryptWithRoot = kmsService.decryptWithRootKey();
const botAppId = decryptWithRoot(encryptedMicrosoftTeamsAppId);
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId);
await verifyTenantFromCode(tenantId, redirectUri, botAppId.toString(), botAppPassword.toString());
await workflowIntegrationDAL.transaction(async (tx) => {
const workflowIntegration = await workflowIntegrationDAL.create(
{
description,
@@ -249,19 +263,24 @@ export const microsoftTeamsServiceFactory = ({
tx
);
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL.create(
{
// @ts-expect-error id is kept as fixed because it is always equal to the workflow integration ID
id: workflowIntegration.id,
tenantId
},
tx
);
const decryptWithRoot = kmsService.decryptWithRootKey();
const botAppId = decryptWithRoot(encryptedMicrosoftTeamsAppId);
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId);
const microsoftTeamsIntegration = await microsoftTeamsIntegrationDAL
.create(
{
// @ts-expect-error id is kept as fixed because it is always equal to the workflow integration ID
id: workflowIntegration.id,
tenantId
},
tx
)
.catch((err) => {
if (err instanceof DatabaseError) {
if ((err.error as Error)?.stack?.includes("duplicate key value violates unique constraint"))
throw new BadRequestError({
message: "Microsoft Teams integration with the same Tenant ID already exists."
});
}
throw err;
});
const teamsBotInfo = await isBotInstalledInTenant(
{
@@ -307,17 +326,37 @@ export const microsoftTeamsServiceFactory = ({
tx
);
}
return {
...workflowIntegration,
status: workflowIntegration.status as WorkflowIntegrationStatus,
tenantId: microsoftTeamsIntegration.tenantId
};
});
return integration;
};
const getClientId = async ({ actorId, actor, actorOrgId, actorAuthMethod }: TGetClientIdDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!serverCfg) {
throw new BadRequestError({
message: "Failed to get server configuration."
});
}
if (!serverCfg.encryptedMicrosoftTeamsAppId) {
throw new BadRequestError({
message: "Microsoft Teams app ID is not set"
});
}
const decryptWithRoot = kmsService.decryptWithRootKey();
const clientId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
return clientId.toString();
};
const getMicrosoftTeamsIntegrationsByOrg = async ({
actorId,
actor,
@@ -335,7 +374,8 @@ export const microsoftTeamsServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
const microsoftTeamsIntegrations = await microsoftTeamsIntegrationDAL.findWithWorkflowIntegrationDetails({
orgId: actorOrgId
orgId: actorOrgId,
status: WorkflowIntegrationStatus.INSTALLED
});
return microsoftTeamsIntegrations.map((integration) => ({
@@ -652,12 +692,13 @@ export const microsoftTeamsServiceFactory = ({
getMicrosoftTeamsIntegrationById,
updateMicrosoftTeamsIntegration,
deleteMicrosoftTeamsIntegration,
createMicrosoftTeamsIntegration,
completeMicrosoftTeamsIntegration,
initializeTeamsBot,
getTeams,
handleMessageEndpoint,
start,
sendNotification,
checkInstallationStatus
checkInstallationStatus,
getClientId
};
};
@@ -3,10 +3,13 @@ import { TNotification } from "@app/lib/workflow-integrations/types";
export type TGetMicrosoftTeamsIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
export type TGetClientIdDTO = Omit<TOrgPermission, "orgId">;
export type TCreateMicrosoftTeamsIntegrationDTO = Omit<TOrgPermission, "orgId"> & {
slug: string;
description?: string;
tenantId: string;
slug: string;
redirectUri: string;
description?: string;
};
export type TCheckInstallationStatusDTO = { workflowIntegrationId: string } & Omit<TOrgPermission, "orgId">;
@@ -67,7 +67,7 @@ import { validateSlackChannelsField } from "../slack/slack-auth-validators";
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { WorkflowIntegration } from "../workflow-integration/workflow-integration-types";
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
import { TProjectDALFactory } from "./project-dal";
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
import { TProjectQueueFactory } from "./project-queue";
@@ -1539,6 +1539,12 @@ export const projectServiceFactory = ({
});
}
if (microsoftTeamsIntegration.status !== WorkflowIntegrationStatus.INSTALLED) {
throw new BadRequestError({
message: "Microsoft Teams integration is not properly installed in your tenant."
});
}
if (microsoftTeamsIntegration.orgId !== actorOrgId) {
throw new ForbiddenRequestError({
message: "Selected Microsoft Teams integration is not in the same organization"