$credentials->domainName, ]); $auth = $this->getAccessToken($credentials); $transactions = [$auth['transaction']]; if (! $auth['token']) { Log::error('MicrosoftGraphService: Failed to retrieve access token, aborting federation config retrieval.'); return [ 'status' => 'error', 'message' => 'Authentication with Microsoft Graph failed.', 'config' => null, 'transactions' => $transactions, ]; } $url = "https://graph.microsoft.com/v1.0/domains/{$credentials->domainName}/federationConfiguration"; $headers = [ 'Authorization' => 'Bearer ********', 'Content-Type' => 'application/json', ]; Log::info('MicrosoftGraphService: Sending GET request to Microsoft Graph for federation configuration.', [ 'url' => $url, ]); try { $response = Http::withToken($auth['token'])->get($url); Log::info('MicrosoftGraphService: Received federation configuration response.', [ 'status_code' => $response->status(), ]); $transactions[] = [ 'name' => 'Get Federation Configuration', 'url' => $url, 'method' => 'GET', 'request_headers' => $headers, 'request_body' => null, 'status' => $response->status(), 'response_headers' => $response->headers(), 'response_body' => $response->json(), ]; if ($response->successful()) { /** @var array $value */ $value = $response->json('value', []); $hasConfig = ! empty($value); Log::info('MicrosoftGraphService: Federation configuration query completed successfully.', [ 'config_found' => $hasConfig, ]); return [ 'status' => 'success', 'config' => $hasConfig ? $value[0] : null, 'transactions' => $transactions, ]; } // Handle standard 404 Request_ResourceNotFound $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 [ 'status' => 'error', 'message' => $errorMsg, 'config' => null, 'transactions' => $transactions, ]; } catch (Exception $e) { Log::error('MicrosoftGraphService: Exception during federation config retrieval.', [ 'message' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); $transactions[] = [ 'name' => 'Get Federation Configuration', 'url' => $url, 'method' => 'GET', 'request_headers' => $headers, 'request_body' => null, 'status' => 'ERROR', 'response_body' => ['error' => $e->getMessage()], ]; return [ 'status' => 'error', 'message' => $e->getMessage(), 'config' => null, 'transactions' => $transactions, ]; } } /** * 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. * * @return array{status: string, message?: string, config?: ?array, transactions: array} */ public function connectFederation( MicrosoftGraphCredentialsData $credentials, string $certPem, string $issuerUri, string $passiveSignInUri, string $activeSignInUri, string $signOutUri ): 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); $transactions = [$auth['transaction']]; if (! $auth['token']) { Log::error('MicrosoftGraphService: Failed to retrieve access token for connectFederation.'); return [ 'status' => 'error', 'message' => 'Authentication with Microsoft Graph failed.', 'transactions' => $transactions, ]; } // Check and delete existing configuration to ensure a clean setup $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 { $checkRes = Http::withToken($auth['token'])->get($checkUrl); Log::info('MicrosoftGraphService: Pre-check status response received.', [ 'status_code' => $checkRes->status(), ]); $transactions[] = [ 'name' => 'Check Existing Federation Configuration', 'url' => $checkUrl, 'method' => 'GET', 'status' => $checkRes->status(), 'response_body' => $checkRes->json(), ]; if ($checkRes->successful()) { /** @var array $configs */ $configs = $checkRes->json('value', []); if (! empty($configs)) { /** @var string $existingId */ $existingId = $configs[0]['id']; $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); Log::info('MicrosoftGraphService: Pre-existing configuration deletion response.', [ 'status_code' => $deleteRes->status(), ]); $transactions[] = [ 'name' => 'Delete Pre-existing Federation Configuration', 'url' => $deleteUrl, 'method' => 'DELETE', 'status' => $deleteRes->status(), 'response_body' => $deleteRes->json() ?: ['message' => 'Deleted successfully'], ]; if (! $deleteRes->successful()) { Log::error('MicrosoftGraphService: Failed to delete pre-existing federation configuration.', [ 'status_code' => $deleteRes->status(), 'response_body' => $deleteRes->json(), ]); return [ 'status' => 'error', 'message' => 'Failed to delete pre-existing federation configuration.', '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) { Log::warning('MicrosoftGraphService: Exception during pre-existing configuration check/cleanup.', [ 'message' => $e->getMessage(), ]); // Suppress exception and proceed } // Prepare the signing certificate PEM string $signingCertificateClean = str_replace( ['-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----', "\r", "\n", ' '], '', $certPem ); $url = "https://graph.microsoft.com/v1.0/domains/{$credentials->domainName}/federationConfiguration"; $body = [ '@odata.type' => '#microsoft.graph.internalDomainFederation', 'displayName' => $credentials->displayName, 'issuerUri' => $issuerUri, 'signingCertificate' => $signingCertificateClean, 'passiveSignInUri' => $passiveSignInUri, 'preferredAuthenticationProtocol' => 'saml', 'activeSignInUri' => $activeSignInUri, 'signOutUri' => $signOutUri, 'promptLoginBehavior' => 'nativeSupport', 'isSignedAuthenticationRequestRequired' => false, 'federatedIdpMfaBehavior' => $credentials->mfaBehavior->value, ]; $headers = [ 'Authorization' => 'Bearer ********', '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 { $response = Http::withToken($auth['token'])->post($url, $body); Log::info('MicrosoftGraphService: Received federation creation response.', [ 'status_code' => $response->status(), 'body' => $response->json(), ]); $transactions[] = [ 'name' => 'Create Federation Configuration', 'url' => $url, 'method' => 'POST', 'request_headers' => $headers, 'request_body' => $body, 'status' => $response->status(), 'response_headers' => $response->headers(), 'response_body' => $response->json(), ]; if ($response->successful()) { Log::info('MicrosoftGraphService: Federation configuration created successfully on Microsoft Entra ID.'); return [ 'status' => 'success', 'config' => $response->json(), 'transactions' => $transactions, ]; } $errorJson = $response->json('error'); $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 [ 'status' => 'error', 'message' => $errorMsg, 'transactions' => $transactions, ]; } catch (Exception $e) { Log::error('MicrosoftGraphService: Exception during federation configuration creation.', [ 'message' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); $transactions[] = [ 'name' => 'Create Federation Configuration', 'url' => $url, 'method' => 'POST', 'request_headers' => $headers, 'request_body' => $body, 'status' => 'ERROR', 'response_body' => ['error' => $e->getMessage()], ]; return [ 'status' => 'error', 'message' => $e->getMessage(), 'transactions' => $transactions, ]; } } /** * Disconnect/Remove federation configuration. * * @return array{status: string, message?: string, transactions: 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); $transactions = [$auth['transaction']]; if (! $auth['token']) { Log::error('MicrosoftGraphService: Failed to retrieve access token for disconnectFederation.'); return [ 'status' => 'error', 'message' => 'Authentication with Microsoft Graph failed.', 'transactions' => $transactions, ]; } $url = "https://graph.microsoft.com/v1.0/domains/{$credentials->domainName}/federationConfiguration/{$federationId}"; $headers = [ 'Authorization' => 'Bearer ********', ]; Log::info('MicrosoftGraphService: Sending DELETE request to Microsoft Graph for federation configuration.', [ 'url' => $url, ]); try { $response = Http::withToken($auth['token'])->delete($url); Log::info('MicrosoftGraphService: Received federation disconnection response.', [ 'status_code' => $response->status(), ]); $transactions[] = [ 'name' => 'Delete Federation Configuration', 'url' => $url, 'method' => 'DELETE', 'request_headers' => $headers, 'request_body' => null, 'status' => $response->status(), 'response_headers' => $response->headers(), 'response_body' => $response->json() ?: ['message' => 'Federation removed successfully. Domain returned to Managed.'], ]; if ($response->successful()) { Log::info('MicrosoftGraphService: Federation configuration deleted successfully on Microsoft Entra ID. Domain returned to Managed.'); return [ 'status' => 'success', 'transactions' => $transactions, ]; } $errorJson = $response->json('error'); $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 [ 'status' => 'error', 'message' => $errorMsg, 'transactions' => $transactions, ]; } catch (Exception $e) { Log::error('MicrosoftGraphService: Exception during federation configuration deletion.', [ 'message' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); $transactions[] = [ 'name' => 'Delete Federation Configuration', 'url' => $url, 'method' => 'DELETE', 'request_headers' => $headers, 'request_body' => null, 'status' => 'ERROR', 'response_body' => ['error' => $e->getMessage()], ]; return [ 'status' => 'error', 'message' => $e->getMessage(), 'transactions' => $transactions, ]; } } }