Authentication via Diia: How We Integrated National Digital Identity into a Legal Platform
A passport on your smartphone — now the key to legal AI. We integrated Diia.Signature for authentication: deep link on mobile, QR code on desktop, ECDSA + SHA256 for hashing, and lawyers verify their identity with the same app they use to show documents at checkpoints. No passwords. No registration. One tap — and you are in.
Authentication via Diia: How We Integrated National Digital Identity
A passport on your smartphone — now the key to legal AI.
Why Diia, Not Yet Another OAuth
A legal platform works with confidential data. Google OAuth confirms you have a Gmail account. Diia confirms that you are you. The difference is fundamental: Diia is tied to a real document — a passport, ID card, or qualified electronic signature.
For a legal platform where attorney-client privilege and party identification are not optional but required by law, this is the only appropriate level of verification.
Architecture: Two Flows
Mobile (Deep Link)
- User taps "Sign in with Diia"
- Backend generates
requestId(ECDSA + SHA256, base64) - Deep link
diia://opens with session parameters - Diia app shows an authorization request
- User confirms → Diia sends a callback with data
- Backend verifies the signature, creates a JWT session
Desktop (QR Code)
- Backend requests a session from Diia API (
api2s.diia.gov.ua) - Receives a deep link → converts to QR code
- User scans QR with the Diia app on their phone
- From there — the same flow: confirmation → callback → JWT
Cryptography: Why ECDSA
The Diia API requires hashing requestId via ECDSA with SHA256. Not HMAC, not RSA — specifically ECDSA. This is the electronic signature standard in Ukraine (DSTU 4145), and Diia follows it.
requestId = base64(ECDSA_SHA256(branchId + offerId + requestId))
Every request is unique. Every signature is verified. Replay attacks are impossible.
What We Get from Diia
After successful authentication:
| Field | Description | |——-|————-| | Full name | Last name, first name, patronymic | | Date of birth | From the document | | Tax ID (IPN) | Individual tax number | | Document series/number | Passport or ID card | | Photo | From the document (optional) |
This is sufficient for full identification on a legal platform — and for future ERAU integration (attorney verification by tax ID).
Security
- Data is not stored on Diia's side — after the callback is delivered, the session is destroyed
- Session token is single-use — reuse is impossible
- JWT with short TTL — 24 hours, refresh via re-authentication
- Basic Auth for API — backend ↔ Diia communication is protected by separate credentials
UX: One Tap Instead of a Form
On mobile:
- Tap "Sign in with Diia" → the app opens → confirm → return to LEX AI authenticated
On desktop:
- See a QR code → point your camera → confirm in the app → the page auto-refreshes
No passwords. No registration forms. No "confirm your email." The same app you use to show your ID at a checkpoint — is now your key to legal AI.
Three Authentication Methods
LEX AI now supports three independent sign-in methods:
| Method | Trust Level | Best For | |——–|————|———-| | Google OAuth | Basic | Quick start, exploration | | Authentik SSO | Corporate | Law firms, organizations | | Diia | Government | Full identification, attorneys |
The lawyer chooses their level. The platform adapts.
Production Post-Mortem: Redis + Nginx
After deploying to production behind the AWS Application Load Balancer, authentication via Diia stopped working. Completely. Users tapped "Sign in with Diia" — and got an error.
The root causes turned out to be two, and both were infrastructure-related.
First: Redis key mismatch. During Diia session initiation we stored the state with one prefix, but during the callback we read with a different one. Redis silently returned null, the backend considered the session invalid and rejected the callback. The fix was unifying key prefixes in one place.
Second: Nginx was overwriting X-Forwarded-Proto. The ALB correctly passed https, but Nginx in its configuration forcefully set http. The callback URL was formed with the HTTP scheme, Diia rejected it as non-matching the registered redirect URI. The solution — Nginx now passes through the original header from the load balancer instead of substituting its own.
Both issues were not reproducible locally, because the dev environment has no ALB and Redis prefixes matched by accident. A reminder: staging should match production as closely as possible.