Refactor: Enhance Microsoft Graph Federation error handling and logging
- Improved logging for `MicrosoftGraphService` methods to provide detailed insights into federation configuration processes. - Refactored error handling for domain federation configuration, including specific checks for authorization and resource errors. - Streamlined federation flow logic and made it more resilient to exceptions. - Updated UI components to handle configuration states more effectively and added button functionality for failed connections. - Expanded test coverage for Microsoft Graph Federation scenarios, including authorization failures and domain validation. - masked the sensitive fields
This commit is contained in:
parent
1a7134419c
commit
215d3cad7f
@ -6,71 +6,13 @@
|
|||||||
|
|
||||||
use App\Data\MicrosoftGraph\MicrosoftGraphCredentialsData;
|
use App\Data\MicrosoftGraph\MicrosoftGraphCredentialsData;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\{Http, Log};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to manage Microsoft Graph API domain federation configuration.
|
* Service to manage Microsoft Graph API domain federation configuration.
|
||||||
*/
|
*/
|
||||||
final class MicrosoftGraphService
|
final class MicrosoftGraphService
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Get an access token using Client Credentials grant flow.
|
|
||||||
*
|
|
||||||
* @return array{token: ?string, transaction: array}
|
|
||||||
*/
|
|
||||||
public function getAccessToken(MicrosoftGraphCredentialsData $credentials): array
|
|
||||||
{
|
|
||||||
$url = "https://login.microsoftonline.com/{$credentials->tenantId}/oauth2/v2.0/token";
|
|
||||||
|
|
||||||
$body = [
|
|
||||||
'client_id' => $credentials->clientId,
|
|
||||||
'client_secret' => $credentials->clientSecret,
|
|
||||||
'scope' => 'https://graph.microsoft.com/.default',
|
|
||||||
'grant_type' => 'client_credentials',
|
|
||||||
];
|
|
||||||
|
|
||||||
$requestHeaders = [
|
|
||||||
'Content-Type' => 'application/x-www-form-urlencoded',
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = Http::asForm()->post($url, $body);
|
|
||||||
|
|
||||||
$transaction = [
|
|
||||||
'name' => 'OAuth 2.0 Get Access Token',
|
|
||||||
'url' => $url,
|
|
||||||
'method' => 'POST',
|
|
||||||
'request_headers' => $requestHeaders,
|
|
||||||
'request_body' => collect($body)->except('client_secret')->put('client_secret', '********')->toArray(),
|
|
||||||
'status' => $response->status(),
|
|
||||||
'response_headers' => $response->headers(),
|
|
||||||
'response_body' => $response->json(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($response->successful()) {
|
|
||||||
return [
|
|
||||||
'token' => $response->json('access_token'),
|
|
||||||
'transaction' => $transaction,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$transaction = [
|
|
||||||
'name' => 'OAuth 2.0 Get Access Token',
|
|
||||||
'url' => $url,
|
|
||||||
'method' => 'POST',
|
|
||||||
'request_headers' => $requestHeaders,
|
|
||||||
'request_body' => collect($body)->except('client_secret')->put('client_secret', '********')->toArray(),
|
|
||||||
'status' => 'ERROR',
|
|
||||||
'response_body' => ['error' => $e->getMessage()],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'token' => null,
|
|
||||||
'transaction' => $transaction,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve federation configuration for the domain.
|
* Retrieve federation configuration for the domain.
|
||||||
*
|
*
|
||||||
@ -78,10 +20,16 @@ public function getAccessToken(MicrosoftGraphCredentialsData $credentials): arra
|
|||||||
*/
|
*/
|
||||||
public function getFederationConfig(MicrosoftGraphCredentialsData $credentials): array
|
public function getFederationConfig(MicrosoftGraphCredentialsData $credentials): array
|
||||||
{
|
{
|
||||||
|
Log::info('MicrosoftGraphService: Initiating getFederationConfig for domain.', [
|
||||||
|
'domain_name' => $credentials->domainName,
|
||||||
|
]);
|
||||||
|
|
||||||
$auth = $this->getAccessToken($credentials);
|
$auth = $this->getAccessToken($credentials);
|
||||||
$transactions = [$auth['transaction']];
|
$transactions = [$auth['transaction']];
|
||||||
|
|
||||||
if (! $auth['token']) {
|
if (! $auth['token']) {
|
||||||
|
Log::error('MicrosoftGraphService: Failed to retrieve access token, aborting federation config retrieval.');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Authentication with Microsoft Graph failed.',
|
'message' => 'Authentication with Microsoft Graph failed.',
|
||||||
@ -96,9 +44,17 @@ public function getFederationConfig(MicrosoftGraphCredentialsData $credentials):
|
|||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Log::info('MicrosoftGraphService: Sending GET request to Microsoft Graph for federation configuration.', [
|
||||||
|
'url' => $url,
|
||||||
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = Http::withToken($auth['token'])->get($url);
|
$response = Http::withToken($auth['token'])->get($url);
|
||||||
|
|
||||||
|
Log::info('MicrosoftGraphService: Received federation configuration response.', [
|
||||||
|
'status_code' => $response->status(),
|
||||||
|
]);
|
||||||
|
|
||||||
$transactions[] = [
|
$transactions[] = [
|
||||||
'name' => 'Get Federation Configuration',
|
'name' => 'Get Federation Configuration',
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
@ -113,16 +69,66 @@ public function getFederationConfig(MicrosoftGraphCredentialsData $credentials):
|
|||||||
if ($response->successful()) {
|
if ($response->successful()) {
|
||||||
/** @var array $value */
|
/** @var array $value */
|
||||||
$value = $response->json('value', []);
|
$value = $response->json('value', []);
|
||||||
|
$hasConfig = ! empty($value);
|
||||||
|
|
||||||
|
Log::info('MicrosoftGraphService: Federation configuration query completed successfully.', [
|
||||||
|
'config_found' => $hasConfig,
|
||||||
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'config' => ! empty($value) ? $value[0] : null,
|
'config' => $hasConfig ? $value[0] : null,
|
||||||
'transactions' => $transactions,
|
'transactions' => $transactions,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var string $errorMsg */
|
// Handle standard 404 Request_ResourceNotFound
|
||||||
$errorMsg = $response->json('error.message', 'Failed to retrieve federation config.');
|
$errorJson = $response->json('error');
|
||||||
|
$errorCode = $errorJson['code'] ?? 'Unknown';
|
||||||
|
$errorMsg = $errorJson['message'] ?? 'Failed to retrieve federation config.';
|
||||||
|
$innerError = $errorJson['innerError'] ?? null;
|
||||||
|
|
||||||
|
if ('Authorization_RequestDenied' === $errorCode) {
|
||||||
|
$errorMsg = "Insufficient privileges to complete the operation. Please verify that your Microsoft Entra application registration has been granted the 'Domain-InternalFederation.ReadWrite.All' Application Permission, and that a Global Admin has clicked 'Grant admin consent' in the Entra ID portal.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (404 === $response->status() && 'Request_ResourceNotFound' === $errorCode) {
|
||||||
|
// If it is a domain-not-found error rather than federationConfiguration-not-found:
|
||||||
|
if (
|
||||||
|
(str_contains(mb_strtolower($errorMsg), 'does not exist') && ! str_contains(mb_strtolower($errorMsg), 'federationconfiguration')) ||
|
||||||
|
str_contains(mb_strtolower($errorMsg), '\'{0}\'')
|
||||||
|
) {
|
||||||
|
Log::error('MicrosoftGraphService: The specified domain does not exist in Microsoft Entra ID.', [
|
||||||
|
'domain' => $credentials->domainName,
|
||||||
|
'error_message' => $errorMsg,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => "The domain '{$credentials->domainName}' does not exist or is not verified in your Microsoft Entra ID tenant. Please add and verify the domain in the Entra portal first.",
|
||||||
|
'config' => null,
|
||||||
|
'transactions' => $transactions,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('MicrosoftGraphService: Domain is currently in a Managed state (no federation configuration exists).', [
|
||||||
|
'status_code' => 404,
|
||||||
|
'error_code' => $errorCode,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'config' => null,
|
||||||
|
'transactions' => $transactions,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::error('MicrosoftGraphService: Microsoft Graph API returned an error response.', [
|
||||||
|
'status_code' => $response->status(),
|
||||||
|
'error_code' => $errorCode,
|
||||||
|
'error_message' => $errorMsg,
|
||||||
|
'inner_error' => $innerError,
|
||||||
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
@ -131,6 +137,11 @@ public function getFederationConfig(MicrosoftGraphCredentialsData $credentials):
|
|||||||
'transactions' => $transactions,
|
'transactions' => $transactions,
|
||||||
];
|
];
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
Log::error('MicrosoftGraphService: Exception during federation config retrieval.', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
$transactions[] = [
|
$transactions[] = [
|
||||||
'name' => 'Get Federation Configuration',
|
'name' => 'Get Federation Configuration',
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
@ -150,6 +161,90 @@ public function getFederationConfig(MicrosoftGraphCredentialsData $credentials):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an access token using Client Credentials grant flow.
|
||||||
|
*
|
||||||
|
* @return array{token: ?string, transaction: array}
|
||||||
|
*/
|
||||||
|
public function getAccessToken(MicrosoftGraphCredentialsData $credentials): array
|
||||||
|
{
|
||||||
|
$url = "https://login.microsoftonline.com/{$credentials->tenantId}/oauth2/v2.0/token";
|
||||||
|
|
||||||
|
Log::info('MicrosoftGraphService: Starting OAuth 2.0 Client Credentials token acquisition.', [
|
||||||
|
'url' => $url,
|
||||||
|
'tenant_id' => $credentials->tenantId,
|
||||||
|
'client_id' => $credentials->clientId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = [
|
||||||
|
'client_id' => $credentials->clientId,
|
||||||
|
'client_secret' => $credentials->clientSecret,
|
||||||
|
'scope' => 'https://graph.microsoft.com/.default',
|
||||||
|
'grant_type' => 'client_credentials',
|
||||||
|
];
|
||||||
|
|
||||||
|
$requestHeaders = [
|
||||||
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log::debug('MicrosoftGraphService: Sending POST token request to login.microsoftonline.com.');
|
||||||
|
$response = Http::asForm()->post($url, $body);
|
||||||
|
|
||||||
|
Log::info('MicrosoftGraphService: OAuth 2.0 token response received.', [
|
||||||
|
'status_code' => $response->status(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$transaction = [
|
||||||
|
'name' => 'OAuth 2.0 Get Access Token',
|
||||||
|
'url' => $url,
|
||||||
|
'method' => 'POST',
|
||||||
|
'request_headers' => $requestHeaders,
|
||||||
|
'request_body' => collect($body)->except('client_secret')->put('client_secret', '********')->toArray(),
|
||||||
|
'status' => $response->status(),
|
||||||
|
'response_headers' => $response->headers(),
|
||||||
|
'response_body' => $response->json(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($response->successful()) {
|
||||||
|
$accessToken = $response->json('access_token');
|
||||||
|
Log::info('MicrosoftGraphService: OAuth 2.0 token retrieved successfully.', [
|
||||||
|
'token_length' => mb_strlen($accessToken),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'token' => $accessToken,
|
||||||
|
'transaction' => $transaction,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::error('MicrosoftGraphService: OAuth 2.0 token request failed.', [
|
||||||
|
'status' => $response->status(),
|
||||||
|
'error_payload' => $response->json(),
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('MicrosoftGraphService: Exception encountered during token retrieval.', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$transaction = [
|
||||||
|
'name' => 'OAuth 2.0 Get Access Token',
|
||||||
|
'url' => $url,
|
||||||
|
'method' => 'POST',
|
||||||
|
'request_headers' => $requestHeaders,
|
||||||
|
'request_body' => collect($body)->except('client_secret')->put('client_secret', '********')->toArray(),
|
||||||
|
'status' => 'ERROR',
|
||||||
|
'response_body' => ['error' => $e->getMessage()],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'token' => null,
|
||||||
|
'transaction' => $transaction,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect/Federate the domain.
|
* Connect/Federate the domain.
|
||||||
*
|
*
|
||||||
@ -163,10 +258,20 @@ public function connectFederation(
|
|||||||
string $activeSignInUri,
|
string $activeSignInUri,
|
||||||
string $signOutUri
|
string $signOutUri
|
||||||
): array {
|
): array {
|
||||||
|
Log::info('MicrosoftGraphService: Initiating connectFederation.', [
|
||||||
|
'domain_name' => $credentials->domainName,
|
||||||
|
'issuer_uri' => $issuerUri,
|
||||||
|
'passive_signin_uri' => $passiveSignInUri,
|
||||||
|
'active_signin_uri' => $activeSignInUri,
|
||||||
|
'signout_uri' => $signOutUri,
|
||||||
|
]);
|
||||||
|
|
||||||
$auth = $this->getAccessToken($credentials);
|
$auth = $this->getAccessToken($credentials);
|
||||||
$transactions = [$auth['transaction']];
|
$transactions = [$auth['transaction']];
|
||||||
|
|
||||||
if (! $auth['token']) {
|
if (! $auth['token']) {
|
||||||
|
Log::error('MicrosoftGraphService: Failed to retrieve access token for connectFederation.');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Authentication with Microsoft Graph failed.',
|
'message' => 'Authentication with Microsoft Graph failed.',
|
||||||
@ -176,8 +281,16 @@ public function connectFederation(
|
|||||||
|
|
||||||
// Check and delete existing configuration to ensure a clean setup
|
// Check and delete existing configuration to ensure a clean setup
|
||||||
$checkUrl = "https://graph.microsoft.com/v1.0/domains/{$credentials->domainName}/federationConfiguration";
|
$checkUrl = "https://graph.microsoft.com/v1.0/domains/{$credentials->domainName}/federationConfiguration";
|
||||||
|
Log::info('MicrosoftGraphService: Checking for pre-existing federation configuration before connecting.', [
|
||||||
|
'check_url' => $checkUrl,
|
||||||
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$checkRes = Http::withToken($auth['token'])->get($checkUrl);
|
$checkRes = Http::withToken($auth['token'])->get($checkUrl);
|
||||||
|
Log::info('MicrosoftGraphService: Pre-check status response received.', [
|
||||||
|
'status_code' => $checkRes->status(),
|
||||||
|
]);
|
||||||
|
|
||||||
$transactions[] = [
|
$transactions[] = [
|
||||||
'name' => 'Check Existing Federation Configuration',
|
'name' => 'Check Existing Federation Configuration',
|
||||||
'url' => $checkUrl,
|
'url' => $checkUrl,
|
||||||
@ -193,7 +306,16 @@ public function connectFederation(
|
|||||||
/** @var string $existingId */
|
/** @var string $existingId */
|
||||||
$existingId = $configs[0]['id'];
|
$existingId = $configs[0]['id'];
|
||||||
$deleteUrl = "https://graph.microsoft.com/v1.0/domains/{$credentials->domainName}/federationConfiguration/{$existingId}";
|
$deleteUrl = "https://graph.microsoft.com/v1.0/domains/{$credentials->domainName}/federationConfiguration/{$existingId}";
|
||||||
|
Log::info('MicrosoftGraphService: Pre-existing configuration detected. Triggering deletion to clean state.', [
|
||||||
|
'existing_id' => $existingId,
|
||||||
|
'delete_url' => $deleteUrl,
|
||||||
|
]);
|
||||||
|
|
||||||
$deleteRes = Http::withToken($auth['token'])->delete($deleteUrl);
|
$deleteRes = Http::withToken($auth['token'])->delete($deleteUrl);
|
||||||
|
Log::info('MicrosoftGraphService: Pre-existing configuration deletion response.', [
|
||||||
|
'status_code' => $deleteRes->status(),
|
||||||
|
]);
|
||||||
|
|
||||||
$transactions[] = [
|
$transactions[] = [
|
||||||
'name' => 'Delete Pre-existing Federation Configuration',
|
'name' => 'Delete Pre-existing Federation Configuration',
|
||||||
'url' => $deleteUrl,
|
'url' => $deleteUrl,
|
||||||
@ -203,15 +325,31 @@ public function connectFederation(
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (! $deleteRes->successful()) {
|
if (! $deleteRes->successful()) {
|
||||||
|
Log::error('MicrosoftGraphService: Failed to delete pre-existing federation configuration.', [
|
||||||
|
'status_code' => $deleteRes->status(),
|
||||||
|
'response_body' => $deleteRes->json(),
|
||||||
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Failed to delete pre-existing federation configuration.',
|
'message' => 'Failed to delete pre-existing federation configuration.',
|
||||||
'transactions' => $transactions,
|
'transactions' => $transactions,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
Log::info('MicrosoftGraphService: Pre-existing federation configuration deleted successfully.');
|
||||||
|
} else {
|
||||||
|
Log::info('MicrosoftGraphService: No pre-existing federation configuration found.');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Log::warning('MicrosoftGraphService: Pre-existing check failed, proceeding anyway.', [
|
||||||
|
'status_code' => $checkRes->status(),
|
||||||
|
'response' => $checkRes->json(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
Log::warning('MicrosoftGraphService: Exception during pre-existing configuration check/cleanup.', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
// Suppress exception and proceed
|
// Suppress exception and proceed
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,9 +380,20 @@ public function connectFederation(
|
|||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Log::info('MicrosoftGraphService: Sending POST request to create federation configuration on Microsoft Entra ID.', [
|
||||||
|
'url' => $url,
|
||||||
|
'display_name' => $credentials->displayName,
|
||||||
|
'mfa_behavior' => $credentials->mfaBehavior->value,
|
||||||
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = Http::withToken($auth['token'])->post($url, $body);
|
$response = Http::withToken($auth['token'])->post($url, $body);
|
||||||
|
|
||||||
|
Log::info('MicrosoftGraphService: Received federation creation response.', [
|
||||||
|
'status_code' => $response->status(),
|
||||||
|
'body' => $response->json(),
|
||||||
|
]);
|
||||||
|
|
||||||
$transactions[] = [
|
$transactions[] = [
|
||||||
'name' => 'Create Federation Configuration',
|
'name' => 'Create Federation Configuration',
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
@ -257,6 +406,8 @@ public function connectFederation(
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ($response->successful()) {
|
if ($response->successful()) {
|
||||||
|
Log::info('MicrosoftGraphService: Federation configuration created successfully on Microsoft Entra ID.');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'config' => $response->json(),
|
'config' => $response->json(),
|
||||||
@ -264,8 +415,18 @@ public function connectFederation(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var string $errorMsg */
|
$errorJson = $response->json('error');
|
||||||
$errorMsg = $response->json('error.message', 'Failed to create federation configuration.');
|
$errorCode = $errorJson['code'] ?? 'Unknown';
|
||||||
|
$errorMsg = $errorJson['message'] ?? 'Failed to create federation configuration.';
|
||||||
|
|
||||||
|
if ('Authorization_RequestDenied' === $errorCode) {
|
||||||
|
$errorMsg = "Insufficient privileges to complete the operation.";
|
||||||
|
}
|
||||||
|
Log::error('MicrosoftGraphService: Federation configuration creation failed on Microsoft Entra ID.', [
|
||||||
|
'status_code' => $response->status(),
|
||||||
|
'error_code' => $errorCode,
|
||||||
|
'error_message' => $errorMsg,
|
||||||
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
@ -273,6 +434,11 @@ public function connectFederation(
|
|||||||
'transactions' => $transactions,
|
'transactions' => $transactions,
|
||||||
];
|
];
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
Log::error('MicrosoftGraphService: Exception during federation configuration creation.', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
$transactions[] = [
|
$transactions[] = [
|
||||||
'name' => 'Create Federation Configuration',
|
'name' => 'Create Federation Configuration',
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
@ -298,10 +464,17 @@ public function connectFederation(
|
|||||||
*/
|
*/
|
||||||
public function disconnectFederation(MicrosoftGraphCredentialsData $credentials, string $federationId): array
|
public function disconnectFederation(MicrosoftGraphCredentialsData $credentials, string $federationId): array
|
||||||
{
|
{
|
||||||
|
Log::info('MicrosoftGraphService: Initiating disconnectFederation.', [
|
||||||
|
'domain_name' => $credentials->domainName,
|
||||||
|
'federation_id' => $federationId,
|
||||||
|
]);
|
||||||
|
|
||||||
$auth = $this->getAccessToken($credentials);
|
$auth = $this->getAccessToken($credentials);
|
||||||
$transactions = [$auth['transaction']];
|
$transactions = [$auth['transaction']];
|
||||||
|
|
||||||
if (! $auth['token']) {
|
if (! $auth['token']) {
|
||||||
|
Log::error('MicrosoftGraphService: Failed to retrieve access token for disconnectFederation.');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Authentication with Microsoft Graph failed.',
|
'message' => 'Authentication with Microsoft Graph failed.',
|
||||||
@ -314,9 +487,17 @@ public function disconnectFederation(MicrosoftGraphCredentialsData $credentials,
|
|||||||
'Authorization' => 'Bearer ********',
|
'Authorization' => 'Bearer ********',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Log::info('MicrosoftGraphService: Sending DELETE request to Microsoft Graph for federation configuration.', [
|
||||||
|
'url' => $url,
|
||||||
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = Http::withToken($auth['token'])->delete($url);
|
$response = Http::withToken($auth['token'])->delete($url);
|
||||||
|
|
||||||
|
Log::info('MicrosoftGraphService: Received federation disconnection response.', [
|
||||||
|
'status_code' => $response->status(),
|
||||||
|
]);
|
||||||
|
|
||||||
$transactions[] = [
|
$transactions[] = [
|
||||||
'name' => 'Delete Federation Configuration',
|
'name' => 'Delete Federation Configuration',
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
@ -329,14 +510,26 @@ public function disconnectFederation(MicrosoftGraphCredentialsData $credentials,
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ($response->successful()) {
|
if ($response->successful()) {
|
||||||
|
Log::info('MicrosoftGraphService: Federation configuration deleted successfully on Microsoft Entra ID. Domain returned to Managed.');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'transactions' => $transactions,
|
'transactions' => $transactions,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var string $errorMsg */
|
$errorJson = $response->json('error');
|
||||||
$errorMsg = $response->json('error.message', 'Failed to remove federation configuration.');
|
$errorCode = $errorJson['code'] ?? 'Unknown';
|
||||||
|
$errorMsg = $errorJson['message'] ?? 'Failed to remove federation configuration.';
|
||||||
|
|
||||||
|
if ('Authorization_RequestDenied' === $errorCode) {
|
||||||
|
$errorMsg = "Insufficient privileges to complete the operation. Please verify that your Microsoft Entra application registration has been granted the 'Domain-InternalFederation.ReadWrite.All' Application Permission, and that a Global Admin has clicked 'Grant admin consent' in the Entra ID portal.";
|
||||||
|
}
|
||||||
|
Log::error('MicrosoftGraphService: Federation configuration deletion failed.', [
|
||||||
|
'status_code' => $response->status(),
|
||||||
|
'error_code' => $errorCode,
|
||||||
|
'error_message' => $errorMsg,
|
||||||
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
@ -344,6 +537,11 @@ public function disconnectFederation(MicrosoftGraphCredentialsData $credentials,
|
|||||||
'transactions' => $transactions,
|
'transactions' => $transactions,
|
||||||
];
|
];
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
Log::error('MicrosoftGraphService: Exception during federation configuration deletion.', [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
$transactions[] = [
|
$transactions[] = [
|
||||||
'name' => 'Delete Federation Configuration',
|
'name' => 'Delete Federation Configuration',
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
|
|||||||
@ -78,7 +78,7 @@ public function mount(): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->credentialsLoaded) {
|
if ($this->credentialsLoaded) {
|
||||||
$this->checkStatus(true);
|
$this->checkStatus(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,13 +324,15 @@ class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semi
|
|||||||
<div class="p-6 space-y-6">
|
<div class="p-6 space-y-6">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<x-mary-input label="Microsoft Entra Tenant ID" :value="$tenantId ?: 'Not Configured'"
|
<x-mary-input label="Microsoft Entra Tenant ID"
|
||||||
|
:value="$tenantId ? '••••••••••••••••••••••••': 'Not Configured'"
|
||||||
disabled class="bg-gray-50/50" icon="lucide.key-round"/>
|
disabled class="bg-gray-50/50" icon="lucide.key-round"/>
|
||||||
<span class="text-xs text-gray-400 mt-1 block">MICROSOFT_GRAPH_TENANT_ID</span>
|
<span class="text-xs text-gray-400 mt-1 block">MICROSOFT_GRAPH_TENANT_ID</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<x-mary-input label="Application (Client) ID" :value="$clientId ?: 'Not Configured'"
|
<x-mary-input label="Application (Client) ID"
|
||||||
|
:value="$clientId ? '••••••••••••••••••••••••' : 'Not Configured'"
|
||||||
disabled class="bg-gray-50/50" icon="lucide.app-window"/>
|
disabled class="bg-gray-50/50" icon="lucide.app-window"/>
|
||||||
<span class="text-xs text-gray-400 mt-1 block">MICROSOFT_GRAPH_CLIENT_ID</span>
|
<span class="text-xs text-gray-400 mt-1 block">MICROSOFT_GRAPH_CLIENT_ID</span>
|
||||||
</div>
|
</div>
|
||||||
@ -397,7 +399,7 @@ class="bg-gray-50/50 text-xs"
|
|||||||
<div
|
<div
|
||||||
class="flex flex-wrap items-center justify-between gap-4 border-t border-gray-100 pt-6 mt-4">
|
class="flex flex-wrap items-center justify-between gap-4 border-t border-gray-100 pt-6 mt-4">
|
||||||
<x-mary-button wire:click="checkStatus" spinner="checkStatus" icon="lucide.refresh-cw"
|
<x-mary-button wire:click="checkStatus" spinner="checkStatus" icon="lucide.refresh-cw"
|
||||||
disabled="!$credentialsLoaded" class="btn-outline">
|
:disabled="!$credentialsLoaded" class="btn-outline">
|
||||||
Check Status
|
Check Status
|
||||||
</x-mary-button>
|
</x-mary-button>
|
||||||
|
|
||||||
@ -409,7 +411,7 @@ class="flex flex-wrap items-center justify-between gap-4 border-t border-gray-10
|
|||||||
</x-mary-button>
|
</x-mary-button>
|
||||||
@else
|
@else
|
||||||
<x-mary-button wire:click="connect" spinner="connect" icon="lucide.link-2"
|
<x-mary-button wire:click="connect" spinner="connect" icon="lucide.link-2"
|
||||||
disabled="!$credentialsLoaded || !$certificateLoaded"
|
:disabled="!$credentialsLoaded || !$certificateLoaded"
|
||||||
class="btn-primary text-white font-medium shadow-sm">
|
class="btn-primary text-white font-medium shadow-sm">
|
||||||
Connect & Federate
|
Connect & Federate
|
||||||
</x-mary-button>
|
</x-mary-button>
|
||||||
@ -456,8 +458,17 @@ class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center mx-au
|
|||||||
<x-mary-icon name="lucide.cloud-off" class="w-6 h-6"/>
|
<x-mary-icon name="lucide.cloud-off" class="w-6 h-6"/>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="font-medium text-gray-700">No Active Federation</h4>
|
<h4 class="font-medium text-gray-700">No Active Federation</h4>
|
||||||
<p class="text-xs text-gray-400 mt-1 max-w-45 mx-auto leading-relaxed">Connect this IDP
|
<p class="text-xs text-gray-400 mt-1 max-w-45 mx-auto leading-relaxed mb-4">
|
||||||
to Microsoft Entra to sync active configurations.</p>
|
Connect this IDP to Microsoft Entra to sync active configurations.
|
||||||
|
</p>
|
||||||
|
<x-mary-button
|
||||||
|
wire:click="connect"
|
||||||
|
spinner="connect"
|
||||||
|
icon="lucide.link-2"
|
||||||
|
:disabled="!$credentialsLoaded || !$certificateLoaded"
|
||||||
|
class="btn-primary btn-sm text-white font-medium shadow-sm">
|
||||||
|
Create Federation
|
||||||
|
</x-mary-button>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -152,3 +152,103 @@
|
|||||||
->assertSet('connectionStatus', ConnectionStatusEnum::Disconnected)
|
->assertSet('connectionStatus', ConnectionStatusEnum::Disconnected)
|
||||||
->assertSet('federationId', null);
|
->assertSet('federationId', null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('renders error state if status check fails due to authorization request denied', function (): void {
|
||||||
|
Config::set('services.microsoft_graph', [
|
||||||
|
'tenant_id' => 'mock-tenant-id',
|
||||||
|
'client_id' => 'mock-client-id',
|
||||||
|
'client_secret' => 'mock-client-secret',
|
||||||
|
'domain_name' => 'mockdomain.com',
|
||||||
|
'display_name' => 'Company SSO',
|
||||||
|
'mfa_behavior' => 'acceptIfMfaDoneByFederatedIdp',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
'https://login.microsoftonline.com/mock-tenant-id/oauth2/v2.0/token' => Http::response([
|
||||||
|
'access_token' => 'mock-access-token',
|
||||||
|
], 200),
|
||||||
|
'https://graph.microsoft.com/v1.0/domains/mockdomain.com/federationConfiguration' => Http::response([
|
||||||
|
'error' => [
|
||||||
|
'code' => 'Authorization_RequestDenied',
|
||||||
|
'message' => 'Insufficient privileges to complete the operation.',
|
||||||
|
'innerError' => [
|
||||||
|
'date' => '2026-06-02T09:06:37',
|
||||||
|
'request-id' => '7f6c85c5-4a55-47f8-91dd-f62e264583e7',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
], 403),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::actingAs($this->admin)
|
||||||
|
->test('pages::connecton-providers.microsoft-federation')
|
||||||
|
->assertSet('connectionStatus', ConnectionStatusEnum::Error)
|
||||||
|
->assertSee('Insufficient privileges to complete the operation.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders managed state if status check returns Request_ResourceNotFound 404', function (): void {
|
||||||
|
Config::set('services.microsoft_graph', [
|
||||||
|
'tenant_id' => 'mock-tenant-id',
|
||||||
|
'client_id' => 'mock-client-id',
|
||||||
|
'client_secret' => 'mock-client-secret',
|
||||||
|
'domain_name' => 'mockdomain.com',
|
||||||
|
'display_name' => 'Company SSO',
|
||||||
|
'mfa_behavior' => 'acceptIfMfaDoneByFederatedIdp',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
'https://login.microsoftonline.com/mock-tenant-id/oauth2/v2.0/token' => Http::response([
|
||||||
|
'access_token' => 'mock-access-token',
|
||||||
|
], 200),
|
||||||
|
'https://graph.microsoft.com/v1.0/domains/mockdomain.com/federationConfiguration' => Http::response([
|
||||||
|
'error' => [
|
||||||
|
'code' => 'Request_ResourceNotFound',
|
||||||
|
'message' => "Resource 'federationConfiguration' does not exist or one of its queried reference-property objects are not present.",
|
||||||
|
'innerError' => [
|
||||||
|
'date' => '2026-06-02T09:40:58',
|
||||||
|
'request-id' => 'e7f31a03-80bf-4b42-b1ca-6af8c156fed7',
|
||||||
|
'client-request-id' => 'e7f31a03-80bf-4b42-b1ca-6af8c156fed7',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
], 404),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::actingAs($this->admin)
|
||||||
|
->test('pages::connecton-providers.microsoft-federation')
|
||||||
|
->assertSet('connectionStatus', ConnectionStatusEnum::Disconnected)
|
||||||
|
->assertSet('federationId', null)
|
||||||
|
->assertSee('No Active Federation')
|
||||||
|
->assertSee('Managed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders error state if status check fails due to domain not found in Entra ID', function (): void {
|
||||||
|
Config::set('services.microsoft_graph', [
|
||||||
|
'tenant_id' => 'mock-tenant-id',
|
||||||
|
'client_id' => 'mock-client-id',
|
||||||
|
'client_secret' => 'mock-client-secret',
|
||||||
|
'domain_name' => 'mockdomain.com',
|
||||||
|
'display_name' => 'Company SSO',
|
||||||
|
'mfa_behavior' => 'acceptIfMfaDoneByFederatedIdp',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
'https://login.microsoftonline.com/mock-tenant-id/oauth2/v2.0/token' => Http::response([
|
||||||
|
'access_token' => 'mock-access-token',
|
||||||
|
], 200),
|
||||||
|
'https://graph.microsoft.com/v1.0/domains/mockdomain.com/federationConfiguration' => Http::response([
|
||||||
|
'error' => [
|
||||||
|
'code' => 'Request_ResourceNotFound',
|
||||||
|
'message' => "Resource '{0}' does not exist or one of its queried reference-property objects are not present.",
|
||||||
|
'innerError' => [
|
||||||
|
'date' => '2026-06-02T09:50:46',
|
||||||
|
'request-id' => '08b35000-608d-4ccd-8caf-8ea42fd13812',
|
||||||
|
'client-request-id' => '08b35000-608d-4ccd-8caf-8ea42fd13812',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
], 404),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::actingAs($this->admin)
|
||||||
|
->test('pages::connecton-providers.microsoft-federation')
|
||||||
|
->assertSet('connectionStatus', ConnectionStatusEnum::Error)
|
||||||
|
->assertSee("Resource '{0}' does not exist");
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user