SA
Sanchalan Docs
DigiLocker eKYC

DigiLocker eKYC

DigiLocker is operated by NeGD (National e-Governance Division). Linking an employee's DigiLocker account to Sanchalan gives us a verified eKYC record (name, DOB, eKYC photo) without needing an AUA license. We use it as the legal identity proof for first-time face-template enrollment from the mobile app.

What's shipped

  • attendance_digilocker_links table — Aadhaar-correlation hash, name+DOB match flags, encrypted access token, sanitised claims.
  • DigiLockerClient service: PKCE OAuth2 flow, eKYC XML parser (XXE-defused).
  • DigiLockerController: status / start / callback (web) + mobile-start / mobile-exchange (token).
  • POST /api/v1/enrollment/face-template/digilocker: self-enrollment gated by a fresh (≤15 min) link with name + DOB matching the employee record.
  • Mobile screen DigiLockerEnrollScreen — full PKCE flow via flutter_web_auth_2, embeds the DigiLocker photo + a live face frame, averages, submits.

End-to-end flow

sequenceDiagram participant U as Employee participant App as Sanchalan app participant Be as backend (Sanchalan) participant DL as DigiLocker (NeGD) U->>App: tap "Enroll with DigiLocker" App->>App: generate PKCE (verifier, challenge, state) App->>Be: GET /api/digilocker/mobile/start?code_challenge=…&state=… Be-->>App: {authorization_url} App->>DL: open authorization_url (in-app browser) DL->>U: login + consent (DigiLocker UI) DL-->>App: redirect sanchalan-attendance://dl/cb?code=…&state=… App->>Be: POST /api/v1/digilocker/mobile/exchange
{code, code_verifier} Be->>DL: POST /oauth2/1/token (server-side; client_secret here) DL-->>Be: {access_token} Be->>DL: GET /oauth2/3/user Be->>DL: GET /oauth2/2/xml/eaadhaar DL-->>Be: eKYC XML (name, DOB, gender, last-4 Aadhaar, photo) Be->>Be: parse XML, compare name + DOB with employee_register Be->>Be: persist link (encrypted token + sanitised claims; no raw Aadhaar) Be-->>App: {link_id, name_match, dob_match, photo_b64} App->>App: embed DigiLocker photo with MobileFaceNet App->>App: live capture (1 frame) + on-device liveness App->>App: average DL embedding + live embedding → unit vector App->>Be: POST /api/v1/enrollment/face-template/digilocker
{link_id, embedding, model, frames_used, consent} Be->>Be: re-validate link (≤15 min, name_match, dob_match) Be->>Be: encrypt + store template (AES-256-GCM) Be-->>App: 201 {template_id, version, identity_proof: 'digilocker_eaadhaar'}

What we store / what we don't

DataWhereEncryption
Raw Aadhaar numberNever persisted.
Aadhaar-correlation hashattendance_digilocker_links.aadhaar_hash — one row per (Aadhaar, employee). Used to flag the same Aadhaar trying to link to multiple employees.Vault-salted SHA-256.
Raw DOBNever persisted. Only dob_year + match boolean.
eKYC photo bytesNever persisted. Returned once on the exchange response so the mobile can compute an embedding. Hash stored for tamper-detect.
DigiLocker access tokenattendance_digilocker_links.access_token_enc (Laravel Crypt-wrapped). Used only to refresh the link if needed.AES-256-CBC + HMAC.
Refresh tokenNever persisted. If we need refresh, we re-prompt the user.
Sanitised claimsattendance_digilocker_links.claims JSONB: {name, dob_year, gender, last_4}

Why DigiLocker for parliament use cases

  • No AUA license required. DigiLocker uses the user's own Aadhaar consent — Sanchalan is just a relying party, not authenticating against UIDAI ourselves.
  • Voluntary by design. The user signs in with their DigiLocker credentials; non-coercive.
  • Stronger than HR-mediated alone. The match between the DigiLocker photo and the live capture is cryptographically tied to the user's verified eKYC record.
  • Suits MPs. No AUA / §7 awkwardness. DigiLocker is widely accepted for member-of-public use cases.
  • Operational simplicity. Just a client_id + client_secret pair, no ASA gateway, no annual UIDAI audit.

Operator setup

  1. Apply for an "agency" registration on DigiLocker (Partner Onboarding portal). Receive client_id + client_secret.
  2. Register two redirect URIs:
    • Web: https://attendance.rajyasabha.digital/api/digilocker/callback
    • Mobile: sanchalan-attendance://dl/cb
  3. Populate .env:
    DIGILOCKER_ENABLED=true
    DIGILOCKER_BASE_URL=https://digilocker.meripehchaan.gov.in
    DIGILOCKER_CLIENT_ID=…
    DIGILOCKER_CLIENT_SECRET=…
    DIGILOCKER_REDIRECT_URI=https://attendance.rajyasabha.digital/api/digilocker/callback
    DIGILOCKER_AADHAAR_SALT=$(openssl rand -hex 32)
  4. Recreate the app container so config is reloaded.
  5. Verify GET /api/digilocker/status{"enabled":true,"has_client_id":true}.
  6. Pilot with a small group (10–20 employees) before announcing wider availability.

Wire format reference

POST /api/v1/digilocker/mobile/exchange

// request
{
  "code":          "...",         // from the redirect's ?code=...
  "code_verifier": "..."          // PKCE — 43-128 url-safe chars
}

// response (link persisted)
{
  "link_id":     42,
  "employee_id": 21,
  "name_match":  true,
  "dob_match":   true,
  "has_photo":   true,
  "photo_b64":   "..."            // ONCE — never persisted server-side
}

POST /api/v1/enrollment/face-template/digilocker

// request
{
  "link_id":     42,              // must be ≤15 minutes old, name+dob match
  "embedding":   "...",           // base64 of 512-d float32 (LE)
  "model":       "mobilefacenet",
  "frames_used": 2,
  "consent": {
    "version":       "1",
    "text_url":      "/static/face-consent-v1.html",
    "signature_b64": null
  }
}

// response (201)
{
  "template_id":     1234,
  "version":         1,
  "enrolled_at":     "2026-04-27T19:30:00.000000Z",
  "identity_proof":  "digilocker_eaadhaar"
}

What happens on a name/DOB mismatch

The link is still persisted (with name_match=false / dob_match=false) for forensic record, but POST /enrollment/face-template/digilocker returns 403. The user must visit HR to enroll in person, or fix the discrepancy in their DigiLocker / employee_register record.

Threat model

ThreatMitigation
Stolen mobile + open DigiLocker sessionPunch still requires the device's BiometricPrompt + the live face match. DigiLocker alone doesn't grant attendance.
Fraudulent DigiLocker photo (deepfake)Live capture during enrollment is averaged with the eKYC photo. A deepfake at enrollment would need to also match the user's actual face at every subsequent punch (which goes through the same liveness gate).
Same Aadhaar linking to two employeesaadhaar_hash uniqueness check at link time — second attempt rejected with audit alert.
Stale link reused after employee separationHard 15-minute window on the link. After that, employee must re-link.
DigiLocker access token leakEncrypted with Laravel Crypt + APP_KEY. Per-token; revoking APP_KEY invalidates all stored tokens. Future: Vault transit.