diff --git a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php new file mode 100644 index 0000000000..913274bea6 --- /dev/null +++ b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php @@ -0,0 +1,62 @@ +serverIndex ?: 'default'); + + return Cache::remember($cacheKey, now()->addMinutes(50), function () { + return $this->requestAccessToken(); + }); + } + + private function requestAccessToken(): string + { + $tenantId = $this->config['tenant_id'] ?? null; + $clientId = $this->config['key'] ?? null; + $clientSecret = $this->config['secret'] ?? null; + + if (!$tenantId || !$clientId || !$clientSecret) { + throw new RuntimeException('Microsoft Graph credentials are not configured.'); + } + + $client = $this->httpClient ?? new Client(); + $response = $client->post(sprintf(self::TOKEN_URL, $tenantId), [ + 'form_params' => [ + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'scope' => self::DEFAULT_SCOPE, + 'grant_type' => 'client_credentials', + ], + 'http_errors' => false, + ]); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() >= 400) { + $message = $body['error_description'] ?? $body['error']['message'] ?? 'Unknown error'; + + throw new RuntimeException('Failed to get Microsoft Graph access token: ' . $message); + } + + return $body['access_token']; + } +} diff --git a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php new file mode 100644 index 0000000000..a4f62d67d5 --- /dev/null +++ b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php @@ -0,0 +1,65 @@ + '', + 'key' => 'client-id', + 'secret' => 'client-secret', + ]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Microsoft Graph credentials are not configured.'); + + $provider->getAccessToken(); + } + + public function testGetAccessTokenRequestsTokenFromMicrosoft() + { + Cache::flush(); + + $tokenResponse = new Response(200, [], json_encode(['access_token' => 'token-from-azure'])); + + $guzzle = Mockery::mock(Client::class); + $guzzle->shouldReceive('post') + ->once() + ->with( + 'https://login.microsoftonline.com/tenant-id-123/oauth2/v2.0/token', + Mockery::on(function ($options) { + return $options['form_params']['client_id'] === 'client-id-123' + && $options['form_params']['client_secret'] === 'client-secret-123' + && $options['form_params']['scope'] === 'https://graph.microsoft.com/.default' + && $options['form_params']['grant_type'] === 'client_credentials' + && $options['http_errors'] === false; + }) + ) + ->andReturn($tokenResponse); + + $provider = new MicrosoftGraphTokenProvider([ + 'tenant_id' => 'tenant-id-123', + 'key' => 'client-id-123', + 'secret' => 'client-secret-123', + ], 1, $guzzle); + + $this->assertSame('token-from-azure', $provider->getAccessToken()); + $this->assertSame('token-from-azure', $provider->getAccessToken()); + } +}