mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 15:40:47 +00:00
feat(microsoft-teams): better authentication flow and doc references
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user