mozhuilungdsuo / laravel-cdac-e-hastakshar
Laravel package for CDAC e-Hastakshar PDF request and response handling.
Package info
github.com/mozhuilungdsuo/laravel-cdac-e-hastakshar
pkg:composer/mozhuilungdsuo/laravel-cdac-e-hastakshar
Requires
- php: ^8.2
- ext-imagick: *
- ext-openssl: *
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/filesystem: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- robrichards/xmlseclibs: ^3.1
- tecnickcom/tc-font-mirror: ^2.1
- tecnickcom/tc-lib-pdf: ^8.38
Requires (Dev)
- laravel/pint: ^1.27
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Laravel package for preparing CDAC e-Hastakshar PDF requests, signing request XML, handling eSign responses, and storing signed PDFs.
This package intentionally does not register routes, controllers, or views. Host applications should implement their own user flow and call the package service.
IAM broker integration
For a multi-application installation, place this package and the C-DAC credentials only in the IAM application. The IAM application can expose an OAuth-protected eSign broker API to client applications; those clients upload a document to IAM, receive an opaque handoff URL, and redirect the browser to C-DAC without receiving the ASP private key or C-DAC response XML.
The recommended broker flow is:
Client app → IAM broker API → single-use IAM handoff URL → C-DAC
Client app ← IAM completion redirect ← IAM C-DAC callback ← C-DAC
Store the private key in IAM with restrictive permissions, bind every broker transaction to its initiating OAuth client and user, and make the C-DAC callback point to IAM. The client completion redirect is only a notification: the client must use IAM's authenticated status/download API to retrieve the final document.
Local package development
Until a release containing the required feature is available on Packagist, an IAM application can temporarily reference this checkout as a symlinked Composer path repository:
{
"require": {
"mozhuilungdsuo/laravel-cdac-e-hastakshar": "dev-main"
},
"repositories": [
{
"type": "path",
"url": "../fresh-esign-app/packages/laravel-cdac-e-hastakshar",
"options": {"symlink": true}
}
]
}
Then run:
composer update mozhuilungdsuo/laravel-cdac-e-hastakshar
The symlink makes local changes available immediately. Once the package has been published, replace dev-main with the released version and remove the temporary repositories entry.
Installation
From Packagist:
composer require mozhuilungdsuo/laravel-cdac-e-hastakshar php artisan vendor:publish --tag=cdac-e-hastakshar-config
Install Requirements
This package requires the PHP Imagick extension because uploaded PDFs/images are converted to page images before the signature placeholder is prepared. It also requires the PHP OpenSSL extension to parse the signer certificate and extract the eSign UID token from successful responses.
Composer will fail with requires ext-imagick * but it is not present or requires ext-openssl * but it is not present until the extensions are installed for the same PHP binary used by Composer.
On macOS with Homebrew:
brew install imagemagick pecl install imagick php --ini php -m | grep -i imagick php -m | grep -i openssl
On Ubuntu/Debian:
sudo apt-get update sudo apt-get install php-imagick sudo systemctl restart apache2 # or, for PHP-FPM: sudo systemctl restart php*-fpm php -m | grep -i imagick php -m | grep -i openssl
If you use a versioned PHP package on Ubuntu/Debian, install the matching extension package:
sudo apt-get install php8.3-imagick
# replace 8.3 with your PHP version
On RHEL/CentOS/Fedora:
sudo dnf install php-pecl-imagick sudo systemctl restart httpd php -m | grep -i imagick php -m | grep -i openssl
Confirm PHP can see the extension:
php -v composer -vvv about php -m | grep imagick php -m | grep openssl
Keys
Private keys and certificates are intentionally not shipped with the package. Add them to the root folder of the app, commonly:
keys/ eSign_Staging_Private.key
Configure the path:
ESIGN_ASP_ID=your-asp-id ESIGN_PRIVATE_KEY=keys/eSign_Staging_Private.key ESIGN_PRIVATE_KEY_PASSPHRASE= ESIGN_SIGNATURE_PAGES=last
ESIGN_SIGNATURE_PAGES controls where the visible signer text is placed. Use last for the last page only, or all for every page.
Usage
Inject Mozhuilungdsuo\LaravelCdacEHastakshar\Services\EsignService in your own controller.
use Illuminate\Http\Request; use Mozhuilungdsuo\LaravelCdacEHastakshar\Services\EsignService; use RuntimeException; class EsignController { public function index() { return view('esign.index'); } public function store(Request $request, EsignService $esign) { $validated = $request->validate([ 'document' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:20480'], 'signer_name' => ['nullable', 'string', 'max:120'], 'signature_pages' => ['nullable', 'in:last,all'], ]); $signerName = $validated['signer_name'] ?? $request->user()?->name; $signaturePages = $validated['signature_pages'] ?? null; $uidToken = $request->user()?->esign_uid_token; $payload = $esign->createRequest($validated['document'], $signerName, $signaturePages, $uidToken); // Optional but recommended: persist $payload['transaction_id'] against // the current officer/user so the unauthenticated callback can save // $result['uid_token'] to the correct account. return view('esign.redirect', $payload); } public function response(Request $request, EsignService $esign) { $responseXml = (string) $request->input('eSignResponse', ''); if ($responseXml === '') { return view('esign.result', [ 'status' => 'failed', 'message' => 'The eSign response was empty.', ]); } try { $result = $esign->completeResponse($responseXml); } catch (RuntimeException $exception) { return view('esign.result', [ 'status' => 'failed', 'message' => $exception->getMessage(), ]); } if ($result['uid_token'] !== null) { // Look up the officer/user by $result['transaction_id'] and save // this 72-character token for future createRequest() calls. } if ($result['signer_name'] !== null) { // Optional: store or display the signer name from the certificate CN. } return view('esign.result', [ 'status' => 'completed', 'transactionId' => $result['transaction_id'], 'downloadUrl' => route('esign.download', $result['transaction_id']), ]); } public function download(string $transactionId, EsignService $esign) { return $esign->signedDownloadResponse($transactionId); } }
Create resources/views/esign/index.blade.php in the host app for the upload form:
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{ __('eSign document') }}</title> </head> <body> <main style="max-width: 720px; margin: 48px auto; font-family: sans-serif;"> <h1>{{ __('eSign document') }}</h1> <p>{{ __('Upload a PDF or image to prepare it for CDAC e-Hastakshar.') }}</p> <form method="POST" action="{{ route('esign.store') }}" enctype="multipart/form-data"> @csrf <div> <label for="signer_name">{{ __('Signer name') }}</label> <input id="signer_name" type="text" name="signer_name" value="{{ old('signer_name', auth()->user()?->name) }}" maxlength="120" > </div> @error('signer_name') <p style="color: #b91c1c;">{{ $message }}</p> @enderror <div> <label for="signature_pages">{{ __('Signer text pages') }}</label> <select id="signature_pages" name="signature_pages"> <option value="last" @selected(old('signature_pages', config('esign.signature_pages', 'last')) !== 'all')> {{ __('Last page') }} </option> <option value="all" @selected(old('signature_pages', config('esign.signature_pages', 'last')) === 'all')> {{ __('All pages') }} </option> </select> </div> @error('signature_pages') <p style="color: #b91c1c;">{{ $message }}</p> @enderror <div> <label for="document">{{ __('Document') }}</label> <input id="document" type="file" name="document" accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png" required > </div> @error('document') <p style="color: #b91c1c;">{{ $message }}</p> @enderror <button type="submit" style="margin-top: 16px;"> {{ __('Start eSign') }} </button> </form> </main> </body> </html>
Create resources/views/esign/redirect.blade.php in the host app to submit the generated request to CDAC:
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{ __('Redirecting to eSign') }}</title> </head> <body> <form action="{{ $endpoint }}" method="post" id="esign-request-form"> <input type="hidden" id="eSignRequest" name="eSignRequest" value="{{ $request_xml }}"> <input type="hidden" id="aspTxnID" name="aspTxnID" value="{{ $txn }}"> <input type="hidden" id="Content-Type" name="Content-Type" value="application/xml"> <noscript> <button type="submit">{{ __('Continue to eSign') }}</button> </noscript> </form> <script> document.getElementById('esign-request-form').submit(); </script> </body> </html>
Create resources/views/esign/result.blade.php in the host app for success/failure responses:
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{ __('eSign result') }}</title> </head> <body> <main style="max-width: 720px; margin: 48px auto; font-family: sans-serif;"> @if ($status === 'completed') <h1>{{ __('eSign completed') }}</h1> <p>{{ __('The signed PDF has been saved and is ready to download.') }}</p> <p>{{ __('Transaction') }}: {{ $transactionId }}</p> <a href="{{ $downloadUrl }}">{{ __('Download') }}</a> @else <h1>{{ __('eSign failed') }}</h1> <p>{{ $message }}</p> @endif </main> </body> </html>
Example host-app routes:
use App\Http\Controllers\EsignController; use Illuminate\Support\Facades\Route; Route::get('esign', [EsignController::class, 'index'])->name('esign.index'); Route::post('esign', [EsignController::class, 'store'])->name('esign.store'); Route::post('esign/response', [EsignController::class, 'response'])->name('esign.response'); Route::get('esign/{transactionId}/download', [EsignController::class, 'download'])->name('esign.download');
For Laravel's application bootstrap middleware configuration, exclude the callback route from CSRF validation:
$middleware->validateCsrfTokens(except: [ 'esign/response', ]);
Direct PDF Signing
Use createDirectRequest() when you need to sign an existing PDF without rebuilding its pages or adding visible text. This is the safer mode for adding another signature to a PDF that is already signed because it appends a new incremental PDF revision with an invisible signature field.
$payload = $esign->createDirectRequest( document: $validated['document'], uidToken: $user->esign_uid_token, signatureFieldName: 'Aadhaar eSign Approval', );
The response is completed with the same completeResponse() method:
$result = $esign->completeResponse($responseXml);
Use the original createRequest() method when you want the package to prepare a visible signature appearance such as Digitally Signed by: .... Use createDirectRequest() when preserving an already signed PDF is more important than adding visible signer text.
UID Token Reuse
For the first eSign by an officer/user, call createRequest() without a UID token. The generated request XML will keep ekycId="", so CDAC will perform the normal Aadhaar authentication flow.
After CDAC posts back a successful response, completeResponse() returns uid_token when it can extract the 72-character token from UserX509Certificate. It also returns signer_name from the certificate common name (CN) when available:
$result = $esign->completeResponse($responseXml); if ($result['uid_token'] !== null) { // Save this against the officer/eSign user in your application. } if ($result['signer_name'] !== null) { // Example: YANGER LONGKUMER }
If you only need to inspect the response certificate and do not need to complete the PDF signing flow, use:
$details = $esign->certificateDetailsFromResponse($responseXml); $details['uid_token']; // 72-character UID token, or null $details['signer_name']; // certificate CN, or null
For the next eSign by the same officer/user, pass the saved token as the fourth argument:
$payload = $esign->createRequest( document: $validated['document'], signerName: $signerName, signaturePages: $signaturePages, uidToken: $user->esign_uid_token, );
The package validates supplied UID tokens as exactly 72 characters and places the token in the request XML as ekycId="...".
The response callback is commonly excluded from authentication and CSRF protection, so do not rely on auth()->user() inside the callback. Persist the returned $payload['transaction_id'] before redirecting to CDAC, map it to your officer/user, then use $result['transaction_id'] in the callback to find the same user and save $result['uid_token'].
Dependencies
The package declares its runtime dependencies in composer.json, including:
robrichards/xmlseclibstecnickcom/tc-lib-pdftecnickcom/tc-font-mirrorext-imagickext-openssl
Composer will install those dependencies when this package is installed.