eID sign-in¶
There are no passwords on this platform. No registration form, no e-mail OTP, no password reset — identity only ever arrives from an external source.
| Method | What | Status |
|---|---|---|
| eID Mongolia | Verification with the national eID (QR / App2App / national-ID push) | Primary |
| Google OAuth | Linking a Google account after eID verification | Secondary |
| Gerege SSO (OIDC) | Signing in through the ecosystem provider | Optional |
1. Signing in with eID¶
The app is an eID Mongolia relying party (pkg/eid, EID_* settings).
sequenceDiagram
participant U as Citizen
participant W as Web (BFF)
participant A as Go API
participant E as eID Mongolia
U->>W: "Sign in with eID"
W->>A: POST /v1/auth/eid/start (or /start-id)
A->>E: start session
E-->>A: session id + QR / deep link
A-->>W: QR code · deep link · sid
U->>E: scan QR / open app / approve push
loop every ~2.5s (holds the IdP up to 25s)
W->>A: POST /v1/auth/eid/poll {sid}
A->>E: session state?
end
E-->>A: COMPLETE + citizen data
A->>A: upsert users (by civil_id)
A-->>W: access + refresh tokens
W-->>U: sets httpOnly cookies, redirects to the dashboard
| Endpoint | What |
|---|---|
POST /api/v1/auth/eid/start |
Starts a session, returns a QR code and mobile deep link |
POST /api/v1/auth/eid/start-id |
Starts by national ID and pushes to the registered device |
POST /api/v1/auth/eid/poll |
The frontend long-polls until COMPLETE |
GET /api/v1/auth/status/{sid} |
Session state |
Why long polling?
The frontend polls about every 2.5 seconds while the backend holds the IdP for up to 25 seconds per poll. That keeps network chatter low and responsiveness high. This path has its own rate limiter (~60/min, burst 30).
User creation: on success the citizen is upserted keyed by civil_id
(the public RP receives civil_id, not national_id). New users default to the
user role.
2. Linking Google¶
pkg/google, GOOGLE_* settings. The button only appears when credentials are
configured.
| Endpoint | What |
|---|---|
POST /api/v1/auth/google |
Exchanges the code and signs in via (or links) the Google account attached to the eID user |
DELETE /api/v1/auth/google/link |
Unlinks |
3. Session lifecycle¶
Regardless of sign-in method the session is a JWT access + refresh pair:
| Endpoint | What |
|---|---|
POST /api/v1/auth/refresh |
Rotates the token pair |
POST /api/v1/auth/logout |
Revokes the refresh token |
Protections:
- A
kindclaim guard prevents using a refresh token as an access token. - The
User.TokensRevokedBeforecutoff rejects every token issued before a credential change. - Logout revokes the refresh token and deny-lists the access token (Redis).
- Changing
JWT_SECRETorJWT_ISSUERinvalidates every session.
Do not call refresh from an RSC
Refresh rotates the token, so calling it where cookies cannot be written
loses the session. tryRefresh in lib/api.ts probes cookie writability
first.
4. Super admin — a separate flow¶
Super admins live in a separate table (superadmin_accounts) from ordinary
admins. One person can therefore be an eID admin and a Google super admin at the
same time.
Onboarding is a multi-step wizard (/superadmin/onboard):
| Endpoint | Step |
|---|---|
POST /v1/auth/superadmin/onboard/google |
Google verification |
POST /v1/auth/superadmin/onboard/eid/{start,start-id,poll} |
eID verification |
POST /v1/auth/superadmin/onboard/email/{send,verify} |
E-mail OTP |
POST /v1/auth/superadmin/onboard/totp/{init,verify} |
TOTP setup and recovery codes |
POST /v1/auth/superadmin/mfa |
MFA on every subsequent sign-in |
TOTP secrets and recovery codes are encrypted with INTEGRATION_ENC_KEY.
Never rotate INTEGRATION_ENC_KEY
It encrypts super-admin TOTP secrets and integration OAuth tokens. Rotating it breaks both, irrecoverably.
The first super admin is bootstrapped from SUPERADMIN_EMAIL (or the database),
never through the API.
5. Sign-in hardening¶
| Control | Value |
|---|---|
| Rate limit | /v1/auth/* ~5/min per IP |
| Body cap | 4 KiB |
| Lockout | Failed attempts recorded in login_events, then locked |
| Security events | Suspicious activity written to security_events |
| Audit | Every mutation lands in the hash-chained audit log |
Further reading¶
- RBAC & super admin — how permissions are resolved
- OIDC provider — the platform as an issuer
- Connect an app (RP) — wiring your own app