From 4c9366a6d7d2aa790fc9c7d557c289e0bc1c4550 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 24 Jan 2026 22:26:08 -0500 Subject: [PATCH 01/15] Enhance partner tenant info display in deployment wizard Added partner tenant information panel to CippTenantModeDeploy, showing organization, user, and partner type details. Updated CIPPM365OAuthButton to improve button icon logic and validation. Extended CippTranslations with partner type labels for better UI clarity. --- .../CippComponents/CIPPM365OAuthButton.jsx | 42 +++--- .../CippComponents/CippTranslations.jsx | 7 + .../CippWizard/CippTenantModeDeploy.jsx | 127 ++++++++++++++---- 3 files changed, 131 insertions(+), 45 deletions(-) diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx index 88e517a2139d..af192ad95421 100644 --- a/src/components/CippComponents/CIPPM365OAuthButton.jsx +++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx @@ -1,5 +1,6 @@ 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"; @@ -32,6 +33,7 @@ export const CIPPM365OAuthButton = ({ const appIdInfo = ApiGetCall({ url: `/api/ExecListAppId`, + queryKey: "listAppId", waiting: true, }); @@ -66,8 +68,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(); @@ -129,7 +131,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 +157,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(); @@ -327,7 +329,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 @@ -550,9 +552,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. @@ -661,22 +663,26 @@ export const CIPPM365OAuthButton = ({ codeRetrievalInProgress || (!applicationId && !/^[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, )) } onClick={useDeviceCode ? handleDeviceCodeAuthentication : handleMsalAuthentication} color="primary" + startIcon={ + authInProgress || codeRetrievalInProgress ? ( + + ) : tokens.accessToken ? ( + + ) : ( + + ) + } > - {authInProgress || codeRetrievalInProgress ? ( - <> - - Authenticating... - - ) : deviceCodeInfo && useDeviceCode ? ( - "Authenticate with Code" - ) : ( - buttonText - )} + {authInProgress || codeRetrievalInProgress + ? "Authenticating..." + : deviceCodeInfo && useDeviceCode + ? "Authenticate with Code" + : buttonText}
); diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx index 99d46a6e5182..eaffa47f71d3 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: "Reseller", + valueAddedResellerPartnerDelegatedAdmin: "Indirect Reseller", + unknownFutureValue: "Unknown", }; diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx index 8f8683af405e..477a0ed36ec2 100644 --- a/src/components/CippWizard/CippTenantModeDeploy.jsx +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -1,10 +1,12 @@ import { useEffect } from "react"; -import { Stack, Box, Typography, Link } from "@mui/material"; +import { Stack, Box, Typography, Link, Chip, Skeleton, SvgIcon } from "@mui/material"; +import { Person, Apartment } 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; @@ -16,6 +18,13 @@ export const CippTenantModeDeploy = (props) => { const updateRefreshToken = ApiPostCall({ urlfromdata: true }); const addTenant = ApiPostCall({ urlfromdata: true }); + // 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) { formControl.setValue("GDAPAuth", true); @@ -55,23 +64,85 @@ export const CippTenantModeDeploy = (props) => { . - - - { - const updatedTokenData = { - ...tokenData, - tenantMode: "GDAP", - }; - updateRefreshToken.mutate({ - url: "/api/ExecUpdateRefreshToken", - data: updatedTokenData, - }); + {partnerTenantInfo.isLoading && ( + + + + )} + + {partnerTenantInfo?.data?.orgName && ( + + - + > + + + + + + + + {partnerTenantInfo.data.orgName} + + + {partnerTenantInfo.data.tenantId} + + + {partnerTenantInfo.data.authenticatedUserDisplayName && ( + + + + + + {partnerTenantInfo.data.authenticatedUserDisplayName} + + + {partnerTenantInfo.data.authenticatedUserPrincipalName} + + + )} + + + {partnerTenantInfo.data.isPartnerTenant ? ( + + ) : ( + + )} + + + + + )} + + + { + const updatedTokenData = { + ...tokenData, + tenantMode: "GDAP", + }; + updateRefreshToken.mutate({ + url: "/api/ExecUpdateRefreshToken", + data: updatedTokenData, + }); + }} + buttonText={ + partnerTenantInfo?.data?.orgName + ? "Change Partner Tenant" + : "Connect to Partner Tenant (Recommended)" + } + showSuccessAlert={false} + /> @@ -105,15 +176,17 @@ export const CippTenantModeDeploy = (props) => { - + + + Date: Sat, 24 Jan 2026 22:26:16 -0500 Subject: [PATCH 02/15] Remove redundant section headers from results components Eliminated the 'GDAP Details' and 'Permission Details' Typography headers from CippGDAPResults and CippPermissionResults components to streamline the UI and reduce unnecessary repetition. --- src/components/CippSettings/CippGDAPResults.jsx | 4 ---- src/components/CippSettings/CippPermissionResults.jsx | 3 --- 2 files changed, 7 deletions(-) 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 && ( Date: Sat, 24 Jan 2026 22:42:09 -0500 Subject: [PATCH 03/15] Update react-dom and remove unused React import Upgraded react-dom from 19.1.1 to 19.2.3 and updated scheduler dependency accordingly. Also removed an unused import of React from CippTenantTable.jsx. --- package.json | 2 +- src/components/CippWizard/CippTenantTable.jsx | 1 - yarn.lock | 18 +++++++++--------- 3 files changed, 10 insertions(+), 11 deletions(-) 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/CippWizard/CippTenantTable.jsx b/src/components/CippWizard/CippTenantTable.jsx index 81c1ae4cde73..72cfe71f64f5 100644 --- a/src/components/CippWizard/CippTenantTable.jsx +++ b/src/components/CippWizard/CippTenantTable.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { Button, SvgIcon } from "@mui/material"; import { CippTablePage } from "../CippComponents/CippTablePage.jsx"; import { Sync, Block, PlayArrow, RestartAlt, Delete, Add } from "@mui/icons-material"; diff --git a/yarn.lock b/yarn.lock index 4c905da26818..6f8601e5e565 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6616,12 +6616,12 @@ react-copy-to-clipboard@^5.1.0: copy-to-clipboard "^3.3.1" prop-types "^15.8.1" -react-dom@19.1.1: - version "19.1.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.1.tgz#2daa9ff7f3ae384aeb30e76d5ee38c046dc89893" - integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw== +react-dom@19.2.3: + version "19.2.3" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17" + integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg== dependencies: - scheduler "^0.26.0" + scheduler "^0.27.0" react-draggable@^4.0.3, react-draggable@^4.4.6: version "4.5.0" @@ -7166,10 +7166,10 @@ scheduler@0.25.0-rc-603e6108-20241029: resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz#684dd96647e104d23e0d29a37f18937daf82df19" integrity sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA== -scheduler@^0.26.0: - version "0.26.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337" - integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== section-matter@^1.0.0: version "1.0.0" From 4dd5d07e8ab42ba7cc7645e46f3f71bf3353f0c1 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 24 Jan 2026 22:50:53 -0500 Subject: [PATCH 04/15] Improve partner tenant connection UX in CippTenantModeDeploy Adds a refresh button for partner tenant info, updates guidance text to clarify partner tenant connection requirements, and improves loading and error states. Also disables connecting separate tenants until a partner tenant is connected, enhancing user flow and preventing misconfiguration. --- .../CippWizard/CippTenantModeDeploy.jsx | 212 +++++++++++++----- 1 file changed, 153 insertions(+), 59 deletions(-) diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx index 477a0ed36ec2..0b56cd37fd65 100644 --- a/src/components/CippWizard/CippTenantModeDeploy.jsx +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -1,6 +1,16 @@ import { useEffect } from "react"; -import { Stack, Box, Typography, Link, Chip, Skeleton, SvgIcon } from "@mui/material"; -import { Person, Apartment } from "@mui/icons-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, ApiGetCall } from "../../api/ApiCall"; @@ -43,14 +53,26 @@ export const CippTenantModeDeploy = (props) => { {/* 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{" "} @@ -64,13 +86,7 @@ export const CippTenantModeDeploy = (props) => { . - {partnerTenantInfo.isLoading && ( - - - - )} - - {partnerTenantInfo?.data?.orgName && ( + {(partnerTenantInfo.isLoading || partnerTenantInfo.isFetching) && ( { }} > - - - - - - - {partnerTenantInfo.data.orgName} - - - {partnerTenantInfo.data.tenantId} - - - {partnerTenantInfo.data.authenticatedUserDisplayName && ( + + + + + + + + + )} + + {!partnerTenantInfo.isLoading && + !partnerTenantInfo.isFetching && + partnerTenantInfo?.data?.orgName && ( + + + + - + - {partnerTenantInfo.data.authenticatedUserDisplayName} + {partnerTenantInfo.data.orgName} - {partnerTenantInfo.data.authenticatedUserPrincipalName} + {partnerTenantInfo.data.tenantId} - )} + {partnerTenantInfo.data.authenticatedUserDisplayName && ( + + + + + + {partnerTenantInfo.data.authenticatedUserDisplayName} + + + {partnerTenantInfo.data.authenticatedUserPrincipalName} + + + )} + + + {partnerTenantInfo.data.isPartnerTenant ? ( + + ) : ( + + )} + + + + )} + + {!partnerTenantInfo.isLoading && + !partnerTenantInfo.isFetching && + !partnerTenantInfo?.data?.orgName && ( + + - {partnerTenantInfo.data.isPartnerTenant ? ( - - ) : ( - - )} + + No partner tenant connected. Click the button below to authenticate with your + partner tenant. + - + - - )} + )} { buttonText={ partnerTenantInfo?.data?.orgName ? "Change Partner Tenant" - : "Connect to Partner Tenant (Recommended)" + : "Connect to Partner Tenant" } showSuccessAlert={false} /> @@ -157,22 +225,48 @@ 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} + /> + + From c7e527fbb38094cae0607c20dcf1a798f9272268 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 24 Jan 2026 23:17:49 -0500 Subject: [PATCH 05/15] Add retry logic for AADSTS650051 in token exchange Implements retry mechanism with exponential backoff for the AADSTS650051 error (service principal already exists) during the token exchange process in CIPPM365OAuthButton. This improves reliability when encountering transient Azure AD errors. --- .../CippComponents/CIPPM365OAuthButton.jsx | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx index af192ad95421..9230b5f52f57 100644 --- a/src/components/CippComponents/CIPPM365OAuthButton.jsx +++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx @@ -358,20 +358,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) { From 37f8b1889fe27dcbdc9b2c5a74a89025b4e28730 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 24 Jan 2026 23:24:19 -0500 Subject: [PATCH 06/15] Reorder CippApiResults components in tenant deploy Moved the CippApiResults for updateRefreshToken and addTenant to better align with their relevant sections in the CippTenantModeDeploy component. This improves the logical grouping and clarity of API result displays. --- src/components/CippWizard/CippTenantModeDeploy.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx index 0b56cd37fd65..aed2102a3bda 100644 --- a/src/components/CippWizard/CippTenantModeDeploy.jsx +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -49,8 +49,6 @@ export const CippTenantModeDeploy = (props) => { return ( - - {/* Partner Tenant (GDAP) */} @@ -214,6 +212,8 @@ export const CippTenantModeDeploy = (props) => { + + {/* Per-Tenant */} @@ -270,6 +270,8 @@ export const CippTenantModeDeploy = (props) => { + + Date: Sat, 24 Jan 2026 23:25:41 -0500 Subject: [PATCH 07/15] Update CippTranslations.jsx --- src/components/CippComponents/CippTranslations.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx index eaffa47f71d3..af96b19cd027 100644 --- a/src/components/CippComponents/CippTranslations.jsx +++ b/src/components/CippComponents/CippTranslations.jsx @@ -56,7 +56,7 @@ export const CippTranslations = { syndicatePartner: "Syndicate Partner", breadthPartner: "Breadth Partner", breadthPartnerDelegatedAdmin: "Breadth Partner (Delegated)", - resellerPartnerDelegatedAdmin: "Reseller", + resellerPartnerDelegatedAdmin: "Direct Reseller", valueAddedResellerPartnerDelegatedAdmin: "Indirect Reseller", unknownFutureValue: "Unknown", }; From 4932a136d5f1d302c3834dc9e0f75ac3257d33be Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 24 Jan 2026 23:38:48 -0500 Subject: [PATCH 08/15] Improve appId refetch logic and add retry for AADSTS650051 Refactors appId refetching to only occur when applicationId is not already present, reducing unnecessary network calls. Adds retry logic with exponential backoff for the AADSTS650051 error during MSAL authentication, improving user experience when service principal consent issues occur. --- .../CippComponents/CIPPM365OAuthButton.jsx | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx index 9230b5f52f57..9ee2076d1b05 100644 --- a/src/components/CippComponents/CIPPM365OAuthButton.jsx +++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx @@ -37,10 +37,6 @@ export const CIPPM365OAuthButton = ({ waiting: true, }); - useEffect(() => { - appIdInfo.refetch(); - }, []); - const handleCloseError = () => { setAuthError(null); }; @@ -57,8 +53,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 @@ -97,8 +95,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 @@ -265,7 +265,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); @@ -279,10 +281,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 @@ -527,7 +531,24 @@ 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", From 8c23378bd31c067b69b25da89f871ae6ee167f6f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 24 Jan 2026 23:56:30 -0500 Subject: [PATCH 09/15] Add queryKey props to tenant components Set relatedQueryKeys for ApiPostCall in CippTenantModeDeploy to improve cache management. Added queryKey prop to CippTablePage in CippTenantTable for more consistent data fetching. --- src/components/CippWizard/CippTenantModeDeploy.jsx | 4 ++-- src/components/CippWizard/CippTenantTable.jsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx index aed2102a3bda..541ebde69201 100644 --- a/src/components/CippWizard/CippTenantModeDeploy.jsx +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -25,8 +25,8 @@ 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: ["listTenants"] }); // Get partner tenant info using the same API call as CIPPM365OAuthButton const partnerTenantInfo = ApiGetCall({ diff --git a/src/components/CippWizard/CippTenantTable.jsx b/src/components/CippWizard/CippTenantTable.jsx index 72cfe71f64f5..8494aa558e19 100644 --- a/src/components/CippWizard/CippTenantTable.jsx +++ b/src/components/CippWizard/CippTenantTable.jsx @@ -115,6 +115,7 @@ export const CippTenantTable = ({ <> Date: Sun, 25 Jan 2026 00:16:37 -0500 Subject: [PATCH 10/15] Limit separate tenant scopes Passed a scope prop with required Microsoft Graph permissions to the ConnectToTenantsButton component in CippTenantModeDeploy. This enables the button to request the minimum necessary permissions during authentication. --- src/components/CippWizard/CippTenantModeDeploy.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx index 541ebde69201..56edf1907513 100644 --- a/src/components/CippWizard/CippTenantModeDeploy.jsx +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -264,6 +264,7 @@ export const CippTenantModeDeploy = (props) => { }} 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" /> From 111465cef592d5b2e8b56268b6a15e492ecf01a4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 25 Jan 2026 01:42:00 -0500 Subject: [PATCH 11/15] Update CippTenantModeDeploy.jsx --- src/components/CippWizard/CippTenantModeDeploy.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx index 56edf1907513..4365cc2a8463 100644 --- a/src/components/CippWizard/CippTenantModeDeploy.jsx +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -26,7 +26,7 @@ export const CippTenantModeDeploy = (props) => { }); const updateRefreshToken = ApiPostCall({ urlfromdata: true, relatedQueryKeys: ["listAppId"] }); - const addTenant = ApiPostCall({ urlfromdata: true, relatedQueryKeys: ["listTenants"] }); + const addTenant = ApiPostCall({ urlfromdata: true, relatedQueryKeys: ["tenants-table"] }); // Get partner tenant info using the same API call as CIPPM365OAuthButton const partnerTenantInfo = ApiGetCall({ From bd9b260f1cf3a605c5bc230de800f217dc5075e9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 25 Jan 2026 01:48:57 -0500 Subject: [PATCH 12/15] Add scope prop to AuthButton in CippTenantModeDeploy Sets required Microsoft Graph API scopes on the AuthButton component to enable necessary permissions for delegated permission grants, directory, and app role assignments. --- src/components/CippWizard/CippTenantModeDeploy.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx index 4365cc2a8463..b63259d456dd 100644 --- a/src/components/CippWizard/CippTenantModeDeploy.jsx +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -208,6 +208,7 @@ export const CippTenantModeDeploy = (props) => { : "Connect to Partner Tenant" } 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" /> From df7a1a71985a0ac4a0de7639b75272ba6eaf8ee2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 25 Jan 2026 02:09:20 -0500 Subject: [PATCH 13/15] Add authentication prompt to M365 OAuth button Introduces a promptBeforeAuth prop to CIPPM365OAuthButton, displaying a confirmation dialog before starting authentication. Updates CippTenantModeDeploy to use this feature, prompting users when changing the partner tenant to prevent accidental changes. --- .../CippComponents/CIPPM365OAuthButton.jsx | 47 +++++++++++++++++-- .../CippWizard/CippTenantModeDeploy.jsx | 5 ++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx index 9ee2076d1b05..9055fd2b2d04 100644 --- a/src/components/CippComponents/CIPPM365OAuthButton.jsx +++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx @@ -3,6 +3,7 @@ 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, @@ -15,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, @@ -542,9 +545,12 @@ export const CIPPM365OAuthButton = ({ setAuthInProgress(false); // Wait before retrying (exponential backoff) - setTimeout(() => { - handleMsalAuthentication(retryCount + 1); - }, 2000 * (retryCount + 1)); + setTimeout( + () => { + handleMsalAuthentication(retryCount + 1); + }, + 2000 * (retryCount + 1), + ); return; } @@ -699,6 +705,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={[]} + /> + )} +