validPagarmeCredentials($request)) { return $this->errorResponse(message: __('http.unauthorized_token'), code: 401); } $this->webhookService->handlePagarme($request->all()); return $this->successResponse(message: __('http.webhook_received')); } public function didit(Request $request): JsonResponse { $payload = $request->all(); Log::channel('didit')->info('Webhook recebido do Didit', [ 'event_id' => data_get($payload, 'event_id'), 'webhook_type' => data_get($payload, 'webhook_type'), 'session_id' => data_get($payload, 'session_id'), 'status' => data_get($payload, 'status'), 'vendor_data' => data_get($payload, 'vendor_data'), 'environment' => data_get($payload, 'environment'), 'is_test' => $request->hasHeader('X-Didit-Test-Webhook'), ]); if (! $this->validDiditSignature($request)) { return $this->errorResponse(message: __('http.unauthorized_token'), code: 401); } $this->webhookService->handleDidit($payload); return $this->successResponse(message: __('http.webhook_received')); } // private function validPagarmeCredentials(Request $request): bool { $configuredUser = config('services.pagarme.webhook_user'); $configuredPassword = config('services.pagarme.webhook_password'); if (empty($configuredUser) || empty($configuredPassword)) { return false; } return is_string($configuredUser) && is_string($configuredPassword) && $this->validBasicAuthCredentials($request, $configuredUser, $configuredPassword); } private function validBasicAuthCredentials(Request $request, string $configuredUser, string $configuredPassword): bool { $receivedUser = $request->getUser(); $receivedPassword = $request->getPassword(); return is_string($receivedUser) && is_string($receivedPassword) && hash_equals($configuredUser, $receivedUser) && hash_equals($configuredPassword, $receivedPassword); } private function validDiditSignature(Request $request): bool { $secrets = $this->diditWebhookSecrets(); if ($secrets === []) { Log::channel('didit')->error('DIDIT_WEBHOOK_SECRET nao configurado; webhook rejeitado'); return false; } if (! $this->validDiditTimestamp($request)) { return false; } $rawBody = $request->getContent(); $candidates = array_filter([ 'X-Signature-V2' => $this->diditCanonicalBody($rawBody), 'X-Signature' => $rawBody, ], static fn ($body) => is_string($body)); foreach ($candidates as $header => $body) { $received = (string) $request->header($header, ''); if ($received === '') { continue; } foreach ($secrets as $secret) { if (hash_equals(hash_hmac('sha256', $body, $secret), $received)) { return true; } } } Log::channel('didit')->warning('Assinatura do webhook Didit invalida', [ 'secrets_testados' => count($secrets), 'received_v2' => substr((string) $request->header('X-Signature-V2', ''), 0, 12), 'body_length' => strlen($rawBody), ]); return false; } /** * @return list */ private function diditWebhookSecrets(): array { $configured = config('services.didit.webhook_secret'); if (! is_string($configured)) { return []; } return array_values(array_filter( array_map('trim', explode(',', $configured)), static fn (string $secret) => $secret !== '', )); } private function validDiditTimestamp(Request $request): bool { $timestamp = $request->header('X-Timestamp'); if (! is_numeric($timestamp)) { Log::channel('didit')->warning('Webhook Didit sem X-Timestamp valido'); return false; } $tolerance = (int) config('services.didit.webhook_tolerance', 300); if (abs(time() - (int) $timestamp) > $tolerance) { Log::channel('didit')->warning('Webhook Didit fora da janela de tolerancia', [ 'timestamp' => (int) $timestamp, 'now' => time(), ]); return false; } return true; } private function diditCanonicalBody(string $rawBody): ?string { try { $decoded = json_decode($rawBody, false, 512, JSON_THROW_ON_ERROR); } catch (\JsonException) { return null; } $canonical = json_encode( $this->canonicalizeDiditValue($decoded), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, ); return $canonical === false ? null : $canonical; } private function canonicalizeDiditValue(mixed $value): mixed { if (is_array($value)) { return array_map(fn ($item) => $this->canonicalizeDiditValue($item), $value); } if ($value instanceof \stdClass) { $data = get_object_vars($value); ksort($data, SORT_STRING); foreach ($data as $key => $item) { $data[$key] = $this->canonicalizeDiditValue($item); } return (object) $data; } if (is_float($value) && is_finite($value) && floor($value) === $value && abs($value) < PHP_INT_MAX) { return (int) $value; } return $value; } }