Skip to content

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 against host on mutating requests (CSRF defense-in-depth, in addition to sameSite=strict), and masking upstream 5xx errors behind a generic message.
  • Auth guard. The (app) layout calls getMe() on the server and redirects to /login if 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 the adminAuth middleware (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 the dev profile, if SMARTID_ADMIN_API_KEY is 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 an otpauth:// 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) and admin_session (1 hour) — both httpOnly, sameSite=strict, and secure in 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.

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 dev profile, if SMARTID_ADMIN_API_KEY is empty, /v3/admin/* becomes an open SUPER_ADMIN (no Bearer required) — for local development only. On staging/prod this NEVER happens; you must configure SMARTID_ADMIN_TOKEN_SECRET + seed or a break-glass key.

Source code: