720 lines
29 KiB
PHP
720 lines
29 KiB
PHP
<?php
|
|
|
|
namespace App\Libraries\Abdm;
|
|
|
|
/**
|
|
* AbdmV3DirectHelper
|
|
*
|
|
* Direct ABDM V3 Client for Find ABHA via Mobile & Account Login.
|
|
* Compliant with ABDM M1 Guidelines:
|
|
* MOBILE NUMBER -> PROFILES -> OTP -> COMPLETE PROFILE FETCHED
|
|
*
|
|
* Uses ABDM Sandbox / Production endpoints with RSA-OAEP encryption.
|
|
*/
|
|
class AbdmV3DirectHelper
|
|
{
|
|
protected string $clientId = '';
|
|
protected string $clientSecret = '';
|
|
protected string $sessionsUrl = 'https://dev.abdm.gov.in/api/hiecm/gateway/v3/sessions';
|
|
protected string $certUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/public/certificate';
|
|
protected string $searchUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/account/abha/search';
|
|
protected string $otpUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/login/request/otp';
|
|
protected string $verifyUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/login/verify';
|
|
protected string $cardUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/account/abha-card';
|
|
protected string $profileUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/account';
|
|
|
|
public function __construct()
|
|
{
|
|
$this->loadSettings();
|
|
}
|
|
|
|
protected function log(string $level, string $msg): void
|
|
{
|
|
if (function_exists('log_message')) {
|
|
\log_message($level, $msg);
|
|
} else {
|
|
error_log("[AbdmV3DirectHelper] [{$level}] {$msg}");
|
|
}
|
|
}
|
|
|
|
protected function loadSettings(): void
|
|
{
|
|
try {
|
|
if (class_exists('\Config\Database') && function_exists('config')) {
|
|
$db = \Config\Database::connect();
|
|
if ($db->tableExists('hospital_setting')) {
|
|
$rows = $db->table('hospital_setting')
|
|
->whereIn('s_name', ['ABDM_CLIENT_ID', 'ABDM_CLIENT_SECRET'])
|
|
->get()->getResultArray();
|
|
foreach ($rows as $r) {
|
|
$name = trim((string) ($r['s_name'] ?? ''));
|
|
$val = trim((string) ($r['s_value'] ?? ''));
|
|
if ($name === 'ABDM_CLIENT_ID' && $val !== '') {
|
|
$this->clientId = $val;
|
|
}
|
|
if ($name === 'ABDM_CLIENT_SECRET' && $val !== '') {
|
|
$this->clientSecret = $val;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$this->log('warning', '[AbdmV3DirectHelper] loadSettings failed: ' . $e->getMessage());
|
|
}
|
|
|
|
if (function_exists('env')) {
|
|
$envClientId = env('abdm.directClientId', env('BRIDGE_SOURCE_CODE', ''));
|
|
if (! empty($envClientId) && empty($this->clientId)) {
|
|
$this->clientId = (string) $envClientId;
|
|
}
|
|
$envSecret = env('abdm.directClientSecret', '');
|
|
if (! empty($envSecret) && empty($this->clientSecret)) {
|
|
$this->clientSecret = (string) $envSecret;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function generateUuid(): string
|
|
{
|
|
return sprintf(
|
|
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
|
mt_rand(0, 0xffff),
|
|
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
|
);
|
|
}
|
|
|
|
protected function curlCall(string $url, string $method, array $body = [], array $headers = []): array
|
|
{
|
|
$ch = curl_init($url);
|
|
$opts = [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 20,
|
|
CURLOPT_HTTPHEADER => array_merge(['Content-Type: application/json', 'Accept: application/json'], $headers),
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_SSL_VERIFYHOST => 0,
|
|
];
|
|
|
|
if ($method === 'POST') {
|
|
$opts[CURLOPT_POST] = true;
|
|
$opts[CURLOPT_POSTFIELDS] = json_encode($body);
|
|
} elseif ($method !== 'GET') {
|
|
$opts[CURLOPT_CUSTOMREQUEST] = $method;
|
|
if (! empty($body)) {
|
|
$opts[CURLOPT_POSTFIELDS] = json_encode($body);
|
|
}
|
|
}
|
|
|
|
curl_setopt_array($ch, $opts);
|
|
$raw = curl_exec($ch);
|
|
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$err = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
$json = json_decode((string) $raw, true);
|
|
return [
|
|
'code' => $code,
|
|
'raw' => (string) $raw,
|
|
'err' => $err,
|
|
'json' => is_array($json) ? $json : [],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get or refresh gateway session Bearer token.
|
|
*/
|
|
public function getSessionToken(bool $forceRefresh = false): string
|
|
{
|
|
$cacheKey = 'abdm_v3_direct_session_token';
|
|
if (! $forceRefresh && function_exists('cache')) {
|
|
try {
|
|
$cached = cache()->get($cacheKey);
|
|
if (is_string($cached) && trim($cached) !== '') {
|
|
return trim($cached);
|
|
}
|
|
} catch (\Throwable $e) {}
|
|
}
|
|
|
|
$resp = $this->curlCall($this->sessionsUrl, 'POST', [
|
|
'clientId' => $this->clientId,
|
|
'clientSecret' => $this->clientSecret,
|
|
'grantType' => 'client_credentials',
|
|
], [
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-CM-ID: sbx',
|
|
]);
|
|
|
|
$token = (string) ($resp['json']['accessToken'] ?? '');
|
|
$expiresIn = (int) ($resp['json']['expiresIn'] ?? 1200);
|
|
|
|
if ($token !== '' && function_exists('cache')) {
|
|
try {
|
|
cache()->save($cacheKey, $token, max(300, $expiresIn - 120));
|
|
} catch (\Throwable $e) {}
|
|
}
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* Get or refresh ABDM RSA Public Certificate PEM.
|
|
*/
|
|
public function getPublicCertificatePem(bool $forceRefresh = false): string
|
|
{
|
|
$cacheKey = 'abdm_v3_direct_public_cert_pem';
|
|
if (! $forceRefresh && function_exists('cache')) {
|
|
try {
|
|
$cached = cache()->get($cacheKey);
|
|
if (is_string($cached) && trim($cached) !== '') {
|
|
return trim($cached);
|
|
}
|
|
} catch (\Throwable $e) {}
|
|
}
|
|
|
|
$token = $this->getSessionToken($forceRefresh);
|
|
if ($token === '') {
|
|
throw new \RuntimeException('Failed to obtain ABDM session token for certificate fetch.');
|
|
}
|
|
|
|
$resp = $this->curlCall($this->certUrl, 'GET', [], [
|
|
'Authorization: Bearer ' . $token,
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-Client-ID: ' . $this->clientId,
|
|
]);
|
|
|
|
$rawKey = (string) ($resp['json']['publicKey'] ?? $resp['json']['Certificate'] ?? $resp['raw'] ?? '');
|
|
if (trim($rawKey) === '') {
|
|
throw new \RuntimeException('ABDM public certificate is empty.');
|
|
}
|
|
|
|
if (! str_contains($rawKey, '-----BEGIN')) {
|
|
$pemKey = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(trim($rawKey), 64, "\n") . "-----END PUBLIC KEY-----\n";
|
|
} else {
|
|
$pemKey = $rawKey;
|
|
}
|
|
|
|
if (function_exists('cache')) {
|
|
try {
|
|
cache()->save($cacheKey, $pemKey, 86400);
|
|
} catch (\Throwable $e) {}
|
|
}
|
|
|
|
return $pemKey;
|
|
}
|
|
|
|
/**
|
|
* RSA-OAEP encrypt text using ABDM public key.
|
|
*/
|
|
public function encrypt(string $plainText): string
|
|
{
|
|
$pem = $this->getPublicCertificatePem();
|
|
$pubKey = openssl_pkey_get_public($pem);
|
|
if ($pubKey === false) {
|
|
$pem = $this->getPublicCertificatePem(true);
|
|
$pubKey = openssl_pkey_get_public($pem);
|
|
if ($pubKey === false) {
|
|
throw new \RuntimeException('Invalid ABDM public key resource: ' . openssl_error_string());
|
|
}
|
|
}
|
|
|
|
$encrypted = '';
|
|
$ok = openssl_public_encrypt($plainText, $encrypted, $pubKey, OPENSSL_PKCS1_OAEP_PADDING);
|
|
if (! $ok) {
|
|
$pem = $this->getPublicCertificatePem(true);
|
|
$pubKey = openssl_pkey_get_public($pem);
|
|
$ok = openssl_public_encrypt($plainText, $encrypted, $pubKey, OPENSSL_PKCS1_OAEP_PADDING);
|
|
if (! $ok) {
|
|
throw new \RuntimeException('RSA-OAEP encryption failed: ' . openssl_error_string());
|
|
}
|
|
}
|
|
|
|
return base64_encode($encrypted);
|
|
}
|
|
|
|
/**
|
|
* Step 1: Search ABHA profiles by 10-digit mobile number.
|
|
* Upstream: POST /abha/api/v3/profile/account/abha/search
|
|
*
|
|
* @return array{ok: int, txn_id?: string, profiles?: array, error_text?: string}
|
|
*/
|
|
public function searchByMobile(string $mobile): array
|
|
{
|
|
$digits = preg_replace('/\D/', '', $mobile);
|
|
if (strlen($digits) !== 10) {
|
|
return ['ok' => 0, 'error_text' => 'Valid 10-digit mobile number is required.'];
|
|
}
|
|
|
|
try {
|
|
$token = $this->getSessionToken();
|
|
if ($token === '') {
|
|
return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.'];
|
|
}
|
|
|
|
$encMobile = $this->encrypt($digits);
|
|
$resp = $this->curlCall($this->searchUrl, 'POST', [
|
|
'scope' => ['search-abha'],
|
|
'mobile' => $encMobile,
|
|
], [
|
|
'Authorization: Bearer ' . $token,
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-Client-ID: ' . $this->clientId,
|
|
]);
|
|
|
|
// If 401 Unauthorized, refresh token and retry once
|
|
if ($resp['code'] === 401) {
|
|
$token = $this->getSessionToken(true);
|
|
$resp = $this->curlCall($this->searchUrl, 'POST', [
|
|
'scope' => ['search-abha'],
|
|
'mobile' => $encMobile,
|
|
], [
|
|
'Authorization: Bearer ' . $token,
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-Client-ID: ' . $this->clientId,
|
|
]);
|
|
}
|
|
|
|
if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) {
|
|
$firstBlock = is_array($resp['json']) && isset($resp['json'][0]) ? $resp['json'][0] : $resp['json'];
|
|
$txnId = (string) ($firstBlock['txnId'] ?? '');
|
|
$rawProfiles = (array) ($firstBlock['ABHA'] ?? $firstBlock['accounts'] ?? []);
|
|
|
|
$profiles = [];
|
|
foreach ($rawProfiles as $p) {
|
|
$methods = [];
|
|
foreach ((array) ($p['authMethods'] ?? []) as $m) {
|
|
$mUpper = strtoupper(trim((string) $m));
|
|
if ($mUpper !== '' && ! in_array($mUpper, $methods, true)) {
|
|
$methods[] = $mUpper;
|
|
}
|
|
}
|
|
if (empty($methods)) {
|
|
$methods = ['MOBILE_OTP', 'AADHAAR_OTP'];
|
|
}
|
|
|
|
$profiles[] = [
|
|
'index' => (string) ($p['index'] ?? (count($profiles) + 1)),
|
|
'abha_number' => (string) ($p['ABHANumber'] ?? $p['abhaNumber'] ?? $p['abha_id'] ?? ''),
|
|
'name' => (string) ($p['name'] ?? ''),
|
|
'gender' => (string) ($p['gender'] ?? ''),
|
|
'dob' => (string) ($p['dob'] ?? $p['dateOfBirth'] ?? ''),
|
|
'kyc_verified' => (string) ($p['kycVerified'] ?? 'true') === 'true',
|
|
'auth_methods' => $methods,
|
|
'profile_photo' => (string) ($p['profilePhoto'] ?? ''),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'txn_id' => $txnId,
|
|
'profiles' => $profiles,
|
|
];
|
|
}
|
|
|
|
$errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? 'ABDM search returned no accounts for this mobile.');
|
|
return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']];
|
|
} catch (\Throwable $e) {
|
|
$this->log('error', '[AbdmV3DirectHelper] searchByMobile exception: ' . $e->getMessage());
|
|
return ['ok' => 0, 'error_text' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Step 2: Request OTP for a selected profile index.
|
|
* Upstream: POST /abha/api/v3/profile/login/request/otp
|
|
*
|
|
* @return array{ok: int, txn_id?: string, message?: string, error_text?: string}
|
|
*/
|
|
public function requestProfileOtp(string $searchTxnId, string $index, string $authMethod = 'MOBILE_OTP'): array
|
|
{
|
|
$authMethod = strtoupper(trim($authMethod));
|
|
$isAadhaar = in_array($authMethod, ['AADHAAR_OTP', 'AADHAAR', 'AADHAAR-VERIFY'], true);
|
|
|
|
try {
|
|
$token = $this->getSessionToken();
|
|
if ($token === '') {
|
|
return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.'];
|
|
}
|
|
|
|
$encIndex = $this->encrypt((string) $index);
|
|
$scope = $isAadhaar
|
|
? ['abha-login', 'search-abha', 'aadhaar-verify']
|
|
: ['abha-login', 'search-abha', 'mobile-verify'];
|
|
$otpSystem = $isAadhaar ? 'aadhaar' : 'abdm';
|
|
|
|
$payload = [
|
|
'scope' => $scope,
|
|
'loginHint' => 'index',
|
|
'loginId' => $encIndex,
|
|
'otpSystem' => $otpSystem,
|
|
'txnId' => $searchTxnId,
|
|
];
|
|
|
|
$resp = $this->curlCall($this->otpUrl, 'POST', $payload, [
|
|
'Authorization: Bearer ' . $token,
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-Client-ID: ' . $this->clientId,
|
|
]);
|
|
|
|
if ($resp['code'] === 401) {
|
|
$token = $this->getSessionToken(true);
|
|
$resp = $this->curlCall($this->otpUrl, 'POST', $payload, [
|
|
'Authorization: Bearer ' . $token,
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-Client-ID: ' . $this->clientId,
|
|
]);
|
|
}
|
|
|
|
if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) {
|
|
$newTxnId = (string) ($resp['json']['txnId'] ?? $searchTxnId);
|
|
$msg = (string) ($resp['json']['message'] ?? 'OTP sent successfully.');
|
|
return [
|
|
'ok' => 1,
|
|
'txn_id' => $newTxnId,
|
|
'message' => $msg,
|
|
];
|
|
}
|
|
|
|
$errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? 'Failed to request OTP from ABDM.');
|
|
return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']];
|
|
} catch (\Throwable $e) {
|
|
$this->log('error', '[AbdmV3DirectHelper] requestProfileOtp exception: ' . $e->getMessage());
|
|
return ['ok' => 0, 'error_text' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Step 3: Verify the OTP and obtain full profile + tokens.
|
|
* Upstream: POST /abha/api/v3/profile/login/verify
|
|
*
|
|
* @return array{ok: int, token?: string, accounts?: array, profile?: array, error_text?: string}
|
|
*/
|
|
public function verifyProfileOtp(string $otpTxnId, string $otp, string $authMethod = 'MOBILE_OTP'): array
|
|
{
|
|
$digits = preg_replace('/\D/', '', $otp);
|
|
if (strlen($digits) !== 6) {
|
|
return ['ok' => 0, 'error_text' => 'Enter a valid 6-digit OTP.'];
|
|
}
|
|
|
|
$authMethod = strtoupper(trim($authMethod));
|
|
$isAadhaar = in_array($authMethod, ['AADHAAR_OTP', 'AADHAAR', 'AADHAAR-VERIFY'], true);
|
|
|
|
try {
|
|
$token = $this->getSessionToken();
|
|
if ($token === '') {
|
|
return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.'];
|
|
}
|
|
|
|
$encOtp = $this->encrypt($digits);
|
|
$scope = $isAadhaar
|
|
? ['abha-login', 'aadhaar-verify']
|
|
: ['abha-login', 'mobile-verify'];
|
|
|
|
$payload = [
|
|
'scope' => $scope,
|
|
'authData' => [
|
|
'authMethods' => ['otp'],
|
|
'otp' => [
|
|
'txnId' => $otpTxnId,
|
|
'otpValue' => $encOtp,
|
|
],
|
|
],
|
|
];
|
|
|
|
$resp = $this->curlCall($this->verifyUrl, 'POST', $payload, [
|
|
'Authorization: Bearer ' . $token,
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-Client-ID: ' . $this->clientId,
|
|
]);
|
|
|
|
if ($resp['code'] === 401) {
|
|
$token = $this->getSessionToken(true);
|
|
$resp = $this->curlCall($this->verifyUrl, 'POST', $payload, [
|
|
'Authorization: Bearer ' . $token,
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-Client-ID: ' . $this->clientId,
|
|
]);
|
|
}
|
|
|
|
if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) {
|
|
$authResult = strtolower((string) ($resp['json']['authResult'] ?? ''));
|
|
if ($authResult === 'failed') {
|
|
$failMsg = (string) ($resp['json']['message'] ?? 'Invalid OTP entered.');
|
|
return ['ok' => 0, 'error_text' => $failMsg];
|
|
}
|
|
|
|
$userToken = (string) ($resp['json']['token'] ?? '');
|
|
$accounts = (array) ($resp['json']['accounts'] ?? []);
|
|
$firstAcc = isset($accounts[0]) && is_array($accounts[0]) ? $accounts[0] : [];
|
|
|
|
// Attempt to fetch full profile details and ABHA card using the userToken
|
|
$fullProfile = [];
|
|
$cardBase64 = '';
|
|
if ($userToken !== '') {
|
|
$fullProfile = $this->fetchProfileDetails($userToken, $token);
|
|
$cardBase64 = $this->fetchCardPng($userToken, $token);
|
|
}
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'txn_id' => (string) ($resp['json']['txnId'] ?? $otpTxnId),
|
|
'token' => $userToken,
|
|
'accounts' => $accounts,
|
|
'account' => $firstAcc,
|
|
'full_profile' => $fullProfile,
|
|
'card_base64' => $cardBase64,
|
|
];
|
|
}
|
|
|
|
$errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? 'OTP verification failed.');
|
|
return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']];
|
|
} catch (\Throwable $e) {
|
|
$this->log('error', '[AbdmV3DirectHelper] verifyProfileOtp exception: ' . $e->getMessage());
|
|
return ['ok' => 0, 'error_text' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* M1 12.1 VRFY_ABHA_401: Request OTP using 12-digit Aadhaar Number.
|
|
* Upstream: POST /abha/api/v3/profile/login/request/otp
|
|
*
|
|
* @return array{ok: int, txn_id?: string, message?: string, error_text?: string, resend_after?: int}
|
|
*/
|
|
public function requestLoginOtpByAadhaar(string $aadhaarNumber): array
|
|
{
|
|
$digits = preg_replace('/\D/', '', $aadhaarNumber);
|
|
if (strlen($digits) !== 12) {
|
|
return ['ok' => 0, 'error_text' => 'Aadhaar Number is not valid.'];
|
|
}
|
|
|
|
try {
|
|
$token = $this->getSessionToken();
|
|
if ($token === '') {
|
|
return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.'];
|
|
}
|
|
|
|
$encAadhaar = $this->encrypt($digits);
|
|
$payload = [
|
|
'scope' => ['abha-login', 'aadhaar-verify'],
|
|
'loginHint' => 'aadhaar',
|
|
'loginId' => $encAadhaar,
|
|
'otpSystem' => 'aadhaar',
|
|
];
|
|
|
|
$resp = $this->curlCall($this->otpUrl, 'POST', $payload, [
|
|
'Authorization: Bearer ' . $token,
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-Client-ID: ' . $this->clientId,
|
|
]);
|
|
|
|
if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) {
|
|
$newTxnId = (string) ($resp['json']['txnId'] ?? '');
|
|
$msg = (string) ($resp['json']['message'] ?? 'OTP sent to Aadhaar linked mobile number.');
|
|
return [
|
|
'ok' => 1,
|
|
'txn_id' => $newTxnId,
|
|
'message' => $msg,
|
|
'resend_after' => 60,
|
|
];
|
|
}
|
|
|
|
$errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? '');
|
|
if ($errMsg === '') {
|
|
$errMsg = 'Failed to request Aadhaar OTP from ABDM.';
|
|
}
|
|
if (stripos($errMsg, 'invalid') !== false && stripos($errMsg, 'aadhaar') !== false) {
|
|
$errMsg = 'Aadhaar Number is not valid.';
|
|
}
|
|
|
|
return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']];
|
|
} catch (\Throwable $e) {
|
|
log_message('error', '[AbdmV3DirectHelper] requestLoginOtpByAadhaar exception: ' . $e->getMessage());
|
|
return ['ok' => 0, 'error_text' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* M1 12.1 VRFY_ABHA_402, 403, 404: Verify Aadhaar OTP and fetch complete ABHA profile + card.
|
|
* Upstream: POST /abha/api/v3/profile/login/verify
|
|
*
|
|
* @return array{ok: int, txn_id?: string, token?: string, accounts?: array, account?: array, full_profile?: array, card_base64?: string, no_abha_found?: bool, error_text?: string}
|
|
*/
|
|
public function verifyLoginOtpByAadhaar(string $otpTxnId, string $otp): array
|
|
{
|
|
$digits = preg_replace('/\D/', '', $otp);
|
|
if (strlen($digits) !== 6) {
|
|
return ['ok' => 0, 'error_text' => 'Enter a valid 6-digit OTP.'];
|
|
}
|
|
|
|
try {
|
|
$token = $this->getSessionToken();
|
|
if ($token === '') {
|
|
return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.'];
|
|
}
|
|
|
|
$encOtp = $this->encrypt($digits);
|
|
$payload = [
|
|
'scope' => ['abha-login', 'aadhaar-verify'],
|
|
'authData' => [
|
|
'authMethods' => ['otp'],
|
|
'otp' => [
|
|
'txnId' => $otpTxnId,
|
|
'otpValue' => $encOtp,
|
|
],
|
|
],
|
|
];
|
|
|
|
$resp = $this->curlCall($this->verifyUrl, 'POST', $payload, [
|
|
'Authorization: Bearer ' . $token,
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-Client-ID: ' . $this->clientId,
|
|
]);
|
|
|
|
if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) {
|
|
$authResult = strtolower((string) ($resp['json']['authResult'] ?? ''));
|
|
if ($authResult === 'failed') {
|
|
$failMsg = (string) ($resp['json']['message'] ?? 'Incorrect OTP.');
|
|
return ['ok' => 0, 'error_text' => $failMsg];
|
|
}
|
|
|
|
$userToken = (string) ($resp['json']['token'] ?? '');
|
|
$accounts = (array) ($resp['json']['accounts'] ?? []);
|
|
$firstAcc = isset($accounts[0]) && is_array($accounts[0]) ? $accounts[0] : [];
|
|
|
|
// Check VRFY_ABHA_403: No ABHA user registered with this Aadhaar Number
|
|
if (empty($userToken) && empty($accounts)) {
|
|
return [
|
|
'ok' => 0,
|
|
'no_abha_found' => true,
|
|
'error_text' => 'NO ABHA user registered with this Aadhaar Number',
|
|
];
|
|
}
|
|
|
|
$fullProfile = [];
|
|
$cardBase64 = '';
|
|
if ($userToken !== '') {
|
|
$fullProfile = $this->fetchProfileDetails($userToken, $token);
|
|
$cardBase64 = $this->fetchCardPng($userToken, $token);
|
|
}
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'txn_id' => (string) ($resp['json']['txnId'] ?? $otpTxnId),
|
|
'token' => $userToken,
|
|
'accounts' => $accounts,
|
|
'account' => $firstAcc,
|
|
'full_profile' => $fullProfile,
|
|
'card_base64' => $cardBase64,
|
|
];
|
|
}
|
|
|
|
$errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? '');
|
|
|
|
// Check if error is VRFY_ABHA_403: ABHA does not exist for this Aadhaar
|
|
if (stripos($errMsg, 'no abha') !== false ||
|
|
stripos($errMsg, 'not found') !== false ||
|
|
stripos($errMsg, 'not exist') !== false ||
|
|
stripos($errMsg, 'no user') !== false ||
|
|
stripos($errMsg, 'no account') !== false) {
|
|
return [
|
|
'ok' => 0,
|
|
'no_abha_found' => true,
|
|
'error_text' => 'NO ABHA user registered with this Aadhaar Number',
|
|
'raw' => $resp['raw'],
|
|
];
|
|
}
|
|
|
|
if (stripos($errMsg, 'otp') !== false || stripos($errMsg, 'invalid') !== false || stripos($errMsg, 'incorrect') !== false) {
|
|
$errMsg = 'Incorrect OTP';
|
|
}
|
|
|
|
if ($errMsg === '') {
|
|
$errMsg = 'OTP verification failed.';
|
|
}
|
|
|
|
return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']];
|
|
} catch (\Throwable $e) {
|
|
$this->log('error', '[AbdmV3DirectHelper] verifyLoginOtpByAadhaar exception: ' . $e->getMessage());
|
|
return ['ok' => 0, 'error_text' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch full profile details using the profile X-Token.
|
|
* Upstream: GET /abha/api/v3/profile/account
|
|
*/
|
|
public function fetchProfileDetails(string $xToken, string $gatewayToken = ''): array
|
|
{
|
|
try {
|
|
$token = $gatewayToken !== '' ? $gatewayToken : $this->getSessionToken();
|
|
$headers = [
|
|
'X-Token: Bearer ' . $xToken,
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-Client-ID: ' . $this->clientId,
|
|
];
|
|
if ($token !== '') {
|
|
$headers[] = 'Authorization: Bearer ' . $token;
|
|
}
|
|
$resp = $this->curlCall($this->profileUrl, 'GET', [], $headers);
|
|
return is_array($resp['json']) ? $resp['json'] : [];
|
|
} catch (\Throwable $e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch official ABHA card base64 image using profile X-Token.
|
|
* Upstream: GET /abha/api/v3/profile/account/abha-card
|
|
*/
|
|
public function fetchCardPng(string $xToken, string $gatewayToken = ''): string
|
|
{
|
|
try {
|
|
$token = $gatewayToken !== '' ? $gatewayToken : $this->getSessionToken();
|
|
$headers = [
|
|
'X-Token: Bearer ' . $xToken,
|
|
'REQUEST-ID: ' . $this->generateUuid(),
|
|
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
|
|
'X-Client-ID: ' . $this->clientId,
|
|
];
|
|
if ($token !== '') {
|
|
$headers[] = 'Authorization: Bearer ' . $token;
|
|
}
|
|
$ch = curl_init($this->cardUrl);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 20,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_SSL_VERIFYHOST => 0,
|
|
]);
|
|
$raw = curl_exec($ch);
|
|
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($code >= 200 && $code < 300 && ! empty($raw)) {
|
|
// If returned as raw image binary, base64 encode it
|
|
if (! str_contains($raw, '{')) {
|
|
return base64_encode($raw);
|
|
}
|
|
// If returned as JSON with base64
|
|
$decoded = json_decode($raw, true);
|
|
if (is_array($decoded)) {
|
|
return (string) ($decoded['card'] ?? $decoded['card_base64'] ?? '');
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {}
|
|
return '';
|
|
}
|
|
}
|