- Added `MicrosoftGraphCredentialsData` and `FederatedIdpMfaBehaviorEnum` for managing Entra ID integration. - Implemented `MicrosoftGraphService` to handle federation setup, domain management, and token authentication. - Created UI for managing Microsoft Graph Federation, including connection and disconnection flows. - Extended tests to validate Microsoft Graph Federation functionality.
365 lines
13 KiB
PHP
365 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Data\MicrosoftGraph\MicrosoftGraphCredentialsData;
|
|
use Exception;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
/**
|
|
* Service to manage Microsoft Graph API domain federation configuration.
|
|
*/
|
|
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.
|
|
*
|
|
* @return array{status: string, message?: string, config: ?array, transactions: array}
|
|
*/
|
|
public function getFederationConfig(MicrosoftGraphCredentialsData $credentials): array
|
|
{
|
|
$auth = $this->getAccessToken($credentials);
|
|
$transactions = [$auth['transaction']];
|
|
|
|
if (! $auth['token']) {
|
|
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',
|
|
];
|
|
|
|
try {
|
|
$response = Http::withToken($auth['token'])->get($url);
|
|
|
|
$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', []);
|
|
|
|
return [
|
|
'status' => 'success',
|
|
'config' => ! empty($value) ? $value[0] : null,
|
|
'transactions' => $transactions,
|
|
];
|
|
}
|
|
|
|
/** @var string $errorMsg */
|
|
$errorMsg = $response->json('error.message', 'Failed to retrieve federation config.');
|
|
|
|
return [
|
|
'status' => 'error',
|
|
'message' => $errorMsg,
|
|
'config' => null,
|
|
'transactions' => $transactions,
|
|
];
|
|
} catch (Exception $e) {
|
|
$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,
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
$auth = $this->getAccessToken($credentials);
|
|
$transactions = [$auth['transaction']];
|
|
|
|
if (! $auth['token']) {
|
|
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";
|
|
try {
|
|
$checkRes = Http::withToken($auth['token'])->get($checkUrl);
|
|
$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}";
|
|
$deleteRes = Http::withToken($auth['token'])->delete($deleteUrl);
|
|
$transactions[] = [
|
|
'name' => 'Delete Pre-existing Federation Configuration',
|
|
'url' => $deleteUrl,
|
|
'method' => 'DELETE',
|
|
'status' => $deleteRes->status(),
|
|
'response_body' => $deleteRes->json() ?: ['message' => 'Deleted successfully'],
|
|
];
|
|
|
|
if (! $deleteRes->successful()) {
|
|
return [
|
|
'status' => 'error',
|
|
'message' => 'Failed to delete pre-existing federation configuration.',
|
|
'transactions' => $transactions,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// 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',
|
|
];
|
|
|
|
try {
|
|
$response = Http::withToken($auth['token'])->post($url, $body);
|
|
|
|
$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()) {
|
|
return [
|
|
'status' => 'success',
|
|
'config' => $response->json(),
|
|
'transactions' => $transactions,
|
|
];
|
|
}
|
|
|
|
/** @var string $errorMsg */
|
|
$errorMsg = $response->json('error.message', 'Failed to create federation configuration.');
|
|
|
|
return [
|
|
'status' => 'error',
|
|
'message' => $errorMsg,
|
|
'transactions' => $transactions,
|
|
];
|
|
} catch (Exception $e) {
|
|
$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
|
|
{
|
|
$auth = $this->getAccessToken($credentials);
|
|
$transactions = [$auth['transaction']];
|
|
|
|
if (! $auth['token']) {
|
|
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 ********',
|
|
];
|
|
|
|
try {
|
|
$response = Http::withToken($auth['token'])->delete($url);
|
|
|
|
$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()) {
|
|
return [
|
|
'status' => 'success',
|
|
'transactions' => $transactions,
|
|
];
|
|
}
|
|
|
|
/** @var string $errorMsg */
|
|
$errorMsg = $response->json('error.message', 'Failed to remove federation configuration.');
|
|
|
|
return [
|
|
'status' => 'error',
|
|
'message' => $errorMsg,
|
|
'transactions' => $transactions,
|
|
];
|
|
} catch (Exception $e) {
|
|
$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,
|
|
];
|
|
}
|
|
}
|
|
}
|