Web, mobile, desktop, эсвэл X-Road consumer-аас Гэрэгэ ID-ээр login, document signing, KYC хэрхэн нэгтгэх вэ — кодын жишээ, SDK, sandbox test.gerege.mn-тэй.
Хэрэглэгч "Login with Gerege" товч дарна → Гэрэгэ ID мобайл app-руу push notification ирнэ → биометрик баталгаажуулсны дараа таны системд session нээгдэнэ.
Гэрээний PDF/JSON-ийг hash хийж sign session эхлүүлнэ. Signed XAdES/JAdES баталгаа TSA-аар timestamp хийгдэн legally binding нотолгоо болно.
Иргэний бүртгэл дээр баталгаажсан овог, нэр, төрсөн он, регистр, иргэний харьяалал-уудыг login дараа JWT доторх claim-аар хүлээж авна.
Танай app-аас backend руу: standard OAuth/session. Backend-аас Гэрэгэ-руу: X-Road mTLS + signed message. End-to-end identity baталгаа.
X-Road consumer SS суулгахгүй бол backend нь шууд https://ca.gerege.mn/rp/v1/* RP API руу хандах боломжтой (API key + IP whitelist). MVP-д тохиромжтой.
Login button → backend session initiate → poll → callback. Frontend нь зөвхөн UI, бүх crypto handshake backend дээр.
async function loginWithGerege() {
// 1. Backend-руу session эхлүүлэх хүсэлт
const init = await fetch("/api/auth/initiate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
national_id: prompt("Регистрийн дугаар:"),
display_text: "MyApp руу нэвтрэх",
}),
}).then(r => r.json());
setStatus(`Гэрэгэ ID app-аас баталгаажуулна уу. PIN: ${init.pin}`);
// 2. Status polling — Гэрэгэ ID mobile-аас confirm/refuse хүртэл
const pollId = setInterval(async () => {
const s = await fetch(`/api/auth/status/${init.session_id}`).then(r => r.json());
if (s.status === "CONFIRMED") {
clearInterval(pollId);
window.location.href = "/dashboard"; // session cookie set
} else if (s.status === "REFUSED" || s.status === "EXPIRED") {
clearInterval(pollId);
setStatus(`Цуцалгдсан: ${s.status}`);
}
}, 1500);
}
// POST /api/auth/initiate
func handleInitiate(w http.ResponseWriter, r *http.Request) {
var req struct {
NationalID string `json:"national_id"`
DisplayText string `json:"display_text"`
}
json.NewDecoder(r.Body).Decode(&req)
body, _ := json.Marshal(map[string]string{
"national_id": req.NationalID,
"display_text": req.DisplayText,
})
apiReq, _ := http.NewRequest("POST",
"https://ca.gerege.mn/rp/v1/auth/initiate",
bytes.NewReader(body))
apiReq.Header.Set("Authorization", "Bearer "+os.Getenv("RP_API_KEY"))
apiReq.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(apiReq)
defer resp.Body.Close()
io.Copy(w, resp.Body) // {session_id, pin, expires_at}
}
// GET /api/auth/status/{session_id}
func handleStatus(w http.ResponseWriter, r *http.Request) {
sid := r.PathValue("session_id")
apiReq, _ := http.NewRequest("GET",
"https://ca.gerege.mn/rp/v1/auth/session/"+sid, nil)
apiReq.Header.Set("Authorization", "Bearer "+os.Getenv("RP_API_KEY"))
resp, _ := http.DefaultClient.Do(apiReq)
defer resp.Body.Close()
io.Copy(w, resp.Body) // {status: PENDING|CONFIRMED|REFUSED, user?: {...}}
}
Native mobile app дээр universal-link / app-link-аар Гэрэгэ ID app руу шилжүүлж auth flow эхлүүлнэ.
import Foundation
class GeregeLogin {
let backendURL = "https://api.myapp.mn/auth"
func login(nationalID: String) async throws -> User {
// 1. Initiate via backend
let body = ["national_id": nationalID, "display_text": "MyApp нэвтрэх"]
var req = URLRequest(url: URL(string: "\(backendURL)/initiate")!)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, _) = try await URLSession.shared.data(for: req)
let init_ = try JSONDecoder().decode(InitResp.self, from: data)
// 2. Open Gerege ID app via universal link to auto-confirm
if let url = URL(string: "gerege-id://auth/\(init_.session_id)") {
await UIApplication.shared.open(url)
}
// 3. Poll backend status
while true {
try await Task.sleep(nanoseconds: 1_500_000_000)
let s = try await fetchStatus(init_.session_id)
if s.status == "CONFIRMED" { return s.user! }
if s.status == "REFUSED" || s.status == "EXPIRED" {
throw NSError(domain: "Gerege", code: -1)
}
}
}
}
class GeregeLogin(private val backend: String = "https://api.myapp.mn/auth") {
suspend fun login(nationalId: String): User {
val client = OkHttpClient()
val body = JSONObject().apply {
put("national_id", nationalId)
put("display_text", "MyApp нэвтрэх")
}.toString().toRequestBody("application/json".toMediaType())
// 1. Initiate
val initReq = Request.Builder().url("$backend/initiate").post(body).build()
val initResp = client.newCall(initReq).execute().use {
JSONObject(it.body!!.string())
}
val sessionId = initResp.getString("session_id")
// 2. Open Gerege ID via app link
context.startActivity(Intent(Intent.ACTION_VIEW,
Uri.parse("https://gerege.mn/auth/$sessionId")))
// 3. Poll
while (true) {
delay(1500)
val s = client.newCall(Request.Builder()
.url("$backend/status/$sessionId").build()).execute().use {
JSONObject(it.body!!.string())
}
when (s.getString("status")) {
"CONFIRMED" -> return User.fromJson(s.getJSONObject("user"))
"REFUSED", "EXPIRED" -> throw IOException("auth ${s.getString("status")}")
}
}
}
}
import { Linking } from 'react-native';
export async function loginWithGerege(nationalId: string) {
const init = await fetch('https://api.myapp.mn/auth/initiate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ national_id: nationalId, display_text: 'MyApp' }),
}).then(r => r.json());
// Open Gerege ID app
await Linking.openURL(`gerege-id://auth/${init.session_id}`);
// Poll until confirmed
while (true) {
await new Promise(r => setTimeout(r, 1500));
const s = await fetch(`https://api.myapp.mn/auth/status/${init.session_id}`)
.then(r => r.json());
if (s.status === 'CONFIRMED') return s.user;
if (s.status === 'REFUSED' || s.status === 'EXPIRED') throw new Error(s.status);
}
}
Electron, Tauri, .NET, эсвэл native app — embedded webview-аар browser flow ашиглах эсвэл system browser нээх.
const { BrowserWindow, shell } = require('electron');
async function loginWithGerege(nationalId) {
const init = await fetch('https://api.myapp.mn/auth/initiate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ national_id: nationalId,
display_text: 'MyDesktopApp' }),
}).then(r => r.json());
// Open user's mobile via QR or web fallback
const qrWin = new BrowserWindow({ width: 400, height: 500 });
qrWin.loadURL(`https://gerege.mn/qr?session=${init.session_id}`);
while (true) {
await new Promise(r => setTimeout(r, 1500));
const s = await fetch(`https://api.myapp.mn/auth/status/${init.session_id}`)
.then(r => r.json());
if (s.status === 'CONFIRMED') {
qrWin.close();
return s.user;
}
}
}
public class GeregeAuth {
private readonly HttpClient _http = new();
private const string BACKEND = "https://api.myapp.mn/auth";
public async Task<User> LoginAsync(string nationalId) {
var init = await _http.PostAsJsonAsync($"{BACKEND}/initiate",
new { national_id = nationalId, display_text = "MyApp" });
var initData = await init.Content.ReadFromJsonAsync<InitResp>();
// Open system browser to QR page
Process.Start(new ProcessStartInfo {
FileName = $"https://gerege.mn/qr?session={initData.SessionId}",
UseShellExecute = true,
});
while (true) {
await Task.Delay(1500);
var s = await _http.GetFromJsonAsync<StatusResp>(
$"{BACKEND}/status/{initData.SessionId}");
if (s.Status == "CONFIRMED") return s.User;
if (s.Status == "REFUSED" || s.Status == "EXPIRED")
throw new Exception($"auth {s.Status}");
}
}
}
Production-д mTLS + signed message + audit log-той X-Road consumer pattern. RP API key-тай харьцуулахад илүү secure ба audit-ready.
Байгууллагаа X-Road MN instance-д member болгож, Security Server суулгана. Дэлгэрэнгүй: x-road.mn
SS UI → Clients → Add subsystem (жишээ CLIENT-APP). Энэ subsystem нь consumer identity болно.
Гэрэгэ-ийн SS дээрх geregeIdAuth, geregeIdSign service-уудад таны subsystem нэмэхийг хүснэ.
Танай backend нь өөрийн SS-ийн внутр HTTP endpoint http://ss.local:8080-руу REST request явуулна. SS нь X-Road handshake + signing хийж producer руу дамжуулна.
// X-Road REST call to Gerege ID auth service
// SS internal endpoint: http://ss.local:8080
func authViaXRoad(nationalID string) (*Session, error) {
req, _ := http.NewRequest("POST",
"http://ss.local:8080/r1/MN/COM/6235972/GEREGE-ID/auth/initiate",
strings.NewReader(`{"national_id":"`+nationalID+`"}`))
// Required X-Road headers
req.Header.Set("X-Road-Client",
"MN/COM/6884857/CLIENT-APP") // your subsystem
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
var s Session
json.NewDecoder(resp.Body).Decode(&s)
return &s, nil
}
// URL pattern: /r1/{instance}/{class}/{member}/{subsystem}/{service}/{path}
// instance = MN
// class = COM (commercial) | GOV | NGO
// member = 6235972 (Gerege member code)
// subsystem = GEREGE-ID
// service = auth (with subpath /initiate)
| Шинж | Direct REST | X-Road |
|---|---|---|
| Setup | API key + IP whitelist | SS install + subsystem reg |
| mTLS | Optional | Required (cert chain) |
| Message signing | None | XAdES-T per request |
| Audit log | Backend only | SS message log (legally binding) |
| Use case | MVP / startup | Production / banks / govt |
test.gerege.mn-д бид login + sign flow-ийг бодит ss.gerege.mn ашиглан түүхэн харуулсан. Source code MIT license-тэй.