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
@@ -47,7 +47,7 @@ export async function up(knex: Knex): Promise<void> {
table.foreign("id").references("id").inTable(TableName.WorkflowIntegrations).onDelete("CASCADE"); // the ID itself is the workflow integration ID
table.string("internalTeamsAppId").nullable();
table.string("tenantId").notNullable();
table.string("tenantId").unique().notNullable();
table.binary("encryptedAccessToken").nullable();
table.binary("encryptedBotAccessToken").nullable();
@@ -22,33 +22,63 @@ const sanitizedMicrosoftTeamsIntegrationSchema = WorkflowIntegrationsSchema.pick
export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
method: "GET",
url: "/client-id",
config: {
rateLimit: writeLimit
rateLimit: readLimit
},
schema: {
body: z.object({
tenantId: z.string(),
slug: slugSchema({ max: 64 })
}),
response: {
200: sanitizedMicrosoftTeamsIntegrationSchema
200: z.object({
clientId: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const microsoftTeamsIntegration = await server.services.microsoftTeams.createMicrosoftTeamsIntegration({
const clientId = await server.services.microsoftTeams.getClientId({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
actorOrgId: req.permission.orgId
});
return microsoftTeamsIntegration;
return {
clientId
};
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
body: z.object({
redirectUri: z.string(),
tenantId: z.string(),
slug: z.string(),
description: z.string().optional()
})
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.microsoftTeams.completeMicrosoftTeamsIntegration({
tenantId: req.body.tenantId,
slug: req.body.slug,
description: req.body.description,
redirectUri: req.body.redirectUri,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
}
});
server.route({
method: "GET",
url: "/",
@@ -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"