mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 05:27:48 +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.foreign("id").references("id").inTable(TableName.WorkflowIntegrations).onDelete("CASCADE"); // the ID itself is the workflow integration ID
|
||||||
|
|
||||||
table.string("internalTeamsAppId").nullable();
|
table.string("internalTeamsAppId").nullable();
|
||||||
table.string("tenantId").unique().notNullable();
|
table.string("tenantId").notNullable();
|
||||||
table.binary("encryptedAccessToken").nullable();
|
table.binary("encryptedAccessToken").nullable();
|
||||||
table.binary("encryptedBotAccessToken").nullable();
|
table.binary("encryptedBotAccessToken").nullable();
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ export const registerMicrosoftTeamsRouter = async (server: FastifyZodProvider) =
|
|||||||
redirectUri: z.string(),
|
redirectUri: z.string(),
|
||||||
tenantId: z.string().uuid(),
|
tenantId: z.string().uuid(),
|
||||||
slug: z.string(),
|
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,
|
slug: req.body.slug,
|
||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
redirectUri: req.body.redirectUri,
|
redirectUri: req.body.redirectUri,
|
||||||
|
code: req.body.code,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
|||||||
@@ -16,48 +16,77 @@ import { TWorkflowIntegrationDALFactory } from "../workflow-integration/workflow
|
|||||||
import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
import { WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
|
||||||
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
import { TMicrosoftTeamsIntegrationDALFactory } from "./microsoft-teams-integration-dal";
|
||||||
|
|
||||||
|
const ConsentError = "AADSTS65001";
|
||||||
|
|
||||||
export const verifyTenantFromCode = async (
|
export const verifyTenantFromCode = async (
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
|
code: string,
|
||||||
redirectUri: string,
|
redirectUri: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
clientSecret: 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({
|
return response.data.access_token;
|
||||||
client_id: clientId,
|
};
|
||||||
client_secret: clientSecret,
|
|
||||||
scope: "https://graph.microsoft.com/.default",
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
grant_type: "client_credentials"
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await axios
|
// Azure App-based auth
|
||||||
.post<{ access_token: string }>(tokenEndpoint, params, {
|
const applicationAccessToken = await getAccessToken(
|
||||||
headers: {
|
new URLSearchParams({
|
||||||
"Content-Type": "application/x-www-form-urlencoded"
|
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;
|
// User-based auth
|
||||||
const decodedToken = jwt.decode(accessToken) as { tid: string };
|
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
|
// Verify application token
|
||||||
const tenantIdFromToken = decodedToken.tid;
|
const { tid: tenantIdFromApplicationAccessToken } = jwt.decode(applicationAccessToken) as { tid: string };
|
||||||
|
|
||||||
if (tenantIdFromToken !== tenantId) {
|
if (tenantIdFromApplicationAccessToken !== tenantId) {
|
||||||
throw new BadRequestError({
|
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 (
|
export const getMicrosoftTeamsAccessToken = async (
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ export const microsoftTeamsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const completeMicrosoftTeamsIntegration = async ({
|
const completeMicrosoftTeamsIntegration = async ({
|
||||||
|
code,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
@@ -251,7 +252,7 @@ export const microsoftTeamsServiceFactory = ({
|
|||||||
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
|
const botAppPassword = decryptWithRoot(encryptedMicrosoftTeamsClientSecret);
|
||||||
const botId = decryptWithRoot(encryptedMicrosoftTeamsBotId);
|
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) => {
|
await workflowIntegrationDAL.transaction(async (tx) => {
|
||||||
const workflowIntegration = await workflowIntegrationDAL.create(
|
const workflowIntegration = await workflowIntegrationDAL.create(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type TCreateMicrosoftTeamsIntegrationDTO = Omit<TOrgPermission, "orgId">
|
|||||||
slug: string;
|
slug: string;
|
||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
code: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCheckInstallationStatusDTO = { workflowIntegrationId: string } & Omit<TOrgPermission, "orgId">;
|
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>
|
||||||
|
|
||||||
<Step title="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.
|
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.
|
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>
|
<Note>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export type TUpdateMicrosoftTeamsIntegrationDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateMicrosoftTeamsIntegrationDTO = {
|
export type TCreateMicrosoftTeamsIntegrationDTO = {
|
||||||
|
code: string;
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
+25
-18
@@ -1,3 +1,5 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -13,7 +15,8 @@ const stateSchema = z.object({
|
|||||||
tenantId: z.string(),
|
tenantId: z.string(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
csrfToken: z.string()
|
csrfToken: z.string(),
|
||||||
|
clientId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const OAuthCallbackPage = () => {
|
export const OAuthCallbackPage = () => {
|
||||||
@@ -27,7 +30,9 @@ export const OAuthCallbackPage = () => {
|
|||||||
|
|
||||||
const createMicrosoftTeamsWorkflowIntegration = useCreateMicrosoftTeamsIntegration();
|
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);
|
const state = stateSchema.parse(rawState);
|
||||||
|
|
||||||
@@ -42,21 +47,29 @@ export const OAuthCallbackPage = () => {
|
|||||||
const handleMicrosoftTeams = useCallback(async () => {
|
const handleMicrosoftTeams = useCallback(async () => {
|
||||||
clearState();
|
clearState();
|
||||||
|
|
||||||
if (Boolean(adminConsent.toLowerCase()) !== true) {
|
if (!code) {
|
||||||
throw new Error("Failed to grant admin consent");
|
throw new Error("No code provided");
|
||||||
}
|
|
||||||
|
|
||||||
if (stateTenant !== state.tenantId) {
|
|
||||||
throw new Error(`Invalid tenant ID. Expected ${stateTenant}, got ${state.tenantId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await createMicrosoftTeamsWorkflowIntegration.mutateAsync({
|
await createMicrosoftTeamsWorkflowIntegration.mutateAsync({
|
||||||
orgId: currentOrg.id,
|
orgId: currentOrg.id,
|
||||||
tenantId: state.tenantId,
|
tenantId: state.tenantId,
|
||||||
|
code,
|
||||||
slug: state.slug,
|
slug: state.slug,
|
||||||
description: state.description ?? "",
|
description: state.description ?? "",
|
||||||
redirectUri: state.redirectUri
|
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
|
// Ensure that the localstorage is ready for use, to avoid the form data being malformed
|
||||||
@@ -72,22 +85,16 @@ export const OAuthCallbackPage = () => {
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await handleMicrosoftTeams();
|
await handleMicrosoftTeams();
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully granted Microsoft Teams admin consent",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
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"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate({
|
|
||||||
to: ROUTE_PATHS.Organization.SettingsPage.path
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
}, [isReady]);
|
}, [isReady]);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { OAuthCallbackPage } from "./OauthCallbackPage";
|
|||||||
const SettingsOAuthCallbackPageQueryParamsSchema = z.object({
|
const SettingsOAuthCallbackPageQueryParamsSchema = z.object({
|
||||||
state: z
|
state: z
|
||||||
.object({
|
.object({
|
||||||
|
clientId: z.string(),
|
||||||
tenantId: z.string(),
|
tenantId: z.string(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
@@ -15,8 +16,7 @@ const SettingsOAuthCallbackPageQueryParamsSchema = z.object({
|
|||||||
})
|
})
|
||||||
.nullable()
|
.nullable()
|
||||||
.catch(null),
|
.catch(null),
|
||||||
tenant: z.string().catch(""),
|
code: z.string().catch("")
|
||||||
admin_consent: z.string().catch("")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
|
|||||||
+12
-2
@@ -103,10 +103,20 @@ export const MicrosoftTeamsIntegrationForm = ({ id, onClose }: Props) => {
|
|||||||
tenantId,
|
tenantId,
|
||||||
slug,
|
slug,
|
||||||
description,
|
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;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user