fix: improve auth step to avoid takeovers

This commit is contained in:
Daniel Hougaard
2025-05-01 04:08:58 +04:00
parent aedc6e16ad
commit e08c5f265e
10 changed files with 109 additions and 53 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").unique().notNullable();
table.string("tenantId").notNullable();
table.binary("encryptedAccessToken").nullable();
table.binary("encryptedBotAccessToken").nullable();
@@ -61,7 +61,8 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) =
redirectUri: z.string(),
tenantId: z.string().uuid(),
slug: z.string(),
description: z.string().optional()
description: z.string().optional(),
code: z.string().trim()
})
},
@@ -72,6 +73,7 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) =
slug: req.body.slug,
description: req.body.description,
redirectUri: req.body.redirectUri,
code: req.body.code,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -16,48 +16,77 @@ import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow
import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
const ConsentError = "AADSTS65001";
export const verifyTenantFromCode = async (
tenantId: string,
code: string,
redirectUri: string,
clientId: string,
clientSecret: string
) => {
const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
const getAccessToken = async (params: URLSearchParams) => {
const response = await axios
.post<{ access_token: string }>(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, params, {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
})
.catch((err) => {
if (axios.isAxiosError(err)) {
if ((err.response?.data as { error_description?: string })?.error_description?.includes(ConsentError)) {
throw new BadRequestError({
message: "Unable to verify tenant, please ensure that you have granted admin consent."
});
}
logger.error(err.response?.data, "Error fetching Microsoft Teams access token");
}
throw err;
});
const params = new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
scope: "https://graph.microsoft.com/.default",
redirect_uri: redirectUri,
grant_type: "client_credentials"
});
return response.data.access_token;
};
const response = await axios
.post<{ access_token: string }>(tokenEndpoint, params, {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
// Azure App-based auth
const applicationAccessToken = await getAccessToken(
new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
scope: "https://graph.microsoft.com/.default",
redirect_uri: redirectUri,
grant_type: "client_credentials"
})
.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 decodedToken = jwt.decode(accessToken) as { tid: string };
// User-based auth
const authorizationAccessToken = await getAccessToken(
new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
scope: "https://graph.microsoft.com/.default",
redirect_uri: redirectUri,
grant_type: "authorization_code",
code
})
);
// the 'tid' claim in the token contains the tenant ID
const tenantIdFromToken = decodedToken.tid;
// Verify application token
const { tid: tenantIdFromApplicationAccessToken } = jwt.decode(applicationAccessToken) as { tid: string };
if (tenantIdFromToken !== tenantId) {
if (tenantIdFromApplicationAccessToken !== tenantId) {
throw new BadRequestError({
message: `Invalid tenant state ID. Expected ${tenantId}, got ${tenantIdFromToken}`
message: `Invalid application token tenant ID. Expected ${tenantId}, got ${tenantIdFromApplicationAccessToken}`
});
}
return tenantIdFromToken;
// Verify user authorization token
const { tid: tenantIdFromAuthorizationAccessToken } = jwt.decode(authorizationAccessToken) as { tid: string };
if (tenantIdFromAuthorizationAccessToken !== tenantId) {
throw new BadRequestError({
message: `Invalid authorization token tenant ID. Expected ${tenantId}, got ${tenantIdFromAuthorizationAccessToken}`
});
}
};
export const getMicrosoftTeamsAccessToken = async (
@@ -211,6 +211,7 @@ export const microsoftTeamsServiceFactory = ({
};
const completeMicrosoftTeamsIntegration = async ({
code,
actor,
actorId,
actorOrgId,
@@ -251,7 +252,7 @@ export const microsoftTeamsServiceFactory = ({
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId);
await verifyTenantFromCode(tenantId, redirectUri, botAppId.toString(), botAppPassword.toString());
await verifyTenantFromCode(tenantId, code, redirectUri, botAppId.toString(), botAppPassword.toString());
await workflowIntegrationDAL.transaction(async (tx) => {
const workflowIntegration = await workflowIntegrationDAL.create(
@@ -10,6 +10,7 @@ export type TCreateMicrosoftTeamsIntegrationDTO = Omit<TOrgPermission, "orgId">
slug: string;
redirectUri: string;
description?: string;
code: string;
};
export type TCheckInstallationStatusDTO = { workflowIntegrationId: string } & Omit<TOrgPermission, "orgId">;