ABDM M2 compliance: strict EAtria auth/HFR and callback replay guards
This commit is contained in:
parent
03638979f0
commit
51e5ab026c
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user