diff --git a/package.json b/package.json
index 69ceda7bc256..aa76e8c186f5 100644
--- a/package.json
+++ b/package.json
@@ -81,7 +81,7 @@
"react-apexcharts": "1.7.0",
"react-beautiful-dnd": "13.1.1",
"react-copy-to-clipboard": "^5.1.0",
- "react-dom": "19.1.1",
+ "react-dom": "19.2.3",
"react-dropzone": "14.3.8",
"react-error-boundary": "^6.1.0",
"react-grid-layout": "^1.5.0",
diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx
index 88e517a2139d..0818190ca1cc 100644
--- a/src/components/CippComponents/CIPPM365OAuthButton.jsx
+++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx
@@ -1,7 +1,9 @@
import { useState, useEffect } from "react";
import { Alert, Button, Typography, CircularProgress, Box } from "@mui/material";
+import { Microsoft, Login, Refresh } from "@mui/icons-material";
import { ApiGetCall } from "../../api/ApiCall";
import { CippCopyToClipBoard } from "./CippCopyToClipboard";
+import { CippApiDialog } from "./CippApiDialog";
export const CIPPM365OAuthButton = ({
onAuthSuccess,
@@ -14,12 +16,14 @@ export const CIPPM365OAuthButton = ({
applicationId = null,
autoStartDeviceLogon = false,
validateServiceAccount = true,
+ promptBeforeAuth = false,
}) => {
const [authInProgress, setAuthInProgress] = useState(false);
const [authError, setAuthError] = useState(null);
const [deviceCodeInfo, setDeviceCodeInfo] = useState(null);
const [codeRetrievalInProgress, setCodeRetrievalInProgress] = useState(false);
const [isServiceAccount, setIsServiceAccount] = useState(true);
+ const [promptDialog, setPromptDialog] = useState({ open: false });
const [tokens, setTokens] = useState({
accessToken: null,
refreshToken: null,
@@ -32,13 +36,10 @@ export const CIPPM365OAuthButton = ({
const appIdInfo = ApiGetCall({
url: `/api/ExecListAppId`,
+ queryKey: "listAppId",
waiting: true,
});
- useEffect(() => {
- appIdInfo.refetch();
- }, []);
-
const handleCloseError = () => {
setAuthError(null);
};
@@ -55,8 +56,10 @@ export const CIPPM365OAuthButton = ({
setCodeRetrievalInProgress(true);
setAuthError(null);
- // Refetch appId to ensure we have the latest
- await appIdInfo.refetch();
+ // Only refetch appId if not already present
+ if (!applicationId && !appIdInfo?.data?.applicationId) {
+ await appIdInfo.refetch();
+ }
try {
// Get the application ID to use
@@ -66,8 +69,8 @@ export const CIPPM365OAuthButton = ({
// Request device code from our API endpoint
const deviceCodeResponse = await fetch(
`/api/ExecDeviceCodeLogon?operation=getDeviceCode&clientId=${appId}&scope=${encodeURIComponent(
- scope
- )}`
+ scope,
+ )}`,
);
const deviceCodeData = await deviceCodeResponse.json();
@@ -95,8 +98,10 @@ export const CIPPM365OAuthButton = ({
// Device code authentication function - opens popup and starts polling
const handleDeviceCodeAuthentication = async () => {
- // Refetch appId to ensure we have the latest
- await appIdInfo.refetch();
+ // Only refetch appId if not already present
+ if (!applicationId && !appIdInfo?.data?.applicationId) {
+ await appIdInfo.refetch();
+ }
if (!deviceCodeInfo) {
// If we don't have a device code yet, retrieve it first
@@ -129,7 +134,7 @@ export const CIPPM365OAuthButton = ({
const popup = window.open(
"https://microsoft.com/devicelogin",
"deviceLoginPopup",
- `width=${width},height=${height},left=${left},top=${top}`
+ `width=${width},height=${height},left=${left},top=${top}`,
);
// Start polling for token
@@ -155,7 +160,7 @@ export const CIPPM365OAuthButton = ({
try {
// Poll for token using our API endpoint
const tokenResponse = await fetch(
- `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeInfo.device_code}`
+ `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeInfo.device_code}`,
);
const tokenData = await tokenResponse.json();
@@ -263,7 +268,9 @@ export const CIPPM365OAuthButton = ({
};
// MSAL-like authentication function
- const handleMsalAuthentication = async () => {
+ const handleMsalAuthentication = async (retryCount = 0) => {
+ const maxRetries = 3;
+
// Clear previous authentication state when starting a new authentication
setAuthInProgress(true);
setAuthError(null);
@@ -277,10 +284,12 @@ export const CIPPM365OAuthButton = ({
onmicrosoftDomain: null,
});
- // Refetch app ID info to ensure we have the latest
- await appIdInfo.refetch();
+ // Only refetch app ID if not already present
+ if (!applicationId && !appIdInfo?.data?.applicationId) {
+ await appIdInfo.refetch();
+ }
- // Get the application ID to use - now we're sure to have the latest after the await
+ // Get the application ID to use
const appId = applicationId || appIdInfo?.data?.applicationId;
// Generate MSAL-like authentication parameters
@@ -327,7 +336,7 @@ export const CIPPM365OAuthButton = ({
const popup = window.open(
authUrl,
"msalAuthPopup",
- `width=${width},height=${height},left=${left},top=${top}`
+ `width=${width},height=${height},left=${left},top=${top}`,
);
// Function to actually exchange the authorization code for tokens
@@ -356,20 +365,43 @@ export const CIPPM365OAuthButton = ({
};
// Make the token request through our API proxy to avoid origin header issues
- const tokenResponse = await fetch(`/api/ExecTokenExchange`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- tokenRequest,
- tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
- tenantId: appId, // Pass the tenant ID to retrieve the correct client secret
- }),
- });
+ // Retry logic for AADSTS650051 (service principal already exists)
+ let retryCount = 0;
+ const maxRetries = 3;
+ let tokenResponse;
+ let tokenData;
+
+ while (retryCount <= maxRetries) {
+ tokenResponse = await fetch(`/api/ExecTokenExchange`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ tokenRequest,
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
+ tenantId: appId, // Pass the tenant ID to retrieve the correct client secret
+ }),
+ });
- // Parse the token response
- const tokenData = await tokenResponse.json();
+ // Parse the token response
+ tokenData = await tokenResponse.json();
+
+ // Check if it's the AADSTS650051 error (service principal already exists)
+ if (
+ tokenData.error === "invalid_client" &&
+ tokenData.error_description?.includes("AADSTS650051")
+ ) {
+ retryCount++;
+ if (retryCount <= maxRetries) {
+ // Wait before retrying (exponential backoff)
+ await new Promise((resolve) => setTimeout(resolve, 2000 * retryCount));
+ continue;
+ }
+ }
+ // If no error or different error, break out of retry loop
+ break;
+ }
// Check if the response contains an error
if (tokenData.error) {
@@ -408,6 +440,9 @@ export const CIPPM365OAuthButton = ({
if (!refreshResponse.ok) {
console.warn("Failed to store refresh token, but continuing with authentication");
+ } else {
+ // Invalidate the listAppId and tenants-table queryKeys to refresh data
+ appIdInfo.refetch();
}
} catch (error) {
console.error("Failed to store refresh token:", error);
@@ -502,7 +537,27 @@ export const CIPPM365OAuthButton = ({
const errorCode = urlParams.get("error");
const errorDescription = urlParams.get("error_description");
- // Set the error state
+ // Check if it's the AADSTS650051 error (service principal already exists during consent)
+ if (
+ errorCode === "invalid_client" &&
+ errorDescription?.includes("AADSTS650051") &&
+ retryCount < maxRetries
+ ) {
+ // Close the popup
+ popup.close();
+ setAuthInProgress(false);
+
+ // Wait before retrying (exponential backoff)
+ setTimeout(
+ () => {
+ handleMsalAuthentication(retryCount + 1);
+ },
+ 2000 * (retryCount + 1),
+ );
+ return;
+ }
+
+ // Set the error state for non-retryable errors
const error = {
errorCode: errorCode,
errorMessage: errorDescription || "Unknown authentication error",
@@ -550,9 +605,9 @@ export const CIPPM365OAuthButton = ({
{!applicationId &&
!appIdInfo.isLoading &&
- appIdInfo?.data && // Only check if data is available
+ appIdInfo?.data?.applicationId && // Only check if applicationId is present in data
!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
- appIdInfo?.data?.applicationId
+ appIdInfo?.data?.applicationId,
) && (
The Application ID is not valid. Please check your configuration.
@@ -653,6 +708,30 @@ export const CIPPM365OAuthButton = ({
) : null}
)}
+
+ {promptBeforeAuth !== false && (
+ setPromptDialog({ open: false }),
+ }}
+ api={{
+ type: "POST",
+ confirmText: promptBeforeAuth,
+ noConfirm: false,
+ customFunction: () => {
+ setPromptDialog({ open: false });
+ const authFunction = useDeviceCode
+ ? handleDeviceCodeAuthentication
+ : handleMsalAuthentication;
+ authFunction();
+ },
+ }}
+ fields={[]}
+ />
+ )}
+
);
diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx
index 99d46a6e5182..af96b19cd027 100644
--- a/src/components/CippComponents/CippTranslations.jsx
+++ b/src/components/CippComponents/CippTranslations.jsx
@@ -52,4 +52,11 @@ export const CippTranslations = {
includeTenantId: "Include Tenant ID in Notifications",
logsToInclude: "Logs to Include in notifications",
assignmentFilterManagementType: "Filter Type",
+ microsoftSupport: "Microsoft Support",
+ syndicatePartner: "Syndicate Partner",
+ breadthPartner: "Breadth Partner",
+ breadthPartnerDelegatedAdmin: "Breadth Partner (Delegated)",
+ resellerPartnerDelegatedAdmin: "Direct Reseller",
+ valueAddedResellerPartnerDelegatedAdmin: "Indirect Reseller",
+ unknownFutureValue: "Unknown",
};
diff --git a/src/components/CippSettings/CippGDAPResults.jsx b/src/components/CippSettings/CippGDAPResults.jsx
index 5c381668c78a..89897fd278d0 100644
--- a/src/components/CippSettings/CippGDAPResults.jsx
+++ b/src/components/CippSettings/CippGDAPResults.jsx
@@ -149,10 +149,6 @@ export const CippGDAPResults = (props) => {
}}
extendedInfo={[]}
>
-
- GDAP Details
-
-
{results?.Results?.GDAPIssues?.length > 0 && (
<>
{
}}
extendedInfo={[]}
>
-
- Permission Details
-
{results?.Results?.Links.length > 0 && (
{
const [tokens, setTokens] = useState(null);
@@ -12,7 +13,7 @@ export const CIPPDeploymentUpdateTokens = ({ formControl }) => {
// Get application ID information for the card header
const appId = ApiGetCall({
url: `/api/ExecListAppId`,
- queryKey: `ExecListAppId`,
+ queryKey: "listAppId",
waiting: true,
});
@@ -46,10 +47,79 @@ export const CIPPDeploymentUpdateTokens = ({ formControl }) => {
/>
}
>
-
+
Click the button to refresh the Graph token for your tenants using popup authentication.
- This method opens a popup window where you can sign in to your Microsoft account.
+ Use this to update your refresh token or change the logged in user. This method opens a
+ popup window where you can sign in to your Microsoft account.
+
+
+ Current Tenant Info
+
+
+ {(appId.isLoading || appId.isFetching) && (
+
+
+
+
+
+
+
+ )}
+ {!appId.isLoading && !appId.isFetching && appId?.data?.orgName && (
+
+
+
+
+
+
+
+ {appId.data.orgName}
+
+
+ {appId.data.tenantId}
+
+
+ {appId.data.authenticatedUserDisplayName && (
+
+
+
+
+
+ {appId.data.authenticatedUserDisplayName}
+
+
+ {appId.data.authenticatedUserPrincipalName}
+
+
+ )}
+
+
+ {appId.data.isPartnerTenant ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx
index 8f8683af405e..52f9722e6254 100644
--- a/src/components/CippWizard/CippTenantModeDeploy.jsx
+++ b/src/components/CippWizard/CippTenantModeDeploy.jsx
@@ -1,10 +1,22 @@
import { useEffect } from "react";
-import { Stack, Box, Typography, Link } from "@mui/material";
+import {
+ Stack,
+ Box,
+ Typography,
+ Link,
+ Chip,
+ Skeleton,
+ SvgIcon,
+ IconButton,
+ Tooltip,
+} from "@mui/material";
+import { Person, Apartment, Sync } from "@mui/icons-material";
import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton";
import { CippApiResults } from "../CippComponents/CippApiResults";
-import { ApiPostCall } from "../../api/ApiCall";
+import { ApiPostCall, ApiGetCall } from "../../api/ApiCall";
import { CippWizardStepButtons } from "./CippWizardStepButtons";
import { CippTenantTable } from "./CippTenantTable";
+import { getCippTranslation } from "../../utils/get-cipp-translation";
export const CippTenantModeDeploy = (props) => {
const { formControl, currentStep, onPreviousStep, onNextStep } = props;
@@ -13,8 +25,15 @@ export const CippTenantModeDeploy = (props) => {
required: true,
});
- const updateRefreshToken = ApiPostCall({ urlfromdata: true });
- const addTenant = ApiPostCall({ urlfromdata: true });
+ const updateRefreshToken = ApiPostCall({ urlfromdata: true, relatedQueryKeys: ["listAppId"] });
+ const addTenant = ApiPostCall({ urlfromdata: true, relatedQueryKeys: ["tenants-table"] });
+
+ // Get partner tenant info using the same API call as CIPPM365OAuthButton
+ const partnerTenantInfo = ApiGetCall({
+ url: `/api/ExecListAppId`,
+ queryKey: "listAppId",
+ waiting: true,
+ });
useEffect(() => {
if (updateRefreshToken.isSuccess) {
@@ -30,18 +49,28 @@ export const CippTenantModeDeploy = (props) => {
return (
-
-
{/* Partner Tenant (GDAP) */}
-
- Partner Tenant
-
+
+
+ Partner Tenant
+
+
+ partnerTenantInfo.refetch()}
+ disabled={partnerTenantInfo.isLoading}
+ >
+
+
+
+
Using GDAP is recommended for CIPP, however you can also authenticate to individual
- tenants. It is still highly recommended to connect to your partner tenant first, even if
- you are not a Microsoft CSP. This allows CIPP to send notifications, perform permission
- checks, and update permissions when required.
+ tenants. It is required to connect to your partner tenant first, even if you are not a
+ Microsoft CSP. This is where the multi-tenant App Registration (CIPP-SAM) is installed. It
+ also allows CIPP to send notifications, perform permission checks, and update permissions
+ when required.
Please remember to log onto a service account dedicated for CIPP. More info? Check out the{" "}
@@ -55,26 +84,142 @@ export const CippTenantModeDeploy = (props) => {
.
-
-
- {
- const updatedTokenData = {
- ...tokenData,
- tenantMode: "GDAP",
- };
- updateRefreshToken.mutate({
- url: "/api/ExecUpdateRefreshToken",
- data: updatedTokenData,
- });
+ {(partnerTenantInfo.isLoading || partnerTenantInfo.isFetching) && (
+
+
-
+ >
+
+
+
+
+
+
+
+
+
+ )}
+
+ {!partnerTenantInfo.isLoading &&
+ !partnerTenantInfo.isFetching &&
+ partnerTenantInfo?.data?.orgName && (
+
+
+
+
+
+
+
+
+
+ {partnerTenantInfo.data.orgName}
+
+
+ {partnerTenantInfo.data.tenantId}
+
+
+ {partnerTenantInfo.data.authenticatedUserDisplayName && (
+
+
+
+
+
+ {partnerTenantInfo.data.authenticatedUserDisplayName}
+
+
+ {partnerTenantInfo.data.authenticatedUserPrincipalName}
+
+
+ )}
+
+
+ {partnerTenantInfo.data.isPartnerTenant ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ )}
+
+ {!partnerTenantInfo.isLoading &&
+ !partnerTenantInfo.isFetching &&
+ !partnerTenantInfo?.data?.orgName && (
+
+
+
+
+ No partner tenant connected. Click the button below to authenticate with your
+ partner tenant.
+
+
+
+
+ )}
+
+
+ {
+ const updatedTokenData = {
+ ...tokenData,
+ tenantMode: "GDAP",
+ };
+ updateRefreshToken.mutate({
+ url: "/api/ExecUpdateRefreshToken",
+ data: updatedTokenData,
+ });
+ }}
+ buttonText={
+ partnerTenantInfo?.data?.orgName
+ ? "Change Partner Tenant"
+ : "Connect to Partner Tenant"
+ }
+ showSuccessAlert={false}
+ promptBeforeAuth={
+ partnerTenantInfo?.data?.orgName
+ ? `Are you sure you want to change the partner tenant from '${partnerTenantInfo?.data?.orgName}'? If you are trying to add another tenant, use the per-tenant authentication below.`
+ : false
+ }
+ scope="https://graph.microsoft.com/DelegatedPermissionGrant.ReadWrite.All https://graph.microsoft.com/Directory.ReadWrite.All https://graph.microsoft.com/AppRoleAssignment.ReadWrite.All offline_access profile openid"
+ />
+
+
{/* Per-Tenant */}
@@ -86,34 +231,65 @@ export const CippTenantModeDeploy = (props) => {
wrong tenant? Use the table below to remove it.
+ {!partnerTenantInfo?.data?.orgName && (
+
+
+ Please connect to your partner tenant first before adding separate tenants.
+
+
+ )}
+
- {
- const updatedTokenData = {
- ...tokenData,
- tenantMode: "perTenant",
- };
- addTenant.mutate({
- url: "/api/ExecAddTenant",
- data: updatedTokenData,
- });
- }}
- buttonText="Connect to Separate Tenants"
- showSuccessAlert={false}
- />
+
+ {!partnerTenantInfo?.data?.orgName && (
+
+ )}
+
+ {
+ if (!partnerTenantInfo?.data?.orgName) return;
+ const updatedTokenData = {
+ ...tokenData,
+ tenantMode: "perTenant",
+ };
+ addTenant.mutate({
+ url: "/api/ExecAddTenant",
+ data: updatedTokenData,
+ });
+ }}
+ buttonText="Connect to Separate Tenants"
+ showSuccessAlert={false}
+ scope="https://graph.microsoft.com/DelegatedPermissionGrant.ReadWrite.All https://graph.microsoft.com/Directory.ReadWrite.All https://graph.microsoft.com/AppRoleAssignment.ReadWrite.All offline_access profile openid"
+ />
+
+
-
+
+
+
+
+