From 51e5ab026c449a27a33e14c25adfd60a07012b42 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Fri, 26 Jun 2026 17:52:21 +0530 Subject: [PATCH] ABDM M2 compliance: strict EAtria auth/HFR and callback replay guards --- app/Controllers/AbdmGateway.php | 68 +++++++++--- app/Libraries/Abdm/EAtriaBridgeConnector.php | 108 ++++--------------- 2 files changed, 76 insertions(+), 100 deletions(-) diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index ac92321..abae4b5 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -3364,9 +3364,9 @@ class AbdmGateway extends BaseController return $signatureFailure; } - $timestampFailure = $this->validateGatewayToHmsTimestamp(); - if ($timestampFailure !== null) { - return $timestampFailure; + $metaFailure = $this->validateGatewayToHmsRequestMeta(); + if ($metaFailure !== null) { + return $metaFailure; } return null; @@ -3376,7 +3376,12 @@ class AbdmGateway extends BaseController { $secret = trim((string) ($this->readRuntimeSetting('GATEWAY_TO_HMS_HMAC_SECRET') ?: $this->readRuntimeSetting('EKA_WEBHOOK_SECRET'))); if ($secret === '') { - return null; + return $this->response->setStatusCode(500)->setJSON([ + 'ok' => 0, + 'error' => 'SERVER_MISCONFIGURED', + 'message' => 'Missing gateway webhook HMAC secret', + 'request_id' => trim((string) $this->request->getHeaderLine('X-Request-Id')), + ]); } $signature = trim((string) ($this->request->getHeaderLine('X-Eka-Signature') ?: '')); @@ -3408,11 +3413,26 @@ class AbdmGateway extends BaseController return null; } - private function validateGatewayToHmsTimestamp() + private function validateGatewayToHmsRequestMeta() { + $requestId = trim((string) $this->request->getHeaderLine('X-Request-Id')); + if ($requestId === '') { + return $this->response->setStatusCode(401)->setJSON([ + 'ok' => 0, + 'error' => 'UNAUTHORIZED', + 'message' => 'Missing X-Request-Id header', + 'request_id' => '', + ]); + } + $timestamp = trim((string) $this->request->getHeaderLine('X-Timestamp')); if ($timestamp === '') { - return null; + return $this->response->setStatusCode(401)->setJSON([ + 'ok' => 0, + 'error' => 'UNAUTHORIZED', + 'message' => 'Missing X-Timestamp header', + 'request_id' => $requestId, + ]); } try { @@ -3424,7 +3444,7 @@ class AbdmGateway extends BaseController 'ok' => 0, 'error' => 'UNAUTHORIZED', 'message' => 'Request timestamp outside allowed skew', - 'request_id' => trim((string) $this->request->getHeaderLine('X-Request-Id')), + 'request_id' => $requestId, ]); } } catch (\Throwable) { @@ -3432,10 +3452,27 @@ class AbdmGateway extends BaseController 'ok' => 0, 'error' => 'UNAUTHORIZED', 'message' => 'Invalid X-Timestamp value', - 'request_id' => trim((string) $this->request->getHeaderLine('X-Request-Id')), + 'request_id' => $requestId, ]); } + $cache = \Config\Services::cache(); + $cacheKey = 'abdm_gateway_reqid_' . sha1($requestId); + try { + $existing = $cache->get($cacheKey); + if ($existing !== null) { + return $this->response->setStatusCode(409)->setJSON([ + 'ok' => 0, + 'error' => 'REPLAY_REQUEST', + 'message' => 'Duplicate X-Request-Id detected', + 'request_id' => $requestId, + ]); + } + $cache->save($cacheKey, 1, 600); + } catch (\Throwable $e) { + log_message('warning', '[AbdmGateway] replay-guard cache failed: ' . $e->getMessage()); + } + return null; } @@ -3482,11 +3519,18 @@ class AbdmGateway extends BaseController } if ($this->db->tableExists('hospital_setting')) { - $row = $this->db->table('hospital_setting') + $fields = $this->db->getFieldNames('hospital_setting') ?? []; + $orderCol = in_array('id', $fields, true) + ? 'id' + : (in_array('s_id', $fields, true) ? 's_id' : null); + + $builder = $this->db->table('hospital_setting') ->select('s_value') - ->where('s_name', $name) - ->get(1) - ->getRowArray(); + ->where('s_name', $name); + if ($orderCol !== null) { + $builder->orderBy($orderCol, 'DESC'); + } + $row = $builder->get(1)->getRowArray(); $value = trim((string) ($row['s_value'] ?? '')); if ($value !== '') { return $value; diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index e489af0..ff059c6 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -18,6 +18,8 @@ namespace App\Libraries\Abdm; */ class EAtriaBridgeConnector implements AbdmConnectorInterface { + private const REQUIRED_HFR_ID = 'IN0510000870'; + private string $baseUrl; private string $token; private string $hfrId; @@ -40,7 +42,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface $this->tokenSourceByValue[$this->token] = 'config.eatriaBridgeToken'; $this->tokenSource = 'config.eatriaBridgeToken'; } - $this->hfrId = ''; + $this->hfrId = self::REQUIRED_HFR_ID; $this->bridgeHospitalId = ''; $this->timeoutSec = (int) ($config->eatriaBridgeTimeoutSec ?? 30); @@ -70,12 +72,6 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface ->select('s_name, s_value') ->whereIn('s_name', [ 'EATRIA_BRIDGE_TOKEN', - 'EKA_GATEWAY_TOKEN', - 'GATEWAY_TO_HMS_TOKEN', - 'ABDM_GATEWAY_TO_HMS_TOKEN', - 'ABDM_BRIDGE_TOKEN', - 'ABDM_GATEWAY_TOKEN', - 'EATRIA_BRIDGE_API_KEY', 'EATRIA_BRIDGE_URL', 'ABDM_HFR_ID', 'H_HFR_ID', @@ -103,15 +99,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface $tokenCandidates = []; $tokenSourceByValue = []; - foreach ([ - 'EATRIA_BRIDGE_TOKEN', - 'EKA_GATEWAY_TOKEN', - 'GATEWAY_TO_HMS_TOKEN', - 'ABDM_GATEWAY_TO_HMS_TOKEN', - 'ABDM_BRIDGE_TOKEN', - 'ABDM_GATEWAY_TOKEN', - 'EATRIA_BRIDGE_API_KEY', - ] as $tokenKey) { + foreach (['EATRIA_BRIDGE_TOKEN'] as $tokenKey) { $rawToken = trim((string) ($dbSettings[$tokenKey] ?? '')); if ($rawToken === '') { continue; @@ -133,7 +121,12 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface $dbUrl = trim((string) ($dbSettings['EATRIA_BRIDGE_URL'] ?? '')); if ($dbUrl !== '') { - $this->baseUrl = rtrim($dbUrl, '/'); + $host = (string) (parse_url($dbUrl, PHP_URL_HOST) ?? ''); + if ($host !== '' && in_array(strtolower($host), ['127.0.0.1', 'localhost', '::1'], true)) { + log_message('warning', '[EAtriaBridge] Ignoring localhost EATRIA_BRIDGE_URL from hospital_setting'); + } else { + $this->baseUrl = rtrim($dbUrl, '/'); + } } $dbHfrId = trim((string) ( @@ -142,9 +135,10 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface ?? $dbSettings['ABDM_HOSPITAL_HFR_ID'] ?? '' )); - if ($dbHfrId !== '') { - $this->hfrId = $dbHfrId; + if ($dbHfrId !== '' && strcasecmp($dbHfrId, self::REQUIRED_HFR_ID) !== 0) { + log_message('warning', '[EAtriaBridge] Overriding configured HFR ID with mandated HFR ' . self::REQUIRED_HFR_ID . ' (found ' . $dbHfrId . ')'); } + $this->hfrId = self::REQUIRED_HFR_ID; $dbBridgeHospitalId = trim((string) ( $dbSettings['ABDM_BRIDGE_HOSPITAL_ID'] @@ -337,7 +331,12 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface }; if ($this->token === '') { - log_message('warning', '[EAtriaBridge] Authorization token missing for request: ' . $method . ' ' . $url); + log_message('error', '[EAtriaBridge] Authorization token missing for request: ' . $method . ' ' . $url); + return [ + 'ok' => 0, + 'http_code' => 0, + 'error_text' => 'Missing EATRIA_BRIDGE_TOKEN in hospital_setting', + ]; } $attempt = $callWithToken($this->token); @@ -715,74 +714,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface $body[$optional] = (string) $data[$optional]; } - $result = $this->post('/v3/records/push', $body); - - $httpCode = (int) ($result['http_code'] ?? 0); - $authErrorText = strtolower(trim((string) ($result['message'] ?? $result['error_text'] ?? $result['error'] ?? ''))); - $isAuthFailure = in_array($httpCode, [401, 403], true) - && ($authErrorText === '' - || str_contains($authErrorText, 'unauthorized') - || str_contains($authErrorText, 'invalid authorization token') - || str_contains($authErrorText, 'invalid token')); - - // Some hospital keys are scoped to /v1/bridge dispatcher paths. - // Fallback to event dispatch when direct /v3/records/push returns auth failure. - if (! $isAuthFailure) { - return $result; - } - - $dispatchPayload = [ - 'event_type' => 'abdm.fhir.share.requested', - 'payload' => [ - 'hi_type' => $hiType, - 'record_type' => $recordType, - 'fhir_bundle' => $body['fhir_bundle'] ?? (object) [], - 'bundle' => $body['fhir_bundle'] ?? (object) [], - 'care_context_reference' => $careContextReference, - 'care_context_display' => $careContextDisplay, - 'queue_id' => $queueId, - 'abha_id' => $abhaId, - 'abha_address' => $abhaAddress, - 'patient_id' => (string) ($data['patient_id'] ?? ''), - 'patient_name' => $patientName, - 'visit_date' => (string) ($data['visit_date'] ?? date('Y-m-d')), - 'hfr_id' => $this->hfrId, - 'hospital_id' => $this->bridgeHospitalId, - ], - ]; - - $dispatch = $this->post('/v1/bridge', $dispatchPayload); - $dispatchOk = (int) ($dispatch['ok'] ?? 0) === 1; - $dispatchNode = is_array($dispatch['dispatch'] ?? null) ? $dispatch['dispatch'] : []; - $dispatchHttp = (int) ($dispatchNode['http_code'] ?? $dispatch['http_code'] ?? 0); - $dispatchResponse = is_array($dispatchNode['response'] ?? null) ? $dispatchNode['response'] : []; - $dispatchErr = trim((string) ( - $dispatchResponse['message'] - ?? $dispatchResponse['error_text'] - ?? $dispatchResponse['error'] - ?? $dispatch['message'] - ?? $dispatch['error_text'] - ?? $dispatch['error'] - ?? '' - )); - - if (! $dispatchOk) { - return array_merge($result, [ - 'fallback' => 'v1_bridge_dispatch_failed', - 'fallback_http_code' => $dispatchHttp, - 'fallback_error' => $dispatchErr !== '' ? $dispatchErr : 'Bridge dispatcher failed', - ]); - } - - return [ - 'ok' => (int) ($dispatchResponse['ok'] ?? 1), - 'http_code' => $dispatchHttp > 0 ? $dispatchHttp : 200, - 'id' => (int) ($dispatchResponse['id'] ?? $dispatchResponse['record_id'] ?? 0), - 'queue_id' => (string) ($dispatchResponse['queue_id'] ?? $dispatchResponse['request_id'] ?? $dispatch['request_id'] ?? $queueId), - 'message' => (string) ($dispatchResponse['message'] ?? 'Record dispatched via v1 bridge'), - 'dispatch' => $dispatch, - 'fallback' => 'v1_bridge_dispatch', - ]; + return $this->post('/v3/records/push', $body); } // -------------------------------------------------------------------------