eID — Admin console (operator panel)¶
A guide for operators/developers: the eID platform's Admin console is a combined system of a
Next.js operator panel (admin/, port 3001) and the Go admin API (/v3/admin/*). Through it an
operator manages citizens, devices, certificates, sessions, KYC/DAN, organizations (Legal Person),
relying parties (RP), audit logs, and system status.
Separate app. The Admin console is a completely separate Next.js application from
web/(the citizen RP demo, port 3000). Code tree:admin/src/.
1. What it manages¶
| Section | Capability |
|---|---|
Citizens (/users) |
Search (etsi / РД / name / documentNumber), details + devices + certificates + audit |
Devices (/devices) |
List/search, details, deactivate lost devices |
Certificates (/certificates) |
List/search, status, revoke |
PKI (/pki, /org-ca) |
CA status, OCSP/CRL paths, e-Seal QSCD state |
Organizations (/organizations) |
Legal Person registration, representatives, issue e-Seal cert |
RP (/rps) |
Register/edit/deactivate/reactivate relying parties, rotate secret, manage subsystems |
Approvals (/approvals) |
4-eyes: another admin confirms sensitive operations |
Audit (/audit) |
Log of all operations + CSV export (eIDAS/ISO 27001) |
Admin users (/admins) |
Create/change-role/deactivate admins (SUPER_ADMIN only) |
System (/system) |
Version, uptime, NON-SECRET status of config/HSM/KYC/PKI |
2. Architecture — BFF (the browser never sees the secret)¶
The Admin console follows a Backend-for-Frontend (BFF) pattern. The browser NEVER sees the
admin session token — the token is stored only in the Next server's httpOnly cookie.
Browser ──fetch──▶ Next BFF (port 3001) ──Bearer──▶ Go admin API (/v3/admin/*)
(cookie: /api/login, /api/mfa, adminAuth middleware
admin_session, /api/proxy/[...path] + requireCap (RBAC)
httpOnly)
- Client → BFF. When pages call
api("users?q=…"), the request goes to the/api/proxy/*route (admin/src/lib/client.ts). - BFF → Go. The proxy route reads the session token from the cookie and forwards it as
Authorization: Bearer <token>to${ADMIN_BACKEND_URL}/v3/admin/<path>(admin/src/app/api/proxy/[...path]/route.ts). The token is never exposed to the browser, so it cannot be stolen via XSS. - Protection. Path-traversal allowlist (
[A-Za-z0-9._-], forbids./..), matching the Origin/Referer host againsthoston mutating requests (CSRF defense-in-depth, in addition tosameSite=strict), and masking upstream 5xx errors behind a generic message. - Auth guard. The
(app)layout callsgetMe()on the server and redirects to/loginif there is no valid session (admin/src/app/(app)/layout.tsx). - CSP/security headers.
admin/src/middleware.ts— nonce +strict-dynamic,connect-src 'self',frame-ancestors 'none', HSTS, and more.
Trust boundary. The Go side protects
/v3/admin/*with theadminAuthmiddleware (server/internal/httpapi/server.go). Order: (1)Authorization: Bearer <session token>→ real RBAC; (2)X-Admin-Key→ break-glass SUPER_ADMIN (constant-time comparison); (3) only on thedevprofile, ifSMARTID_ADMIN_API_KEYis empty, an open SUPER_ADMIN. On staging/prod it must NEVER fail-open — a Bearer or break-glass key is mandatory.
3. Login + MFA¶
Flow: email/password → TOTP (MFA) → session token. On the first login the user enrolls an authenticator via QR.
| Step | BFF route | Go endpoint | Description |
|---|---|---|---|
| 1. Login | POST /api/login |
POST /v3/admin/auth/login |
Verify email/password; on success an mfa-scope token |
| 2. MFA | POST /api/mfa |
POST /v3/admin/auth/mfa |
TOTP code → session-scope token |
| 3. Logout | POST /api/logout |
POST /v3/admin/auth/logout |
Delete cookie (token is stateless) |
- Password. Hashed and stored via
internal/admin/password.go; errors are generalized ("email or password incorrect") to protect against timing/enumeration. - TOTP (MFA). RFC 6238, HMAC-SHA1, 30-second step, 6 digits — implemented in-house without an
external library
(
server/internal/admin/totp.go). On the first login the server returns anotpauth://URI + secret; the login page turns it into a QR to add to Google/Microsoft Authenticator. The code has a ±1-step window (clock skew), and the used counter is stored to block replay (RFC 6238 §5.2). - Token. Stateless token signed with HMAC-SHA256 (no JWT library),
<b64url(payload)>.<b64url(HMAC)>(server/internal/admin/token.go).mfa-scope ~5 min,session-scope ~1 hour. - Cookie.
admin_mfa(5 min, awaiting MFA) andadmin_session(1 hour) — bothhttpOnly,sameSite=strict, andsecurein production. - Rate limit. login (per-IP+email) and MFA (per-IP) are throttled; on exceeding, 429 +
Retry-After. - Change password.
POST /v3/admin/auth/password— a logged-in admin changes their own password (confirming the current one). No cap required.
4. RBAC — role and capability¶
Each route declares the capability it requires; the requireCap middleware checks whether the
logged-in admin's role has that cap (least-privilege). Matrix:
server/internal/admin/roles.go.
Capabilities: rp:read, rp:write, org:read, org:write, user:read, device:read,
device:revoke, cert:read, cert:revoke, session:read, audit:read, admin:manage,
config:read, config:write.
Role → capability:
| Role | Granted capabilities |
|---|---|
SUPER_ADMIN |
all capabilities (includes admin management) |
RP_OPERATOR |
rp:read rp:write org:read org:write user:read session:read audit:read |
SUPPORT |
rp:read org:read user:read device:read device:revoke cert:read session:read audit:read |
AUDITOR |
rp:read org:read user:read device:read cert:read session:read audit:read config:read |
SECURITY_OFFICER |
org:read user:read device:read device:revoke cert:read cert:revoke session:read audit:read config:read |
If the cap is insufficient, the Go side returns 403 ("insufficient permission for this operation"). The frontend Nav shows all pages, but an unauthorized operation is blocked with a 403 at the backend.
5. Core capabilities and /v3/admin/* endpoints¶
All behind adminAuth. The required capability is shown alongside.
Citizen / device / certificate / session (read)¶
| Method + path | Cap | Description |
|---|---|---|
GET /users?q=&limit=&offset= |
user:read |
Search citizens (etsi/РД/name); if none found, search for the owner by documentNumber |
GET /users/{etsi} |
user:read |
Citizen + devices + certificates + audit |
GET /devices?q=&active= |
device:read |
Search devices |
GET /devices/{documentNumber} |
device:read |
Device details |
GET /certificates?q=&status= |
cert:read |
Search certificates |
GET /users/{etsi}/certificates |
cert:read |
A citizen's certificate history |
GET /sessions/{sessionId} |
session:read |
Secure view of a session |
Secret material is NEVER returned. The view functions (
userView/deviceView/certView/sessionView,handlers_admin_read.go) strip out the HSM handle,Enc(x_client), Paillier modulus, session secret/token, and the large cert base64, showing only the safe fields.
RP (relying party)¶
| Method + path | Cap | Description |
|---|---|---|
POST /relying-parties |
rp:write |
Register an RP — the API secret is returned ONLY here, ONCE |
GET /relying-parties, GET /relying-parties/{id} |
rp:read |
List / details |
PATCH /relying-parties/{id} |
rp:write |
Edit |
POST /relying-parties/{id}/deactivate | /reactivate |
rp:write |
Deactivate / reactivate |
POST /relying-parties/{id}/rotate-secret |
rp:write |
New secret (ONLY here, ONCE) |
RP subsystem. A subsystem is auto-registered by the RP itself (find-or-create); an admin can only list / rename / deactivate / merge:
| Method + path | Cap | Description |
|---|---|---|
GET /relying-parties/{id}/subsystems |
rp:read |
List of subsystems within an RP |
PATCH /relying-parties/{id}/subsystems/{sid} |
rp:write |
Edit a subsystem's name (display) |
POST /relying-parties/{id}/subsystems/{sid}/deactivate | /activate |
rp:write |
Deactivate / activate |
POST /relying-parties/{id}/subsystems/{sid}/merge |
rp:write |
Merge into another subsystem |
Full RP integration guide: RP_INTEGRATION.md. Subsystem model / wire: RP_SUBSYSTEMS.md.
Certificate revocation + PKI¶
| Method + path | Cap | Description |
|---|---|---|
POST /certificates/{serial}/revoke |
cert:revoke |
Revoke by serial number (OCSP/CRL + status + device deactivated); {reason} 0–10 |
POST /devices/{documentNumber}/deactivate |
device:revoke |
Deactivate a lost device (default reason=1 keyCompromise) |
GET /pki/status |
config:read |
CA subject/serial/validity, revocation count, OCSP/CRL URL, Org CA, e-Seal QSCD |
KYC / DAN¶
The KYC flow runs mostly through the mobile app + DAN callback (/v3/kyc/*, not behind the admin
console —
server.go mountKyc).
GET /v3/kyc/methods indicates the active KYC methods (dan / gsign / passport / citizenCard).
The kyc block in the system's GET /v3/admin/system shows the NON-SECRET status of the KYC provider.
Organization (Legal Person)¶
| Method + path | Cap | Description |
|---|---|---|
GET /organizations?q= |
org:read |
List/search |
GET /organizations/{etsi} |
org:read |
Details + representatives |
POST /organizations |
org:write |
Register (PENDING or directly ACTIVE) |
PATCH /organizations/{etsi} |
org:write |
Name / status (FSM) |
POST /organizations/{etsi}/representatives |
org:write |
Add a representative |
DELETE /organizations/{etsi}/representatives/{id} |
org:write |
Deactivate a representation |
POST /organizations/{etsi}/seal-certificate |
org:write |
Issue e-Seal (NTRMN) cert |
Organization onboarding: ORG_ONBOARDING.md.
Audit + statistics¶
| Method + path | Cap | Description |
|---|---|---|
GET /audit?type=&subject=&limit=&offset= |
audit:read |
Audit log (paginated) |
GET /audit/export?type=&subject= |
audit:read |
CSV export (eIDAS/ISO 27001 report) |
GET /stats |
audit:read |
Dashboard metrics + trends |
System / HSM status (read-only)¶
| Method + path | Cap | Description |
|---|---|---|
GET /system |
config:read |
Version, uptime, NON-SECRET status of security/kyc/pki/hsm/push config |
No secrets exposed.
adminSystemInfo(handlers_admin_system.go) NEVER exposes keys/passwords/API keys — it only indicates "whether configured" (e.g.swMasterKeyConfigured: true).
Admin user management (SUPER_ADMIN only)¶
| Method + path | Cap | Description |
|---|---|---|
GET /admins |
admin:manage |
Admin list (secret-free view) |
POST /admins |
admin:manage |
New admin (email + 8+ character password + role) |
PATCH /admins/{id} |
admin:manage |
Change role |
POST /admins/{id}/deactivate |
admin:manage |
Deactivate (instead of delete — preserves audit) |
4-eyes approval workflow¶
When SMARTID_ADMIN_REQUIRE_4EYES=true, a sensitive operation (cert revoke, device deactivate, RP
deactivate) is not executed immediately — it becomes a PENDING request (202) that another
admin confirms.
| Method + path | Cap | Description |
|---|---|---|
GET /approvals?status= |
audit:read |
Pending requests |
POST /approvals/{id}/approve |
(the operation's cap, dynamic) | Approve |
POST /approvals/{id}/reject |
(the operation's cap, dynamic) | Reject |
The approver must (a) have the operation's cap (e.g. cert revoke → cert:revoke) AND (b) be
different from the requester — an attempt to approve one's own request is blocked with
ErrSelfApproval (403)
(handlers_admin_approval.go).
6. Running¶
Frontend (admin console):
cd admin
npm run dev # port 3001 (next dev -p 3001)
npm run build
npm run lint
npm run test # vitest
One key env: ADMIN_BACKEND_URL (Go backend address, default http://localhost:8080).
Backend (Go admin API) — run from server/ (go run ./cmd/smartid). Admin/RBAC
env vars (server/internal/config/config.go, all SMARTID_*):
| Env | Description |
|---|---|
SMARTID_ADMIN_TOKEN_SECRET |
Session token HMAC secret (≥16 characters). If empty, admin authn is disabled |
SMARTID_ADMIN_SEED_EMAIL |
Email of the first SUPER_ADMIN (startup seed, idempotent) |
SMARTID_ADMIN_SEED_PASSWORD |
The first admin's password (seed only — must be changed afterward) |
SMARTID_ADMIN_SEED_ROLE |
Seed role (default SUPER_ADMIN) |
SMARTID_ADMIN_API_KEY |
Break-glass X-Admin-Key (SUPER_ADMIN outside RBAC; optional in prod) |
SMARTID_ADMIN_REQUIRE_4EYES |
Require 2-admin approval for sensitive operations (default false) |
Dev configuration. On the
devprofile, ifSMARTID_ADMIN_API_KEYis empty,/v3/admin/*becomes an open SUPER_ADMIN (no Bearer required) — for local development only. On staging/prod this NEVER happens; you must configureSMARTID_ADMIN_TOKEN_SECRET+ seed or a break-glass key.
7. Links¶
- RP integration (RP_INTEGRATION.md) — connecting a relying party
- Organization onboarding (ORG_ONBOARDING.md) — Legal Person / e-Seal
- PKI / CA onboarding (PKI_CA_ONBOARDING.md) — CA chain, OCSP/CRL
Source code:
- Go admin API:
server/internal/httpapi/handlers_admin_*.go, route:server/internal/httpapi/server.go - RBAC / authn:
server/internal/admin/(roles.go,service.go,totp.go,token.go,password.go) - Config:
server/internal/config/config.go - Frontend:
admin/src/