@gerege-systems/eid-mongolia-sdk 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -102
- package/dist/button.cjs +952 -0
- package/dist/button.d.cts +79 -0
- package/dist/button.d.ts +79 -0
- package/dist/button.js +923 -0
- package/dist/index.cjs +350 -282
- package/dist/index.d.cts +122 -142
- package/dist/index.d.ts +122 -142
- package/dist/index.js +349 -278
- package/package.json +12 -4
package/dist/index.js
CHANGED
|
@@ -167,13 +167,6 @@ function routePath(route, ...args) {
|
|
|
167
167
|
if (i !== args.length) throw new Error(`eid: route ${route.path}: \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0438\u043B\u04AF\u04AF`);
|
|
168
168
|
return out;
|
|
169
169
|
}
|
|
170
|
-
var OIDC_EXPECT = {
|
|
171
|
-
defaultScope: "openid",
|
|
172
|
-
pkceMethod: "S256",
|
|
173
|
-
idTokenAlg: "ES256",
|
|
174
|
-
/** `Authorization: Basic` (RFC 6749 §2.3.1). */
|
|
175
|
-
tokenEndpointAuthMethod: "client_secret_basic"
|
|
176
|
-
};
|
|
177
170
|
|
|
178
171
|
// src/crypto.ts
|
|
179
172
|
import { createHash, randomBytes } from "crypto";
|
|
@@ -1590,6 +1583,8 @@ var EidClient = class {
|
|
|
1590
1583
|
token;
|
|
1591
1584
|
session;
|
|
1592
1585
|
validator;
|
|
1586
|
+
/** `credentials.rpName` — иргэний утсанд харагдах RP нэр. */
|
|
1587
|
+
rpName;
|
|
1593
1588
|
constructor(cfg) {
|
|
1594
1589
|
if (!cfg.credentials?.apiSecret) throw new Error("EidClient: credentials.apiSecret \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
1595
1590
|
const base = resolveBaseUrl(cfg.baseUrl);
|
|
@@ -1614,6 +1609,7 @@ var EidClient = class {
|
|
|
1614
1609
|
this.token = new TokenApi(http, cfg.credentials);
|
|
1615
1610
|
this.session = new SessionApi(http);
|
|
1616
1611
|
this.validator = new ResponseValidator(cfg.trust ?? {});
|
|
1612
|
+
this.rpName = cfg.credentials.rpName;
|
|
1617
1613
|
}
|
|
1618
1614
|
};
|
|
1619
1615
|
|
|
@@ -1687,293 +1683,371 @@ function qrDeviceLinkTicker(input, onLink, opts) {
|
|
|
1687
1683
|
return () => clearInterval(t);
|
|
1688
1684
|
}
|
|
1689
1685
|
|
|
1690
|
-
// src/
|
|
1691
|
-
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
"invalid_request",
|
|
1702
|
-
"invalid_scope",
|
|
1703
|
-
"unauthorized_client",
|
|
1704
|
-
"unsupported_response_type",
|
|
1705
|
-
"request_not_supported",
|
|
1706
|
-
"temporarily_unavailable",
|
|
1707
|
-
"server_error",
|
|
1708
|
-
"invalid_client",
|
|
1709
|
-
"invalid_grant",
|
|
1710
|
-
"unsupported_grant_type"
|
|
1711
|
-
]);
|
|
1712
|
-
var EidOidcError = class extends EidError {
|
|
1713
|
-
constructor(code, message, description) {
|
|
1714
|
-
super(message);
|
|
1715
|
-
this.code = code;
|
|
1716
|
-
this.description = description;
|
|
1717
|
-
}
|
|
1718
|
-
code;
|
|
1719
|
-
description;
|
|
1720
|
-
};
|
|
1721
|
-
var EidOidcClient = class {
|
|
1722
|
-
constructor(cfg) {
|
|
1723
|
-
this.cfg = cfg;
|
|
1724
|
-
if (!cfg.clientId) throw new EidError("EidOidcClient: clientId \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
1725
|
-
if (!cfg.clientSecret) throw new EidError("EidOidcClient: clientSecret \u0445\u043E\u043E\u0441\u043E\u043D");
|
|
1726
|
-
if (!/^https:\/\//.test(cfg.redirectUri ?? "")) throw new EidError("EidOidcClient: redirectUri https:// \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439");
|
|
1727
|
-
this.issuer = (cfg.issuer ?? EID_OIDC_ISSUER).replace(/\/+$/, "");
|
|
1728
|
-
this.scope = cfg.scope ?? OIDC_EXPECT.defaultScope;
|
|
1729
|
-
}
|
|
1730
|
-
cfg;
|
|
1731
|
-
issuer;
|
|
1732
|
-
scope;
|
|
1733
|
-
discovery;
|
|
1734
|
-
jwks;
|
|
1735
|
-
/** Discovery document (1 цаг cache). `issuer` нь тохиргоотой яг таарах ёстой (OIDC Discovery §4.3). */
|
|
1736
|
-
discover() {
|
|
1737
|
-
const c = this.discovery;
|
|
1738
|
-
if (c && Date.now() - c.at < CACHE_MS) return c.d;
|
|
1739
|
-
const d = this.fetchDiscovery();
|
|
1740
|
-
this.discovery = { at: Date.now(), d };
|
|
1741
|
-
d.catch(() => {
|
|
1742
|
-
if (this.discovery?.d === d) this.discovery = void 0;
|
|
1743
|
-
});
|
|
1744
|
-
return d;
|
|
1745
|
-
}
|
|
1746
|
-
async fetchDiscovery() {
|
|
1747
|
-
const d = await this.getJson(this.issuer + "/.well-known/openid-configuration");
|
|
1748
|
-
if (!d || d.issuer !== this.issuer) {
|
|
1749
|
-
throw new EidOidcError("discovery_failed", `OIDC discovery-\u0438\u0439\u043D issuer \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439 (\u0445\u04AF\u043B\u044D\u044D\u0441\u044D\u043D ${this.issuer})`);
|
|
1750
|
-
}
|
|
1751
|
-
for (const k of ["authorization_endpoint", "token_endpoint", "jwks_uri"]) {
|
|
1752
|
-
if (typeof d[k] !== "string" || !d[k].startsWith("https://")) {
|
|
1753
|
-
throw new EidOidcError("discovery_failed", `OIDC discovery: ${k} \u0431\u0443\u0440\u0443\u0443`);
|
|
1686
|
+
// src/login.ts
|
|
1687
|
+
import { createHash as createHash5, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1688
|
+
function memoryLoginStore() {
|
|
1689
|
+
const m = /* @__PURE__ */ new Map();
|
|
1690
|
+
return {
|
|
1691
|
+
get(k) {
|
|
1692
|
+
const e = m.get(k);
|
|
1693
|
+
if (!e) return void 0;
|
|
1694
|
+
if (e.exp <= Date.now()) {
|
|
1695
|
+
m.delete(k);
|
|
1696
|
+
return void 0;
|
|
1754
1697
|
}
|
|
1698
|
+
return e.tx;
|
|
1699
|
+
},
|
|
1700
|
+
set(k, tx, ttl) {
|
|
1701
|
+
const now = Date.now();
|
|
1702
|
+
if (m.size > 1e3) {
|
|
1703
|
+
for (const [key, e] of m) if (e.exp <= now) m.delete(key);
|
|
1704
|
+
}
|
|
1705
|
+
m.set(k, { tx, exp: now + ttl });
|
|
1706
|
+
},
|
|
1707
|
+
delete(k) {
|
|
1708
|
+
return m.delete(k);
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
var CALLBACK_PATH = "/auth/eid/callback";
|
|
1713
|
+
var ID_RE = /^(\p{L}{2}[0-9]{8}|[0-9]{8,14}|PNOMN-[0-9]{8,14})$/u;
|
|
1714
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
1715
|
+
var B64URL_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
1716
|
+
var IOS_BROWSERS = [
|
|
1717
|
+
[/CriOS/i, "googlechromes"],
|
|
1718
|
+
[/EdgiOS/i, "microsoft-edge-https"],
|
|
1719
|
+
[/FxiOS/i, "firefox"],
|
|
1720
|
+
[/OPT\//i, "touch-https"],
|
|
1721
|
+
[/OPiOS|OPR\//i, "opera-https"],
|
|
1722
|
+
[/DuckDuckGo/i, "ddgQuickLink"]
|
|
1723
|
+
];
|
|
1724
|
+
var ANDROID_BROWSERS = [
|
|
1725
|
+
[/EdgA\//i, "com.microsoft.emmx"],
|
|
1726
|
+
[/Opera Mini/i, "com.opera.mini.native"],
|
|
1727
|
+
[/OPR\//i, "com.opera.browser"],
|
|
1728
|
+
[/SamsungBrowser\//i, "com.sec.android.app.sbrowser"],
|
|
1729
|
+
[/YaBrowser\//i, "com.yandex.browser"],
|
|
1730
|
+
[/DuckDuckGo\//i, "com.duckduckgo.mobile.android"],
|
|
1731
|
+
[/Vivaldi\//i, "com.vivaldi.browser"],
|
|
1732
|
+
[/Firefox\//i, "org.mozilla.firefox"],
|
|
1733
|
+
[/Chrome\//i, "com.android.chrome"]
|
|
1734
|
+
];
|
|
1735
|
+
function withReturnHint(callbackUrl, userAgent) {
|
|
1736
|
+
const u = new URL(callbackUrl);
|
|
1737
|
+
const pick = (t) => t.find(([re]) => re.test(userAgent))?.[1];
|
|
1738
|
+
if (/iPhone|iPad|iPod/i.test(userAgent)) {
|
|
1739
|
+
const v = pick(IOS_BROWSERS);
|
|
1740
|
+
if (v) u.searchParams.set("retScheme", v);
|
|
1741
|
+
} else if (/Android/i.test(userAgent)) {
|
|
1742
|
+
const v = pick(ANDROID_BROWSERS);
|
|
1743
|
+
if (v) u.searchParams.set("retPkg", v);
|
|
1744
|
+
}
|
|
1745
|
+
return u.toString();
|
|
1746
|
+
}
|
|
1747
|
+
function header(h, name) {
|
|
1748
|
+
const v = h[name] ?? h[name.toLowerCase()];
|
|
1749
|
+
return (Array.isArray(v) ? v[0] : v) ?? "";
|
|
1750
|
+
}
|
|
1751
|
+
function readCookie(h, name) {
|
|
1752
|
+
for (const part of header(h, "cookie").split(";")) {
|
|
1753
|
+
const i = part.indexOf("=");
|
|
1754
|
+
if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();
|
|
1755
|
+
}
|
|
1756
|
+
return "";
|
|
1757
|
+
}
|
|
1758
|
+
var sha256hex = (s) => createHash5("sha256").update(s).digest("hex");
|
|
1759
|
+
function safeEqualHex(a, b) {
|
|
1760
|
+
const x = Buffer.from(a, "hex");
|
|
1761
|
+
const y = Buffer.from(b, "hex");
|
|
1762
|
+
return x.length === y.length && x.length > 0 && timingSafeEqual2(x, y);
|
|
1763
|
+
}
|
|
1764
|
+
function toIdentity(r, v, flowType) {
|
|
1765
|
+
const s = v.parsedSubject;
|
|
1766
|
+
const etsi = s.serialNumber ?? "";
|
|
1767
|
+
const m = /^PNOMN-([0-9]+)$/.exec(etsi);
|
|
1768
|
+
return {
|
|
1769
|
+
etsi,
|
|
1770
|
+
civilId: m ? m[1] : null,
|
|
1771
|
+
givenName: s.givenName ?? null,
|
|
1772
|
+
surname: s.surname ?? null,
|
|
1773
|
+
name: s.name,
|
|
1774
|
+
country: s.country ?? null,
|
|
1775
|
+
documentNumber: v.documentNumber,
|
|
1776
|
+
certificateLevel: v.certificateLevel,
|
|
1777
|
+
certificateDerB64: r.certificateDerB64 ?? "",
|
|
1778
|
+
flowType,
|
|
1779
|
+
person: personMatchesCertificate(r.person, s) ? r.person ?? null : null
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
function createEidLogin(opts) {
|
|
1783
|
+
const client = opts.client;
|
|
1784
|
+
if (!opts.onSuccess) throw new Error("createEidLogin: onSuccess \u0448\u0430\u0430\u0440\u0434\u043B\u0430\u0433\u0430\u0442\u0430\u0439");
|
|
1785
|
+
if (opts.callbackUrl) {
|
|
1786
|
+
const u = new URL(opts.callbackUrl);
|
|
1787
|
+
if (u.protocol !== "https:" || u.pathname !== CALLBACK_PATH) {
|
|
1788
|
+
throw new Error(`createEidLogin: callbackUrl \u043D\u044C https://<\u0442\u0430\u043D\u044B host>${CALLBACK_PATH} \u0431\u0430\u0439\u0445 \u0451\u0441\u0442\u043E\u0439`);
|
|
1755
1789
|
}
|
|
1756
|
-
return d;
|
|
1757
1790
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1791
|
+
const store = opts.store ?? memoryLoginStore();
|
|
1792
|
+
const ttl = opts.ttlMs ?? 12e4;
|
|
1793
|
+
const pollMs = opts.pollMs ?? 1e3;
|
|
1794
|
+
const cookieName = opts.cookie?.name ?? "eid_login";
|
|
1795
|
+
const cookiePath = opts.cookie?.path ?? "/";
|
|
1796
|
+
const secure = opts.cookie?.secure ?? true;
|
|
1797
|
+
const successRedirect = opts.successRedirect ?? "/";
|
|
1798
|
+
const failureRedirect = opts.failureRedirect ?? "/";
|
|
1799
|
+
const onError = opts.onError ?? ((e) => console.error("eid-login:", e instanceof Error ? e.message : e));
|
|
1800
|
+
const pushHits = /* @__PURE__ */ new Map();
|
|
1801
|
+
const rateLimit = opts.rateLimit ?? ((key) => {
|
|
1802
|
+
if (!key.startsWith("push:")) return true;
|
|
1803
|
+
const now = Date.now();
|
|
1804
|
+
const hits = (pushHits.get(key) ?? []).filter((t) => now - t < 6e4);
|
|
1805
|
+
if (pushHits.size > 1e4) pushHits.clear();
|
|
1806
|
+
if (hits.length >= 3) return false;
|
|
1807
|
+
hits.push(now);
|
|
1808
|
+
pushHits.set(key, hits);
|
|
1809
|
+
return true;
|
|
1810
|
+
});
|
|
1811
|
+
const displayText = (opts.displayText ?? `${client.rpName}-\u0434 \u043D\u044D\u0432\u0442\u0440\u044D\u0445`).slice(0, 60);
|
|
1812
|
+
const interactions = [{ type: "displayTextAndPIN", displayText60: displayText }];
|
|
1813
|
+
const json = (status2, body, extra = {}) => ({
|
|
1814
|
+
status: status2,
|
|
1815
|
+
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", ...extra },
|
|
1816
|
+
body: JSON.stringify(body)
|
|
1817
|
+
});
|
|
1818
|
+
const cookieAttrs = (maxAge) => `Path=${cookiePath}; Max-Age=${maxAge}; HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}` + (opts.cookie?.domain ? `; Domain=${opts.cookie.domain}` : "");
|
|
1819
|
+
const clearCookie = `${cookieName}=; ${cookieAttrs(0)}`;
|
|
1820
|
+
const redirect = (to, extra = {}) => ({
|
|
1821
|
+
status: 303,
|
|
1822
|
+
headers: { Location: to, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", ...extra },
|
|
1823
|
+
body: ""
|
|
1824
|
+
});
|
|
1825
|
+
const failTo = (code) => {
|
|
1826
|
+
const u = new URL(failureRedirect, "http://x");
|
|
1827
|
+
u.searchParams.set("eid_error", code);
|
|
1828
|
+
return /^https?:/i.test(failureRedirect) ? u.toString() : u.pathname + u.search + u.hash;
|
|
1829
|
+
};
|
|
1830
|
+
const mergeHeaders = (a, b) => {
|
|
1831
|
+
const out = { ...a };
|
|
1832
|
+
for (const [k, v] of Object.entries(b ?? {})) {
|
|
1833
|
+
const key = Object.keys(out).find((x) => x.toLowerCase() === k.toLowerCase()) ?? k;
|
|
1834
|
+
const prev = out[key];
|
|
1835
|
+
out[key] = key.toLowerCase() === "set-cookie" && prev ? [...[prev].flat(), ...[v].flat()] : v;
|
|
1836
|
+
}
|
|
1837
|
+
return out;
|
|
1838
|
+
};
|
|
1839
|
+
async function loadTx(req) {
|
|
1840
|
+
const raw = readCookie(req.headers, cookieName);
|
|
1841
|
+
const dot = raw.indexOf(".");
|
|
1842
|
+
const sid = raw.slice(0, dot);
|
|
1843
|
+
const binding = raw.slice(dot + 1);
|
|
1844
|
+
if (dot < 0 || !UUID_RE.test(sid) || !B64URL_RE.test(binding)) return void 0;
|
|
1845
|
+
const tx = await store.get(sid);
|
|
1846
|
+
if (!tx || !safeEqualHex(tx.bindingHash, sha256hex(binding))) return void 0;
|
|
1847
|
+
if (tx.expiresAt <= Date.now()) {
|
|
1848
|
+
await store.delete(sid);
|
|
1849
|
+
return void 0;
|
|
1850
|
+
}
|
|
1851
|
+
return tx;
|
|
1852
|
+
}
|
|
1853
|
+
function qrLink(tx) {
|
|
1854
|
+
if (tx.mode !== "qr" || !tx.link) return void 0;
|
|
1855
|
+
return buildDeviceLink({
|
|
1856
|
+
deviceLinkBase: tx.link.base,
|
|
1857
|
+
deviceLinkType: "QR",
|
|
1858
|
+
sessionToken: tx.link.token,
|
|
1859
|
+
sessionSecret: tx.link.secret,
|
|
1860
|
+
sessionType: "auth",
|
|
1861
|
+
lang: tx.link.lang,
|
|
1862
|
+
receivedAt: tx.link.receivedAt,
|
|
1863
|
+
rpChallenge: tx.acsp.rpChallenge,
|
|
1864
|
+
relyingPartyName: tx.acsp.relyingPartyName,
|
|
1865
|
+
interactions: tx.acsp.interactions
|
|
1777
1866
|
});
|
|
1778
|
-
if (opts.display) p.set("display", opts.display);
|
|
1779
|
-
u.search = p.toString();
|
|
1780
|
-
return { url: u.toString(), state, nonce, codeVerifier };
|
|
1781
1867
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1868
|
+
async function begin(req, mode, idCode, lang) {
|
|
1869
|
+
const level = opts.certificateLevel ? { certificateLevel: opts.certificateLevel } : {};
|
|
1870
|
+
let tx;
|
|
1871
|
+
let appLink;
|
|
1872
|
+
if (mode === "push") {
|
|
1873
|
+
const s = await client.auth.notificationByEtsi(idCode, interactions, level);
|
|
1874
|
+
tx = { sessionId: s.sessionId, mode, vc: s.vc, acsp: s.acsp };
|
|
1875
|
+
} else {
|
|
1876
|
+
const callbackUrl = mode === "web2app" ? withReturnHint(opts.callbackUrl, header(req.headers, "user-agent")) : void 0;
|
|
1877
|
+
const s = await client.auth.deviceLinkAnonymous(interactions, { ...level, callbackUrl });
|
|
1878
|
+
if (!s.deviceLinkBase || !s.sessionToken || !s.sessionSecret || !s.acsp) throw new Error("eID \u0445\u0430\u0440\u0438\u0443\u043D\u0434 device link \u0442\u0430\u043B\u0431\u0430\u0440 \u0434\u0443\u0442\u0443\u0443");
|
|
1879
|
+
tx = {
|
|
1880
|
+
sessionId: s.sessionId,
|
|
1881
|
+
mode,
|
|
1882
|
+
vc: s.vc,
|
|
1883
|
+
acsp: s.acsp,
|
|
1884
|
+
link: { base: s.deviceLinkBase, token: s.sessionToken, secret: s.sessionSecret, receivedAt: s.receivedAt, lang }
|
|
1885
|
+
};
|
|
1886
|
+
if (mode === "web2app") {
|
|
1887
|
+
appLink = buildDeviceLink({
|
|
1888
|
+
deviceLinkBase: s.deviceLinkBase,
|
|
1889
|
+
deviceLinkType: "Web2App",
|
|
1890
|
+
sessionToken: s.sessionToken,
|
|
1891
|
+
sessionSecret: s.sessionSecret,
|
|
1892
|
+
sessionType: "auth",
|
|
1893
|
+
lang,
|
|
1894
|
+
rpChallenge: s.acsp.rpChallenge,
|
|
1895
|
+
relyingPartyName: s.acsp.relyingPartyName,
|
|
1896
|
+
interactions: s.acsp.interactions,
|
|
1897
|
+
initialCallbackUrl: s.acsp.initialCallbackUrl
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1796
1900
|
}
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1901
|
+
const binding = randomBytes2(32).toString("base64url");
|
|
1902
|
+
const full = { ...tx, bindingHash: sha256hex(binding), expiresAt: Date.now() + ttl };
|
|
1903
|
+
await store.set(full.sessionId, full, ttl);
|
|
1904
|
+
const setCookie = `${cookieName}=${full.sessionId}.${binding}; ${cookieAttrs(Math.ceil(ttl / 1e3))}`;
|
|
1905
|
+
return { tx: full, setCookie, appLink };
|
|
1906
|
+
}
|
|
1907
|
+
async function start(req) {
|
|
1908
|
+
const url = new URL(req.url, "http://x");
|
|
1909
|
+
const site = header(req.headers, "sec-fetch-site");
|
|
1910
|
+
if (site && site !== "same-origin" && site !== "same-site" && site !== "none") return json(403, { error: "cross_site" });
|
|
1911
|
+
let body = {};
|
|
1912
|
+
if (req.method === "GET") {
|
|
1913
|
+
if (url.searchParams.get("mode") !== "web2app") return json(405, { error: "method_not_allowed" });
|
|
1914
|
+
body = { mode: "web2app", lang: url.searchParams.get("lang") ?? void 0 };
|
|
1915
|
+
} else if (req.method === "POST") {
|
|
1916
|
+
if (!header(req.headers, "content-type").toLowerCase().startsWith("application/json")) {
|
|
1917
|
+
return json(415, { error: "json_required" });
|
|
1918
|
+
}
|
|
1919
|
+
try {
|
|
1920
|
+
const b = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
|
|
1921
|
+
if (b && typeof b === "object") body = b;
|
|
1922
|
+
} catch {
|
|
1923
|
+
return json(400, { error: "invalid_body" });
|
|
1924
|
+
}
|
|
1925
|
+
} else {
|
|
1926
|
+
return json(405, { error: "method_not_allowed" });
|
|
1801
1927
|
}
|
|
1802
|
-
const
|
|
1803
|
-
if (
|
|
1804
|
-
const
|
|
1805
|
-
const
|
|
1806
|
-
return {
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
method: "POST",
|
|
1814
|
-
headers: {
|
|
1815
|
-
Authorization: "Basic " + basic,
|
|
1816
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
1817
|
-
Accept: "application/json"
|
|
1818
|
-
},
|
|
1819
|
-
body: new URLSearchParams({
|
|
1820
|
-
grant_type: "authorization_code",
|
|
1821
|
-
code,
|
|
1822
|
-
redirect_uri: this.cfg.redirectUri,
|
|
1823
|
-
code_verifier: codeVerifier
|
|
1824
|
-
}).toString()
|
|
1825
|
-
});
|
|
1826
|
-
let j = {};
|
|
1928
|
+
const mode = body.mode;
|
|
1929
|
+
if (mode !== "qr" && mode !== "push" && mode !== "web2app") return json(400, { error: "invalid_mode" });
|
|
1930
|
+
const lang = body.lang === "en" ? "eng" : "mon";
|
|
1931
|
+
const ctx = { request: req, mode };
|
|
1932
|
+
if (mode === "web2app" && !opts.callbackUrl) return json(400, { error: "web2app_not_configured" });
|
|
1933
|
+
const idCode = typeof body.idCode === "string" ? body.idCode.replace(/\s+/g, "").toUpperCase() : "";
|
|
1934
|
+
if (mode === "push" && !ID_RE.test(idCode)) return json(400, { error: "invalid_id" });
|
|
1935
|
+
const limited = () => json(429, { error: "rate_limited" }, { "Retry-After": "60" });
|
|
1936
|
+
if (req.ip && !await rateLimit(`start:${req.ip}`)) return limited();
|
|
1937
|
+
if (mode === "push" && !await rateLimit(`push:${idCode}`)) return limited();
|
|
1938
|
+
let r;
|
|
1827
1939
|
try {
|
|
1828
|
-
|
|
1829
|
-
} catch {
|
|
1940
|
+
r = await begin(req, mode, idCode, lang);
|
|
1941
|
+
} catch (e) {
|
|
1942
|
+
onError(e, ctx);
|
|
1943
|
+
const status2 = e instanceof ApiError && e.status === 429 ? 429 : 502;
|
|
1944
|
+
return json(status2, { error: status2 === 429 ? "rate_limited" : "eid_unavailable" });
|
|
1830
1945
|
}
|
|
1831
|
-
if (
|
|
1832
|
-
|
|
1833
|
-
throw new EidOidcError(
|
|
1834
|
-
e,
|
|
1835
|
-
`OIDC token ${res.status}: ${e}`,
|
|
1836
|
-
typeof j.error_description === "string" ? j.error_description : void 0
|
|
1837
|
-
);
|
|
1946
|
+
if (mode === "web2app") {
|
|
1947
|
+
return { status: 302, headers: { Location: r.appLink, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "Set-Cookie": r.setCookie }, body: "" };
|
|
1838
1948
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1949
|
+
return json(
|
|
1950
|
+
200,
|
|
1951
|
+
{ mode, vc: r.tx.vc, ...mode === "qr" ? { qr: qrLink(r.tx) } : {}, expiresIn: Math.ceil(ttl / 1e3) },
|
|
1952
|
+
{ "Set-Cookie": r.setCookie }
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
async function finish(req, tx, r, flow, verifier) {
|
|
1956
|
+
const v = client.validator.validateAuth(r, tx.acsp, { expectedFlowType: flow, userChallengeVerifier: verifier });
|
|
1957
|
+
if (!await store.delete(tx.sessionId)) return void 0;
|
|
1958
|
+
const out = await opts.onSuccess(toIdentity(r, v, flow), { request: req, mode: tx.mode }) ?? {};
|
|
1959
|
+
return out;
|
|
1960
|
+
}
|
|
1961
|
+
async function status(req) {
|
|
1962
|
+
if (req.method !== "GET") return json(405, { error: "method_not_allowed" });
|
|
1963
|
+
const tx = await loadTx(req);
|
|
1964
|
+
if (!tx) return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
|
|
1965
|
+
const ctx = { request: req, mode: tx.mode };
|
|
1966
|
+
let r;
|
|
1967
|
+
try {
|
|
1968
|
+
r = await client.session.poll(tx.sessionId, pollMs);
|
|
1969
|
+
} catch (e) {
|
|
1970
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
1971
|
+
await store.delete(tx.sessionId);
|
|
1972
|
+
return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
|
|
1973
|
+
}
|
|
1974
|
+
onError(e, ctx);
|
|
1975
|
+
return json(502, { error: "eid_unavailable" });
|
|
1841
1976
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
const alias = d.mtls_endpoint_aliases?.token_endpoint;
|
|
1848
|
-
if (alias === void 0) return d.token_endpoint;
|
|
1849
|
-
if (typeof alias !== "string" || !alias.startsWith("https://")) {
|
|
1850
|
-
throw new EidOidcError("discovery_failed", "OIDC discovery: mtls_endpoint_aliases.token_endpoint \u0431\u0443\u0440\u0443\u0443");
|
|
1977
|
+
const expiresIn = Math.max(0, Math.ceil((tx.expiresAt - Date.now()) / 1e3));
|
|
1978
|
+
if (r.state !== "COMPLETE") return json(200, { state: "PENDING", qr: qrLink(tx), vc: tx.vc, expiresIn });
|
|
1979
|
+
if (r.endResult !== "OK") {
|
|
1980
|
+
await store.delete(tx.sessionId);
|
|
1981
|
+
return json(200, { state: "FAILED", error: r.endResult ?? "UNKNOWN" }, { "Set-Cookie": clearCookie });
|
|
1851
1982
|
}
|
|
1852
|
-
return
|
|
1853
|
-
}
|
|
1854
|
-
/**
|
|
1855
|
-
* id_token шалгана: JWKS (`kid`, cache; танихгүй kid → нэг удаа дахин татна) ES256 гарын үсэг, `iss`, `aud`
|
|
1856
|
-
* (= clientId; олон aud бол `azp`), `exp`/`iat` (±60 сек), `nonce`. Амжилтгүй бол `invalid_id_token`.
|
|
1857
|
-
*/
|
|
1858
|
-
async verifyIdToken(idToken, opts) {
|
|
1859
|
-
const bad = (why) => new EidOidcError("invalid_id_token", `id_token: ${why}`);
|
|
1860
|
-
const parts = idToken.split(".");
|
|
1861
|
-
if (parts.length !== 3) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
|
|
1862
|
-
const header = decodeJsonPart(parts[0]);
|
|
1863
|
-
const payload = decodeJsonPart(parts[1]);
|
|
1864
|
-
if (!header || !payload) throw bad("\u0445\u044D\u043B\u0431\u044D\u0440 \u0431\u0443\u0440\u0443\u0443");
|
|
1865
|
-
if (header.alg !== OIDC_EXPECT.idTokenAlg) throw bad("alg ES256 \u0431\u0438\u0448");
|
|
1866
|
-
const kid = typeof header.kid === "string" ? header.kid : "";
|
|
1867
|
-
if (!kid) throw bad("kid \u0430\u043B\u0433\u0430");
|
|
1868
|
-
const pick = (ks) => ks.find(
|
|
1869
|
-
(k) => k.kid === kid && k.kty === "EC" && k.crv === "P-256" && (k.use === void 0 || k.use === "sig") && (k.alg === void 0 || k.alg === "ES256")
|
|
1870
|
-
);
|
|
1871
|
-
const jwk = pick(await this.keys(false)) ?? pick(await this.keys(true));
|
|
1872
|
-
if (!jwk || !jwk.x || !jwk.y) throw bad(`\u0442\u0430\u043D\u0438\u0445\u0433\u04AF\u0439 kid ${kid}`);
|
|
1873
|
-
const sig = Buffer.from(parts[2], "base64url");
|
|
1874
|
-
if (sig.length !== 64) throw bad("\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
1875
|
-
let ok = false;
|
|
1983
|
+
if (tx.mode === "web2app") return json(200, { state: "WAIT_CALLBACK" });
|
|
1876
1984
|
try {
|
|
1877
|
-
const
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
);
|
|
1884
|
-
|
|
1885
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
1886
|
-
key,
|
|
1887
|
-
sig,
|
|
1888
|
-
new TextEncoder().encode(parts[0] + "." + parts[1])
|
|
1889
|
-
);
|
|
1890
|
-
} catch {
|
|
1891
|
-
ok = false;
|
|
1985
|
+
const out = await finish(req, tx, r, tx.mode === "qr" ? "QR" : "Notification");
|
|
1986
|
+
if (!out) return json(200, { state: "EXPIRED" }, { "Set-Cookie": clearCookie });
|
|
1987
|
+
return json(200, { state: "OK", redirect: out.redirect, data: out.data }, mergeHeaders({ "Set-Cookie": clearCookie }, out.headers));
|
|
1988
|
+
} catch (e) {
|
|
1989
|
+
onError(e, ctx);
|
|
1990
|
+
if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return json(500, { error: "internal" });
|
|
1991
|
+
await store.delete(tx.sessionId);
|
|
1992
|
+
return json(200, { state: "FAILED", error: "VALIDATION" }, { "Set-Cookie": clearCookie });
|
|
1892
1993
|
}
|
|
1893
|
-
if (!ok) throw bad("\u0433\u0430\u0440\u044B\u043D \u04AF\u0441\u044D\u0433 \u0442\u0430\u0430\u0440\u0441\u0430\u043D\u0433\u04AF\u0439");
|
|
1894
|
-
const c = payload;
|
|
1895
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
1896
|
-
if (c.iss !== this.issuer) throw bad("iss \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1897
|
-
const aud = Array.isArray(c.aud) ? c.aud : [c.aud];
|
|
1898
|
-
if (!aud.includes(this.cfg.clientId)) throw bad("aud \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1899
|
-
if (aud.length > 1 && c.azp !== this.cfg.clientId) throw bad("azp \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1900
|
-
if (typeof c.exp !== "number" || now > c.exp + CLOCK_SKEW_S) throw bad("\u0445\u0443\u0433\u0430\u0446\u0430\u0430 \u0434\u0443\u0443\u0441\u0441\u0430\u043D");
|
|
1901
|
-
if (typeof c.iat !== "number" || c.iat > now + CLOCK_SKEW_S) throw bad("iat \u0438\u0440\u044D\u044D\u0434\u04AF\u0439\u0434");
|
|
1902
|
-
if (typeof c.nonce !== "string" || !opts.nonce || !timingSafeEqualStr(c.nonce, opts.nonce)) throw bad("nonce \u0442\u0430\u0430\u0440\u0430\u0445\u0433\u04AF\u0439");
|
|
1903
|
-
if (typeof c.sub !== "string" || c.sub === "") throw bad("sub \u0430\u043B\u0433\u0430");
|
|
1904
|
-
return c;
|
|
1905
|
-
}
|
|
1906
|
-
keys(force) {
|
|
1907
|
-
const c = this.jwks;
|
|
1908
|
-
if (!force && c && Date.now() - c.at < CACHE_MS) return c.keys;
|
|
1909
|
-
const keys = this.discover().then(async (d) => {
|
|
1910
|
-
const j = await this.getJson(d.jwks_uri);
|
|
1911
|
-
return Array.isArray(j?.keys) ? j.keys : [];
|
|
1912
|
-
});
|
|
1913
|
-
this.jwks = { at: Date.now(), keys };
|
|
1914
|
-
keys.catch(() => {
|
|
1915
|
-
if (this.jwks?.keys === keys) this.jwks = void 0;
|
|
1916
|
-
});
|
|
1917
|
-
return keys;
|
|
1918
1994
|
}
|
|
1919
|
-
async
|
|
1920
|
-
|
|
1921
|
-
|
|
1995
|
+
async function callback(req) {
|
|
1996
|
+
if (req.method !== "GET") return json(405, { error: "method_not_allowed" });
|
|
1997
|
+
const q = new URL(req.url, "http://x").searchParams;
|
|
1998
|
+
const sid = q.get("sessionId") ?? "";
|
|
1999
|
+
const verifier = q.get("userChallengeVerifier") ?? "";
|
|
2000
|
+
const fail2 = (code) => redirect(failTo(code), { "Set-Cookie": clearCookie });
|
|
2001
|
+
const tx = await loadTx(req);
|
|
2002
|
+
if (!tx || tx.sessionId !== sid || tx.mode !== "web2app") return fail2(tx ? "unbound" : "expired");
|
|
2003
|
+
if (!verifier) return fail2("unbound");
|
|
2004
|
+
const ctx = { request: req, mode: tx.mode };
|
|
2005
|
+
let r;
|
|
1922
2006
|
try {
|
|
1923
|
-
|
|
1924
|
-
} catch {
|
|
1925
|
-
|
|
2007
|
+
r = await client.session.poll(tx.sessionId, 1e4);
|
|
2008
|
+
} catch (e) {
|
|
2009
|
+
onError(e, ctx);
|
|
2010
|
+
return fail2("eid_unavailable");
|
|
2011
|
+
}
|
|
2012
|
+
if (r.state !== "COMPLETE") return fail2("not_complete");
|
|
2013
|
+
if (r.endResult !== "OK") {
|
|
2014
|
+
await store.delete(tx.sessionId);
|
|
2015
|
+
return fail2(r.endResult ?? "UNKNOWN");
|
|
2016
|
+
}
|
|
2017
|
+
try {
|
|
2018
|
+
const out = await finish(req, tx, r, "Web2App", verifier);
|
|
2019
|
+
if (!out) return fail2("expired");
|
|
2020
|
+
return redirect(out.redirect ?? successRedirect, mergeHeaders({ "Set-Cookie": clearCookie }, out.headers));
|
|
2021
|
+
} catch (e) {
|
|
2022
|
+
onError(e, ctx);
|
|
2023
|
+
if (!(e instanceof ValidationError || e instanceof SessionFailedError)) return fail2("internal");
|
|
2024
|
+
await store.delete(tx.sessionId);
|
|
2025
|
+
return fail2("VALIDATION");
|
|
1926
2026
|
}
|
|
1927
2027
|
}
|
|
1928
|
-
async
|
|
1929
|
-
const
|
|
1930
|
-
const req = {
|
|
1931
|
-
...init,
|
|
1932
|
-
redirect: "error",
|
|
1933
|
-
signal: AbortSignal.timeout(this.cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
1934
|
-
};
|
|
1935
|
-
if (this.cfg.dispatcher) req.dispatcher = this.cfg.dispatcher;
|
|
2028
|
+
async function handleRequest(req) {
|
|
2029
|
+
const seg = new URL(req.url, "http://x").pathname.replace(/\/+$/, "").split("/").pop();
|
|
1936
2030
|
try {
|
|
1937
|
-
return await
|
|
2031
|
+
if (seg === "start") return await start(req);
|
|
2032
|
+
if (seg === "status") return await status(req);
|
|
2033
|
+
if (seg === "callback") return await callback(req);
|
|
1938
2034
|
} catch (e) {
|
|
1939
|
-
|
|
1940
|
-
if (
|
|
1941
|
-
|
|
1942
|
-
}
|
|
1943
|
-
throw new NetworkError(`OIDC \u0441\u04AF\u043B\u0436\u044D\u044D\u043D\u0438\u0439 \u0430\u043B\u0434\u0430\u0430 (${url}): ${err.message}`);
|
|
2035
|
+
onError(e, { request: req, mode: "qr" });
|
|
2036
|
+
if (e instanceof AuthenticationError || e instanceof ForbiddenError) return json(502, { error: "eid_unavailable" });
|
|
2037
|
+
return json(500, { error: "internal" });
|
|
1944
2038
|
}
|
|
2039
|
+
return json(404, { error: "not_found" });
|
|
1945
2040
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
}
|
|
1954
|
-
function formEncode(s) {
|
|
1955
|
-
return new URLSearchParams({ v: s }).toString().slice(2);
|
|
1956
|
-
}
|
|
1957
|
-
function timingSafeEqualStr(a, b) {
|
|
1958
|
-
const x = Buffer.from(a, "utf8");
|
|
1959
|
-
const y = Buffer.from(b, "utf8");
|
|
1960
|
-
return x.length === y.length && timingSafeEqual2(x, y);
|
|
1961
|
-
}
|
|
1962
|
-
function decodeJsonPart(s) {
|
|
1963
|
-
try {
|
|
1964
|
-
const v = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
|
|
1965
|
-
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
1966
|
-
} catch {
|
|
1967
|
-
return null;
|
|
2041
|
+
async function handle(req) {
|
|
2042
|
+
const headers = {};
|
|
2043
|
+
req.headers.forEach((v, k) => headers[k] = v);
|
|
2044
|
+
const body = req.method === "POST" ? await req.text() : void 0;
|
|
2045
|
+
const r = await handleRequest({ method: req.method, url: req.url, headers, body });
|
|
2046
|
+
const h = new Headers();
|
|
2047
|
+
for (const [k, v] of Object.entries(r.headers)) for (const x of [v].flat()) h.append(k, x);
|
|
2048
|
+
return new Response(r.status === 302 || r.status === 303 || r.body === "" ? null : r.body, { status: r.status, headers: h });
|
|
1968
2049
|
}
|
|
1969
|
-
}
|
|
1970
|
-
function callbackParams(input) {
|
|
1971
|
-
if (input instanceof URLSearchParams) return input;
|
|
1972
|
-
if (input instanceof URL) return input.searchParams;
|
|
1973
|
-
const s = input.trim();
|
|
1974
|
-
if (/^https?:\/\//i.test(s)) return new URL(s).searchParams;
|
|
1975
|
-
const i = s.indexOf("?");
|
|
1976
|
-
return new URLSearchParams(i >= 0 ? s.slice(i + 1) : s);
|
|
2050
|
+
return { handleRequest, handle };
|
|
1977
2051
|
}
|
|
1978
2052
|
export {
|
|
1979
2053
|
ApiError,
|
|
@@ -1984,13 +2058,8 @@ export {
|
|
|
1984
2058
|
DEFAULT_PDF_MAX_BYTES,
|
|
1985
2059
|
DEFAULT_PDF_TIMEOUT_MS,
|
|
1986
2060
|
DeviceLinkError,
|
|
1987
|
-
EID_ACR_ADVANCED,
|
|
1988
|
-
EID_ACR_QUALIFIED,
|
|
1989
|
-
EID_OIDC_ISSUER,
|
|
1990
2061
|
EidClient,
|
|
1991
2062
|
EidError,
|
|
1992
|
-
EidOidcClient,
|
|
1993
|
-
EidOidcError,
|
|
1994
2063
|
ForbiddenError,
|
|
1995
2064
|
Http,
|
|
1996
2065
|
NetworkError,
|
|
@@ -2009,15 +2078,16 @@ export {
|
|
|
2009
2078
|
acspV2Digest,
|
|
2010
2079
|
buildAcspV2Payload,
|
|
2011
2080
|
buildDeviceLink,
|
|
2081
|
+
createEidLogin,
|
|
2012
2082
|
deviceLink,
|
|
2013
2083
|
interactionsToBase64,
|
|
2014
2084
|
isMobileUserAgent,
|
|
2085
|
+
memoryLoginStore,
|
|
2015
2086
|
openOrShowQR,
|
|
2016
2087
|
parseRetryAfter,
|
|
2017
2088
|
parseSessionResult,
|
|
2018
2089
|
parseSubject,
|
|
2019
2090
|
personMatchesCertificate,
|
|
2020
|
-
pkceChallenge,
|
|
2021
2091
|
qrDeviceLinkTicker,
|
|
2022
2092
|
randomChallenge,
|
|
2023
2093
|
resolveBaseUrl,
|
|
@@ -2027,5 +2097,6 @@ export {
|
|
|
2027
2097
|
userChallengeOf,
|
|
2028
2098
|
verifyPayloadSignature,
|
|
2029
2099
|
verifyPrehashedSignature,
|
|
2030
|
-
verifyTokenCertificates
|
|
2100
|
+
verifyTokenCertificates,
|
|
2101
|
+
withReturnHint
|
|
2031
2102
|
};
|