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

View File

@@ -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();

View File

@@ -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,

View File

@@ -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 (

View File

@@ -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(

View File

@@ -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">;

View File

@@ -126,12 +126,17 @@ This guide will provide step by step instructions on how to configure Microsoft
![microsoft-teams-configure-bot](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-configure-bot.png)
</Step>
<Step title="Run an app validation test">
To ensure that the Microsoft Teams App is working correctly, you can run an app validation test.
<Step title="Run an app validation test (Recommended)">
To ensure that the Microsoft Teams App is working correctly, you can run an app validation test. This step is optional, but recommended to ensure the app is working correctly.
You should expect to see two errors related to sending welcome messages, because we haven't configured the Microsoft Teams App inside Infisical yet, which is required for proactive messages.
![microsoft-teams-app-validation-test](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-app-validation.png)
<Note>
You may see manifest validation errors. Before running an app validation test, you must ensure that your app has all errors resolved, such as having a description and a valid name.
</Note>
![microsoft-teams-app-validation-test-results](/images/platform/workflow-integrations/microsoft-teams-integration/teams-dev-portal-app-validation-result.png)
<Note>

View File

@@ -59,6 +59,7 @@ export type TUpdateMicrosoftTeamsIntegrationDTO = {
};
export type TCreateMicrosoftTeamsIntegrationDTO = {
code: string;
tenantId: string;
slug: string;
description?: string;

View File

@@ -1,3 +1,5 @@
import crypto from "crypto";
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { z } from "zod";
@@ -13,7 +15,8 @@ const stateSchema = z.object({
tenantId: z.string(),
slug: z.string(),
description: z.string().optional(),
csrfToken: z.string()
csrfToken: z.string(),
clientId: z.string()
});
export const OAuthCallbackPage = () => {
@@ -27,7 +30,9 @@ export const OAuthCallbackPage = () => {
const createMicrosoftTeamsWorkflowIntegration = useCreateMicrosoftTeamsIntegration();
const { state: rawState, tenant: stateTenant, admin_consent: adminConsent } = search;
const { state: rawState, code } = search;
console.log("the code is ", code);
const state = stateSchema.parse(rawState);
@@ -42,21 +47,29 @@ export const OAuthCallbackPage = () => {
const handleMicrosoftTeams = useCallback(async () => {
clearState();
if (Boolean(adminConsent.toLowerCase()) !== true) {
throw new Error("Failed to grant admin consent");
}
if (stateTenant !== state.tenantId) {
throw new Error(`Invalid tenant ID. Expected ${stateTenant}, got ${state.tenantId}`);
if (!code) {
throw new Error("No code provided");
}
await createMicrosoftTeamsWorkflowIntegration.mutateAsync({
orgId: currentOrg.id,
tenantId: state.tenantId,
code,
slug: state.slug,
description: state.description ?? "",
redirectUri: state.redirectUri
});
createNotification({
text: "Successfully granted Microsoft Teams admin consent",
type: "success"
});
navigate({
to: ROUTE_PATHS.Organization.SettingsPage.path
});
return;
}, []);
// Ensure that the localstorage is ready for use, to avoid the form data being malformed
@@ -72,22 +85,16 @@ export const OAuthCallbackPage = () => {
(async () => {
try {
await handleMicrosoftTeams();
createNotification({
text: "Successfully granted Microsoft Teams admin consent",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to grant Microsoft Teams admin consent",
text:
code !== ""
? "Failed to create Microsoft Teams workflow integration"
: "Failed to grant Microsoft Teams admin consent",
type: "error"
});
}
navigate({
to: ROUTE_PATHS.Organization.SettingsPage.path
});
})();
}, [isReady]);

View File

@@ -7,6 +7,7 @@ import { OAuthCallbackPage } from "./OauthCallbackPage";
const SettingsOAuthCallbackPageQueryParamsSchema = z.object({
state: z
.object({
clientId: z.string(),
tenantId: z.string(),
slug: z.string(),
description: z.string().optional(),
@@ -15,8 +16,7 @@ const SettingsOAuthCallbackPageQueryParamsSchema = z.object({
})
.nullable()
.catch(null),
tenant: z.string().catch(""),
admin_consent: z.string().catch("")
code: z.string().catch("")
});
export const Route = createFileRoute(

View File

@@ -103,10 +103,20 @@ export const MicrosoftTeamsIntegrationForm = ({ id, onClose }: Props) => {
tenantId,
slug,
description,
csrfToken
csrfToken,
clientId: microsoftTeamsClientId.clientId
};
const url = `https://login.microsoftonline.com/${tenantId}/adminconsent?client_id=${microsoftTeamsClientId.clientId}&redirect_uri=${state.redirectUri}&state=${encodeURIComponent(JSON.stringify(state))}`;
const url = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?
client_id=${microsoftTeamsClientId.clientId}
&redirect_uri=${state.redirectUri}
&response_type=code
&response_mode=query
&scope=https://graph.microsoft.com/.default
&state=${encodeURIComponent(JSON.stringify(state))}
&prompt=consent
&admin_consent=true`;
window.location.href = url;
}
};