mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
fix: improve auth step to avoid takeovers
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").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">;
|
||||
|
||||
@@ -126,12 +126,17 @@ This guide will provide step by step instructions on how to configure Microsoft
|
||||

|
||||
</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.
|
||||
|
||||

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

|
||||
|
||||
<Note>
|
||||
|
||||
@@ -59,6 +59,7 @@ export type TUpdateMicrosoftTeamsIntegrationDTO = {
|
||||
};
|
||||
|
||||
export type TCreateMicrosoftTeamsIntegrationDTO = {
|
||||
code: string;
|
||||
tenantId: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user