Skip to content

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 kind claim guard prevents using a refresh token as an access token.
  • The User.TokensRevokedBefore cutoff rejects every token issued before a credential change.
  • Logout revokes the refresh token and deny-lists the access token (Redis).
  • Changing JWT_SECRET or JWT_ISSUER invalidates 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):

invite allow-list  →  Google  →  eID  →  e-mail OTP  →  TOTP + recovery codes
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