Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
182 changes: 137 additions & 45 deletions src/components/CippComponents/CIPPM365OAuthButton.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -32,13 +36,10 @@ export const CIPPM365OAuthButton = ({

const appIdInfo = ApiGetCall({
url: `/api/ExecListAppId`,
queryKey: "listAppId",
waiting: true,
});

useEffect(() => {
appIdInfo.refetch();
}, []);

const handleCloseError = () => {
setAuthError(null);
};
Expand All @@ -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
Expand All @@ -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();

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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();

Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -550,9 +605,9 @@ export const CIPPM365OAuthButton = ({
<div>
{!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,
) && (
<Alert severity="warning" sx={{ mt: 1 }}>
The Application ID is not valid. Please check your configuration.
Expand Down Expand Up @@ -653,6 +708,30 @@ export const CIPPM365OAuthButton = ({
) : null}
</Box>
)}

{promptBeforeAuth !== false && (
<CippApiDialog
title={"Microsoft 365 Authentication"}
createDialog={{
open: promptDialog.open,
handleClose: () => setPromptDialog({ open: false }),
}}
api={{
type: "POST",
confirmText: promptBeforeAuth,
noConfirm: false,
customFunction: () => {
setPromptDialog({ open: false });
const authFunction = useDeviceCode
? handleDeviceCodeAuthentication
: handleMsalAuthentication;
authFunction();
},
}}
fields={[]}
/>
)}

<Button
variant="contained"
disabled={
Expand All @@ -661,22 +740,35 @@ 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}
onClick={() => {
if (promptBeforeAuth !== false) {
setPromptDialog({ open: true });
} else {
const authFunction = useDeviceCode
? handleDeviceCodeAuthentication
: handleMsalAuthentication;
authFunction();
}
}}
color="primary"
startIcon={
authInProgress || codeRetrievalInProgress ? (
<CircularProgress size="1rem" color="inherit" />
) : tokens.accessToken ? (
<Refresh />
) : (
<Microsoft />
)
}
>
{authInProgress || codeRetrievalInProgress ? (
<>
<CircularProgress size="1rem" color="inherit" sx={{ mr: 1 }} />
Authenticating...
</>
) : deviceCodeInfo && useDeviceCode ? (
"Authenticate with Code"
) : (
buttonText
)}
{authInProgress || codeRetrievalInProgress
? "Authenticating..."
: deviceCodeInfo && useDeviceCode
? "Authenticate with Code"
: buttonText}
</Button>
</div>
);
Expand Down
7 changes: 7 additions & 0 deletions src/components/CippComponents/CippTranslations.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};
4 changes: 0 additions & 4 deletions src/components/CippSettings/CippGDAPResults.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,6 @@ export const CippGDAPResults = (props) => {
}}
extendedInfo={[]}
>
<Typography variant="h4" sx={{ mx: 3 }}>
GDAP Details
</Typography>

{results?.Results?.GDAPIssues?.length > 0 && (
<>
<CippDataTable
Expand Down
3 changes: 0 additions & 3 deletions src/components/CippSettings/CippPermissionResults.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,6 @@ export const CippPermissionResults = (props) => {
}}
extendedInfo={[]}
>
<Typography variant="h4" sx={{ mx: 3 }}>
Permission Details
</Typography>
{results?.Results?.Links.length > 0 && (
<CippPropertyListCard
title="Documentation"
Expand Down
Loading