@voltro/plugin-sso-saml 0.85.0 → 0.86.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/CHANGELOG.md +666 -0
- package/THIRD-PARTY-NOTICES.md +5 -33
- package/dist/index.js +145 -378
- package/dist/saml-cache.js +73 -0
- package/dist/saml-errors.js +11 -0
- package/dist/saml-logout.js +79 -0
- package/dist/saml-metadata.js +66 -0
- package/dist/saml.js +34 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,463 +1,230 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</KeyInfo>
|
|
14
|
-
</KeyDescriptor>` : "", n = e.sloUrl ? `
|
|
15
|
-
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="${h(e.sloUrl)}"/>` : "";
|
|
16
|
-
return `<?xml version="1.0"?>
|
|
17
|
-
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${h(e.entityId)}">
|
|
18
|
-
<SPSSODescriptor AuthnRequestsSigned="${e.authnRequestsSigned ? "true" : "false"}" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">${t}${n}
|
|
19
|
-
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress</NameIDFormat>
|
|
20
|
-
<AssertionConsumerService index="0" isDefault="true" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${h(e.acsUrl)}"/>
|
|
21
|
-
</SPSSODescriptor>
|
|
22
|
-
</EntityDescriptor>`;
|
|
23
|
-
}, h = (e) => e.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """), g = (e) => {
|
|
24
|
-
let t = e.attributes ?? {}, n = (...n) => {
|
|
25
|
-
for (let r of n) {
|
|
26
|
-
let n = e[r] ?? t[r];
|
|
27
|
-
if (typeof n == "string" && n.length > 0) return n;
|
|
28
|
-
if (Array.isArray(n) && typeof n[0] == "string") return n[0];
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}, r = String(e.nameID ?? e.nameId ?? n("nameID") ?? "");
|
|
32
|
-
return {
|
|
33
|
-
nameId: r,
|
|
34
|
-
email: n("email", "mail", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress") ?? (r.includes("@") ? r : null),
|
|
35
|
-
displayName: n("displayName", "name", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "cn"),
|
|
36
|
-
attributes: t
|
|
37
|
-
};
|
|
38
|
-
}, _ = "_voltro_saml_replay", v = d(_, {
|
|
39
|
-
id: u({ prefix: "samlrq" }),
|
|
40
|
-
requestId: f().unique(),
|
|
41
|
-
instant: f(),
|
|
42
|
-
createdAtMs: f()
|
|
43
|
-
}).index("bySamlRequestId", ["requestId"]), y = [v], b = (e, t) => ({
|
|
44
|
-
kind: "binary",
|
|
45
|
-
op: "eq",
|
|
46
|
-
column: e,
|
|
47
|
-
value: t
|
|
48
|
-
}), x = (e, t = 6e5) => {
|
|
49
|
-
let n = async (t) => (await e.query({
|
|
50
|
-
table: _,
|
|
51
|
-
predicate: b("requestId", t),
|
|
52
|
-
order: [{
|
|
53
|
-
column: "id",
|
|
54
|
-
direction: "asc"
|
|
55
|
-
}],
|
|
56
|
-
take: 1,
|
|
57
|
-
skip: void 0,
|
|
58
|
-
projection: void 0
|
|
59
|
-
}))[0], r = (e, n) => {
|
|
60
|
-
let r = Number(e.createdAtMs);
|
|
61
|
-
return Number.isFinite(r) && n - r > t;
|
|
62
|
-
};
|
|
63
|
-
return {
|
|
64
|
-
saveAsync: async (t, n) => {
|
|
65
|
-
let r = Date.now();
|
|
66
|
-
try {
|
|
67
|
-
return await e.insert(_, {
|
|
68
|
-
requestId: t,
|
|
69
|
-
instant: n,
|
|
70
|
-
createdAtMs: String(r)
|
|
71
|
-
}), {
|
|
72
|
-
value: n,
|
|
73
|
-
createdAt: r
|
|
74
|
-
};
|
|
75
|
-
} catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
getAsync: async (t) => {
|
|
80
|
-
let i = await n(t);
|
|
81
|
-
return i ? r(i, Date.now()) ? (await e.delete(_, String(i.id)).catch(() => void 0), null) : String(i.instant) : null;
|
|
82
|
-
},
|
|
83
|
-
removeAsync: async (t) => {
|
|
84
|
-
if (t == null) return null;
|
|
85
|
-
let r = await n(t);
|
|
86
|
-
return r ? (await e.delete(_, String(r.id)).catch(() => void 0), t) : null;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
}, S = (e = 6e5) => {
|
|
90
|
-
let t = /* @__PURE__ */ new Map();
|
|
91
|
-
return {
|
|
92
|
-
saveAsync: async (e, n) => {
|
|
93
|
-
if (t.has(e)) return null;
|
|
94
|
-
let r = {
|
|
95
|
-
value: n,
|
|
96
|
-
createdAt: Date.now()
|
|
97
|
-
};
|
|
98
|
-
return t.set(e, r), r;
|
|
99
|
-
},
|
|
100
|
-
getAsync: async (n) => {
|
|
101
|
-
let r = t.get(n);
|
|
102
|
-
return r ? Date.now() - r.createdAt > e ? (t.delete(n), null) : r.value : null;
|
|
103
|
-
},
|
|
104
|
-
removeAsync: async (e) => e == null || !t.has(e) ? null : (t.delete(e), e)
|
|
105
|
-
};
|
|
106
|
-
}, C = "_voltro_saml_logout", w = d(C, {
|
|
107
|
-
id: u({ prefix: "samllo" }),
|
|
108
|
-
sloKey: f().unique(),
|
|
109
|
-
nameId: f(),
|
|
110
|
-
nameIdFormat: f(),
|
|
111
|
-
sessionIndex: f(),
|
|
112
|
-
createdAtMs: f()
|
|
113
|
-
}).index("bySamlSloKey", ["sloKey"]), T = [w], E = (e, t) => ({
|
|
114
|
-
kind: "binary",
|
|
115
|
-
op: "eq",
|
|
116
|
-
column: e,
|
|
117
|
-
value: t
|
|
118
|
-
}), D = (e) => ({
|
|
119
|
-
nameId: String(e.nameId ?? ""),
|
|
120
|
-
...e.nameIdFormat ? { nameIdFormat: String(e.nameIdFormat) } : {},
|
|
121
|
-
...e.sessionIndex ? { sessionIndex: String(e.sessionIndex) } : {}
|
|
122
|
-
}), O = (e, t = 6048e5) => {
|
|
123
|
-
let n = async (t) => (await e.query({
|
|
124
|
-
table: C,
|
|
125
|
-
predicate: E("sloKey", t),
|
|
126
|
-
order: [{
|
|
127
|
-
column: "id",
|
|
128
|
-
direction: "asc"
|
|
129
|
-
}],
|
|
130
|
-
take: 1,
|
|
131
|
-
skip: void 0,
|
|
132
|
-
projection: void 0
|
|
133
|
-
}))[0], r = (e, n) => {
|
|
134
|
-
let r = Number(e.createdAtMs);
|
|
135
|
-
return Number.isFinite(r) && n - r > t;
|
|
136
|
-
}, i = async (t) => {
|
|
137
|
-
await e.delete(C, String(t.id)).catch(() => void 0);
|
|
138
|
-
};
|
|
139
|
-
return {
|
|
140
|
-
save: async (t) => {
|
|
141
|
-
let r = await n(t.sloKey);
|
|
142
|
-
r && await i(r), await e.insert(C, {
|
|
143
|
-
sloKey: t.sloKey,
|
|
144
|
-
nameId: t.nameId,
|
|
145
|
-
nameIdFormat: t.nameIdFormat ?? "",
|
|
146
|
-
sessionIndex: t.sessionIndex ?? "",
|
|
147
|
-
createdAtMs: String(Date.now())
|
|
148
|
-
});
|
|
149
|
-
},
|
|
150
|
-
load: async (e) => {
|
|
151
|
-
let t = await n(e);
|
|
152
|
-
return t ? r(t, Date.now()) ? (await i(t), null) : D(t) : null;
|
|
153
|
-
},
|
|
154
|
-
remove: async (e) => {
|
|
155
|
-
let t = await n(e);
|
|
156
|
-
t && await i(t);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
}, k = (e = 6048e5) => {
|
|
160
|
-
let t = /* @__PURE__ */ new Map();
|
|
161
|
-
return {
|
|
162
|
-
save: async (e) => {
|
|
163
|
-
t.set(e.sloKey, {
|
|
164
|
-
subject: {
|
|
165
|
-
nameId: e.nameId,
|
|
166
|
-
...e.nameIdFormat ? { nameIdFormat: e.nameIdFormat } : {},
|
|
167
|
-
...e.sessionIndex ? { sessionIndex: e.sessionIndex } : {}
|
|
168
|
-
},
|
|
169
|
-
createdAt: Date.now()
|
|
170
|
-
});
|
|
171
|
-
},
|
|
172
|
-
load: async (n) => {
|
|
173
|
-
let r = t.get(n);
|
|
174
|
-
return r ? Date.now() - r.createdAt > e ? (t.delete(n), null) : r.subject : null;
|
|
175
|
-
},
|
|
176
|
-
remove: async (e) => {
|
|
177
|
-
t.delete(e);
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
}, A = class extends t.TaggedError()("SamlMetadataFetchError", {
|
|
181
|
-
url: t.String,
|
|
182
|
-
reason: t.String
|
|
183
|
-
}) {}, j = class extends t.TaggedError()("SamlMetadataParseError", {
|
|
184
|
-
url: t.String,
|
|
185
|
-
reason: t.String
|
|
186
|
-
}) {}, M = "urn:oasis:names:tc:SAML:2.0:metadata", N = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", P = (e) => e.slice(e.indexOf(":") + 1), F = (e, t) => (RegExp(`\\b${t}\\s*=\\s*"([^"]*)"`).exec(e) ?? RegExp(`\\b${t}\\s*=\\s*'([^']*)'`).exec(e))?.[1], I = (e, t) => {
|
|
187
|
-
let n = [], r = /<([A-Za-z][\w.:-]*)\b([^>]*)>/g, i;
|
|
188
|
-
for (; (i = r.exec(e)) !== null;) P(i[1]) === t && n.push(i[0]);
|
|
189
|
-
return n;
|
|
190
|
-
}, L = (e) => {
|
|
191
|
-
let t = [], n = /<([A-Za-z][\w.:-]*KeyDescriptor)\b([^>]*)>/g, r;
|
|
192
|
-
for (; (r = n.exec(e)) !== null;) {
|
|
193
|
-
if (P(r[1]) !== "KeyDescriptor") continue;
|
|
194
|
-
let n = RegExp(`</${r[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}>`), i = e.slice(r.index + r[0].length), a = n.exec(i), o = a ? i.slice(0, a.index) : i;
|
|
195
|
-
t.push({
|
|
196
|
-
use: F(r[0], "use"),
|
|
197
|
-
body: o
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
return t;
|
|
201
|
-
}, R = (e) => {
|
|
202
|
-
let t = L(e), n = t.find((e) => e.use === "signing") ?? t.find((e) => e.use === void 0) ?? t[0];
|
|
203
|
-
if (!n) return;
|
|
204
|
-
let r = /<[^>]*X509Certificate[^>]*>([\s\S]*?)<\/[^>]*X509Certificate>/.exec(n.body)?.[1]?.replace(/\s+/g, "");
|
|
205
|
-
return r && r.length > 0 ? r : void 0;
|
|
206
|
-
}, z = (t, n) => e.gen(function* () {
|
|
207
|
-
if (!t.includes(M) && !/EntityDescriptor/.test(t)) return yield* e.fail(new j({
|
|
208
|
-
url: n,
|
|
209
|
-
reason: "not-saml-metadata"
|
|
210
|
-
}));
|
|
211
|
-
let r = I(t, "SingleSignOnService"), i = r.find((e) => F(e, "Binding") === N) ?? r[0], a = i ? F(i, "Location") : void 0;
|
|
212
|
-
if (!a) return yield* e.fail(new j({
|
|
213
|
-
url: n,
|
|
214
|
-
reason: "no-entryPoint"
|
|
215
|
-
}));
|
|
216
|
-
let o = R(t);
|
|
217
|
-
if (!o) return yield* e.fail(new j({
|
|
218
|
-
url: n,
|
|
219
|
-
reason: "no-signing-cert"
|
|
220
|
-
}));
|
|
221
|
-
let s = I(t, "SingleLogoutService"), c = s.find((e) => F(e, "Binding") === N) ?? s[0], l = c ? F(c, "Location") : void 0, u = I(t, "EntityDescriptor")[0], d = u ? F(u, "entityID") : void 0;
|
|
222
|
-
return {
|
|
223
|
-
entryPoint: a,
|
|
224
|
-
idpCert: o,
|
|
225
|
-
...l ? { logoutUrl: l } : {},
|
|
226
|
-
...d ? { issuer: d } : {}
|
|
227
|
-
};
|
|
228
|
-
}), B = (t) => e.gen(function* () {
|
|
229
|
-
let n = yield* (yield* r.HttpClient).get(t).pipe(e.mapError(() => new A({
|
|
230
|
-
url: t,
|
|
231
|
-
reason: "network"
|
|
232
|
-
})));
|
|
233
|
-
if (n.status < 200 || n.status >= 300) return yield* e.fail(new A({
|
|
234
|
-
url: t,
|
|
235
|
-
reason: `status ${n.status}`
|
|
236
|
-
}));
|
|
237
|
-
let i = yield* n.text.pipe(e.mapError(() => new A({
|
|
238
|
-
url: t,
|
|
239
|
-
reason: "body"
|
|
240
|
-
})));
|
|
241
|
-
return i.trim() ? yield* z(i, t) : yield* e.fail(new A({
|
|
242
|
-
url: t,
|
|
243
|
-
reason: "empty-body"
|
|
244
|
-
}));
|
|
245
|
-
}), V = (e, t) => typeof e != "string" || e[0] !== "/" || /[\u0000-]/.test(e) ? t : e === "/" ? e : e[1] === "/" || e[1] === "\\" ? t : e, H = (e, t, n = "text/plain") => ({
|
|
1
|
+
import { extractProfile as e, spMetadataXml as t } from "./saml.js";
|
|
2
|
+
import { REPLAY_CACHE_TABLE as n, dataStoreReplayCache as r, memoryReplayCache as i, samlReplayTable as a, samlReplayTables as o } from "./saml-cache.js";
|
|
3
|
+
import { LOGOUT_SESSION_TABLE as s, dataStoreLogoutStore as c, memoryLogoutStore as l, samlLogoutTable as u, samlLogoutTables as d } from "./saml-logout.js";
|
|
4
|
+
import { SamlMetadataFetchError as f, SamlMetadataParseError as p } from "./saml-errors.js";
|
|
5
|
+
import { fetchIdpMetadata as m, parseIdpMetadata as h } from "./saml-metadata.js";
|
|
6
|
+
import { Effect as g } from "effect";
|
|
7
|
+
import { FetchHttpClient as _ } from "effect/http";
|
|
8
|
+
import { randomBytes as v } from "node:crypto";
|
|
9
|
+
import { definePlugin as y, pluginInstanceName as b } from "@voltro/protocol";
|
|
10
|
+
import { buildSetCookie as x, readCookie as S, signSession as C } from "@voltro/protocol/session";
|
|
11
|
+
//#region src/index.ts
|
|
12
|
+
var w = (e, t) => typeof e != "string" || e[0] !== "/" || /[\u0000-]/.test(e) ? t : e === "/" ? e : e[1] === "/" || e[1] === "\\" ? t : e, T = (e, t, n = "text/plain") => ({
|
|
246
13
|
status: e,
|
|
247
14
|
body: t,
|
|
248
15
|
contentType: n
|
|
249
|
-
}),
|
|
16
|
+
}), E = (e, t) => {
|
|
250
17
|
let n = { location: e };
|
|
251
18
|
return t !== void 0 && t.length > 0 && (n["set-cookie"] = t), {
|
|
252
19
|
status: 302,
|
|
253
20
|
headers: n
|
|
254
21
|
};
|
|
255
|
-
},
|
|
256
|
-
if (!
|
|
257
|
-
if (!
|
|
258
|
-
let
|
|
259
|
-
base:
|
|
260
|
-
alias:
|
|
261
|
-
instance:
|
|
262
|
-
}),
|
|
263
|
-
if (
|
|
264
|
-
if (
|
|
265
|
-
if (!
|
|
266
|
-
return
|
|
22
|
+
}, D = ":slo", O = "@voltro/plugin-sso-saml", k = (n) => {
|
|
23
|
+
if (!n.sessionSecret?.trim()) throw Error("@voltro/plugin-sso-saml: `sessionSecret` must be a non-empty string");
|
|
24
|
+
if (!n.idp.metadataUrl && (!n.idp.entryPoint || !n.idp.idpCert)) throw Error("@voltro/plugin-sso-saml: provide idp.entryPoint + idp.idpCert, or idp.metadataUrl to load them from the IdP metadata");
|
|
25
|
+
let a = n.basePath ?? "/saml", s = n.cookieName ?? "voltro:session", u = `${s}${D}`, f = n.sessionTtlSeconds ?? 604800, p = n.successRedirect ?? "/", h = n.logoutRedirect ?? "/", k = b({
|
|
26
|
+
base: O,
|
|
27
|
+
alias: n.alias,
|
|
28
|
+
instance: n.name
|
|
29
|
+
}), A = n.idp.metadataRefreshMs ?? 432e5, j = n.replayProtection ?? { store: !0 }, M = typeof j == "object" && j.store === !0, N = typeof j == "object" ? j.ttlMs ?? 6e5 : 6e5, P = typeof n.sloStore == "object" && n.sloStore.store === !0, F = M || P, I, L, R, z, B, V = 0, H, U = () => {
|
|
30
|
+
if (j) {
|
|
31
|
+
if (M) {
|
|
32
|
+
if (!I) throw Error("@voltro/plugin-sso-saml: replayProtection { store: true } but the DataStore is not bound yet");
|
|
33
|
+
return r(I, N);
|
|
267
34
|
}
|
|
268
|
-
return
|
|
35
|
+
return i(N);
|
|
269
36
|
}
|
|
270
|
-
},
|
|
271
|
-
if (
|
|
272
|
-
if (
|
|
273
|
-
if (!
|
|
274
|
-
|
|
275
|
-
} else
|
|
276
|
-
return
|
|
277
|
-
},
|
|
278
|
-
let
|
|
279
|
-
if (
|
|
280
|
-
let
|
|
281
|
-
if (!
|
|
282
|
-
let t = await
|
|
283
|
-
t._tag === "Success" && (
|
|
37
|
+
}, W = () => {
|
|
38
|
+
if (H) return H;
|
|
39
|
+
if (P) {
|
|
40
|
+
if (!I) throw Error("@voltro/plugin-sso-saml: sloStore { store: true } but the DataStore is not bound yet");
|
|
41
|
+
H = c(I, f * 1e3);
|
|
42
|
+
} else H = l(f * 1e3);
|
|
43
|
+
return H;
|
|
44
|
+
}, G = async () => {
|
|
45
|
+
let e = n.idp.metadataUrl;
|
|
46
|
+
if (e) {
|
|
47
|
+
let t = A > 0 && Date.now() - V > A;
|
|
48
|
+
if (!B || t) {
|
|
49
|
+
let t = await g.runPromiseExit(m(e).pipe(g.provide(_.layer)));
|
|
50
|
+
t._tag === "Success" && (B = t.value, V = Date.now());
|
|
284
51
|
}
|
|
285
|
-
if (
|
|
286
|
-
entryPoint:
|
|
287
|
-
idpCert:
|
|
288
|
-
...
|
|
289
|
-
...
|
|
52
|
+
if (B) return {
|
|
53
|
+
entryPoint: B.entryPoint,
|
|
54
|
+
idpCert: B.idpCert,
|
|
55
|
+
...n.idp.logoutUrl ?? B.logoutUrl ? { logoutUrl: n.idp.logoutUrl ?? B.logoutUrl } : {},
|
|
56
|
+
...n.idp.issuer ?? B.issuer ? { idpIssuer: n.idp.issuer ?? B.issuer } : {}
|
|
290
57
|
};
|
|
291
58
|
}
|
|
292
59
|
return {
|
|
293
|
-
entryPoint:
|
|
294
|
-
idpCert:
|
|
295
|
-
...
|
|
296
|
-
...
|
|
60
|
+
entryPoint: n.idp.entryPoint,
|
|
61
|
+
idpCert: n.idp.idpCert,
|
|
62
|
+
...n.idp.logoutUrl ? { logoutUrl: n.idp.logoutUrl } : {},
|
|
63
|
+
...n.idp.issuer ? { idpIssuer: n.idp.issuer } : {}
|
|
297
64
|
};
|
|
298
|
-
}, K = (e,
|
|
299
|
-
callbackUrl:
|
|
65
|
+
}, K = (e, t) => ({
|
|
66
|
+
callbackUrl: n.sp.acsUrl,
|
|
300
67
|
entryPoint: e.entryPoint,
|
|
301
|
-
issuer:
|
|
68
|
+
issuer: n.sp.entityId,
|
|
302
69
|
idpCert: e.idpCert,
|
|
303
70
|
...e.idpIssuer ? { idpIssuer: e.idpIssuer } : {},
|
|
304
71
|
...e.logoutUrl ? { logoutUrl: e.logoutUrl } : {},
|
|
305
72
|
wantAssertionsSigned: !0,
|
|
306
|
-
wantAuthnResponseSigned:
|
|
307
|
-
...
|
|
308
|
-
...
|
|
309
|
-
...
|
|
310
|
-
...
|
|
311
|
-
...
|
|
73
|
+
wantAuthnResponseSigned: n.wantAuthnResponseSigned ?? !1,
|
|
74
|
+
...n.decryptionPvk ? { decryptionPvk: n.decryptionPvk } : {},
|
|
75
|
+
...n.privateKey ? { privateKey: n.privateKey } : {},
|
|
76
|
+
...n.signingCert ? { publicCert: n.signingCert } : {},
|
|
77
|
+
...n.acceptedClockSkewMs === void 0 ? {} : { acceptedClockSkewMs: n.acceptedClockSkewMs },
|
|
78
|
+
...t ? {
|
|
312
79
|
validateInResponseTo: "always",
|
|
313
|
-
cacheProvider:
|
|
314
|
-
requestIdExpirationPeriodMs:
|
|
80
|
+
cacheProvider: t,
|
|
81
|
+
requestIdExpirationPeriodMs: N
|
|
315
82
|
} : {}
|
|
316
83
|
}), q = async () => {
|
|
317
|
-
let e = !!
|
|
318
|
-
if (
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
let
|
|
84
|
+
let e = !!n.idp.metadataUrl && A > 0 && Date.now() - V > A;
|
|
85
|
+
if (L && !e) return L;
|
|
86
|
+
if (R) return;
|
|
87
|
+
z = void 0;
|
|
88
|
+
let t;
|
|
322
89
|
try {
|
|
323
|
-
|
|
90
|
+
t = await import("@node-saml/node-saml");
|
|
324
91
|
} catch {
|
|
325
|
-
|
|
92
|
+
R = "SAML SSO requires the optional dependency @node-saml/node-saml — run `pnpm add @node-saml/node-saml`";
|
|
326
93
|
return;
|
|
327
94
|
}
|
|
328
95
|
try {
|
|
329
|
-
let e = await
|
|
330
|
-
return
|
|
96
|
+
let e = await G(), n = U();
|
|
97
|
+
return L = new t.SAML(K(e, n)), L;
|
|
331
98
|
} catch (e) {
|
|
332
|
-
|
|
99
|
+
z = `@voltro/plugin-sso-saml: could not construct the SAML instance — ${String(e?.message ?? e)}`;
|
|
333
100
|
return;
|
|
334
101
|
}
|
|
335
102
|
}, J = (() => {
|
|
336
103
|
try {
|
|
337
|
-
return new URL(`${
|
|
104
|
+
return new URL(`${a}/slo`, n.sp.acsUrl).toString();
|
|
338
105
|
} catch {
|
|
339
106
|
return;
|
|
340
107
|
}
|
|
341
108
|
})(), Y = {
|
|
342
|
-
...
|
|
343
|
-
authnRequestsSigned: !!
|
|
344
|
-
...
|
|
109
|
+
...n.sp,
|
|
110
|
+
authnRequestsSigned: !!n.privateKey,
|
|
111
|
+
...n.signingCert ? { signingCert: n.signingCert } : {},
|
|
345
112
|
...J ? { sloUrl: J } : {}
|
|
346
|
-
}, X = (e) =>
|
|
113
|
+
}, X = (e) => S(e.headers.cookie ?? e.headers.Cookie, u), Z = () => [x(s, "", {
|
|
347
114
|
maxAgeSeconds: 0,
|
|
348
115
|
httpOnly: !0,
|
|
349
116
|
sameSite: "lax"
|
|
350
|
-
}),
|
|
117
|
+
}), x(u, "", {
|
|
351
118
|
maxAgeSeconds: 0,
|
|
352
119
|
httpOnly: !0,
|
|
353
120
|
sameSite: "lax"
|
|
354
121
|
})];
|
|
355
|
-
return
|
|
356
|
-
name:
|
|
357
|
-
baseName:
|
|
122
|
+
return y({
|
|
123
|
+
name: k,
|
|
124
|
+
baseName: O,
|
|
358
125
|
description: "SAML 2.0 SSO — SP-initiated login + Single Logout, ACS, metadata.",
|
|
359
|
-
permissions:
|
|
126
|
+
permissions: F ? ["store:write"] : [],
|
|
360
127
|
httpRoutes: [{
|
|
361
128
|
method: "*",
|
|
362
|
-
path:
|
|
129
|
+
path: a,
|
|
363
130
|
originGuard: "exempt",
|
|
364
|
-
handle: async (
|
|
365
|
-
let
|
|
366
|
-
if (
|
|
367
|
-
let
|
|
368
|
-
if (!
|
|
369
|
-
if (
|
|
370
|
-
let
|
|
371
|
-
return
|
|
131
|
+
handle: async (r) => {
|
|
132
|
+
let i = r.path.slice(a.length).replace(/\/+$/, ""), o = r.method.toUpperCase();
|
|
133
|
+
if (i === "/metadata" && o === "GET") return T(200, t(Y), "application/xml");
|
|
134
|
+
let c = await q();
|
|
135
|
+
if (!c) return T(500, R ?? z ?? "SAML not available");
|
|
136
|
+
if (i === "/login" && o === "GET") {
|
|
137
|
+
let e = w(new URLSearchParams(r.query).get("returnTo"), p);
|
|
138
|
+
return E(await c.getAuthorizeUrlAsync(e, void 0, {}));
|
|
372
139
|
}
|
|
373
|
-
if (
|
|
374
|
-
let
|
|
375
|
-
if (!
|
|
376
|
-
let
|
|
140
|
+
if (i === "/acs" && o === "POST") {
|
|
141
|
+
let t = new URLSearchParams(new TextDecoder().decode(r.rawBody)), i = t.get("SAMLResponse"), a = w(t.get("RelayState"), p);
|
|
142
|
+
if (!i) return T(400, "missing SAMLResponse");
|
|
143
|
+
let o;
|
|
377
144
|
try {
|
|
378
|
-
({profile:
|
|
145
|
+
({profile: o} = await c.validatePostResponseAsync({ SAMLResponse: i }));
|
|
379
146
|
} catch (e) {
|
|
380
|
-
return
|
|
147
|
+
return T(401, `SAML assertion rejected: ${String(e?.message ?? e)}`);
|
|
381
148
|
}
|
|
382
|
-
if (!
|
|
383
|
-
let
|
|
384
|
-
if (!
|
|
385
|
-
let
|
|
149
|
+
if (!o) return T(401, "no SAML profile");
|
|
150
|
+
let l = await n.onLogin(e(o));
|
|
151
|
+
if (!l) return T(403, "login rejected");
|
|
152
|
+
let d = C(l, n.sessionSecret, { ttlSeconds: f }), m = x(s, d, {
|
|
386
153
|
maxAgeSeconds: f,
|
|
387
154
|
httpOnly: !0,
|
|
388
155
|
sameSite: "lax"
|
|
389
|
-
}),
|
|
390
|
-
if (
|
|
391
|
-
let e =
|
|
392
|
-
await
|
|
156
|
+
}), h = String(o.nameID ?? o.nameId ?? ""), g = [m];
|
|
157
|
+
if (h) {
|
|
158
|
+
let e = v(24).toString("base64url");
|
|
159
|
+
await W().save({
|
|
393
160
|
sloKey: e,
|
|
394
|
-
nameId:
|
|
395
|
-
...
|
|
396
|
-
...
|
|
397
|
-
}),
|
|
161
|
+
nameId: h,
|
|
162
|
+
...o.nameIDFormat ? { nameIdFormat: String(o.nameIDFormat) } : {},
|
|
163
|
+
...o.sessionIndex ? { sessionIndex: String(o.sessionIndex) } : {}
|
|
164
|
+
}), g.push(x(u, e, {
|
|
398
165
|
maxAgeSeconds: f,
|
|
399
166
|
httpOnly: !0,
|
|
400
167
|
sameSite: "lax"
|
|
401
168
|
}));
|
|
402
169
|
}
|
|
403
|
-
return
|
|
170
|
+
return E(a, g);
|
|
404
171
|
}
|
|
405
|
-
if (
|
|
406
|
-
let
|
|
407
|
-
if (!
|
|
172
|
+
if (i === "/logout" && o === "GET") {
|
|
173
|
+
let e = w(new URLSearchParams(r.query).get("returnTo"), h), t = X(r), n = t ? await W().load(t) : null;
|
|
174
|
+
if (!n) return E(e, Z());
|
|
408
175
|
let i = {
|
|
409
|
-
nameID:
|
|
410
|
-
...
|
|
411
|
-
...
|
|
176
|
+
nameID: n.nameId,
|
|
177
|
+
...n.nameIdFormat ? { nameIDFormat: n.nameIdFormat } : {},
|
|
178
|
+
...n.sessionIndex ? { sessionIndex: n.sessionIndex } : {}
|
|
412
179
|
}, a;
|
|
413
180
|
try {
|
|
414
|
-
a = await
|
|
181
|
+
a = await c.getLogoutUrlAsync(i, e, {});
|
|
415
182
|
} catch (e) {
|
|
416
|
-
return
|
|
183
|
+
return T(500, `SAML logout request failed: ${String(e?.message ?? e)}`);
|
|
417
184
|
}
|
|
418
|
-
return
|
|
185
|
+
return t && await W().remove(t), E(a, Z());
|
|
419
186
|
}
|
|
420
|
-
if (
|
|
421
|
-
let
|
|
422
|
-
for (let [e,
|
|
423
|
-
let i =
|
|
424
|
-
if (!
|
|
425
|
-
let
|
|
187
|
+
if (i === "/slo" && (o === "GET" || o === "POST")) {
|
|
188
|
+
let e = o === "GET" ? r.query : new TextDecoder().decode(r.rawBody), t = new URLSearchParams(e), n = {};
|
|
189
|
+
for (let [e, r] of t) n[e] = r;
|
|
190
|
+
let i = w(t.get("RelayState"), h);
|
|
191
|
+
if (!t.has("Signature")) return T(401, "SAML logout rejected: the logout message is not signed");
|
|
192
|
+
let a;
|
|
426
193
|
try {
|
|
427
|
-
|
|
194
|
+
a = await c.validateRedirectAsync(n, e);
|
|
428
195
|
} catch (e) {
|
|
429
|
-
return
|
|
196
|
+
return T(401, `SAML logout rejected: ${String(e?.message ?? e)}`);
|
|
430
197
|
}
|
|
431
|
-
if (
|
|
432
|
-
let
|
|
433
|
-
|
|
198
|
+
if (t.has("SAMLResponse")) return E(i, Z());
|
|
199
|
+
let s = X(r);
|
|
200
|
+
s && await W().remove(s);
|
|
434
201
|
try {
|
|
435
|
-
return
|
|
202
|
+
return E(await c.getLogoutResponseUrlAsync(a.profile ?? {}, i, {}, !0), Z());
|
|
436
203
|
} catch {
|
|
437
|
-
return
|
|
204
|
+
return E(i, Z());
|
|
438
205
|
}
|
|
439
206
|
}
|
|
440
|
-
return
|
|
207
|
+
return T(404, "unknown SAML endpoint");
|
|
441
208
|
}
|
|
442
209
|
}],
|
|
443
|
-
...
|
|
210
|
+
...F ? { extendSchema: { tables: [...M ? o : [], ...P ? d : []] } } : {},
|
|
444
211
|
bindDataStore: (e) => {
|
|
445
|
-
|
|
212
|
+
I = e;
|
|
446
213
|
},
|
|
447
|
-
onActivate: (
|
|
448
|
-
|
|
449
|
-
basePath:
|
|
450
|
-
entityId:
|
|
451
|
-
idpSource:
|
|
452
|
-
replayProtection:
|
|
453
|
-
sloStore:
|
|
454
|
-
requestSigning:
|
|
455
|
-
encryptedAssertions:
|
|
214
|
+
onActivate: (e) => g.sync(() => {
|
|
215
|
+
e.logger.info("saml sso active", {
|
|
216
|
+
basePath: a,
|
|
217
|
+
entityId: n.sp.entityId,
|
|
218
|
+
idpSource: n.idp.metadataUrl ? "metadata-url" : "static",
|
|
219
|
+
replayProtection: j ? M ? "store" : "memory" : "off",
|
|
220
|
+
sloStore: P ? "store" : "memory",
|
|
221
|
+
requestSigning: n.privateKey ? "on" : "off",
|
|
222
|
+
encryptedAssertions: n.decryptionPvk ? "on" : "off",
|
|
456
223
|
wantAssertionsSigned: "required",
|
|
457
|
-
wantAuthnResponseSigned:
|
|
458
|
-
}),
|
|
224
|
+
wantAuthnResponseSigned: n.wantAuthnResponseSigned ?? !1 ? "required" : "optional"
|
|
225
|
+
}), j && !M && e.logger.warn("saml sso: replayProtection uses an IN-PROCESS cache — correct for ONE replica only. Under >1 replica a login and its ACS can land on different processes (InResponseTo fails) and a cross-replica replay is not caught. Use replayProtection: { store: true } in production."), j || e.logger.warn("saml sso: replayProtection is OFF — a captured SAMLResponse can be replayed for as long as its assertion is valid. This also ENABLES IdP-initiated SSO (the app-tile flow), which is the only reason to turn it off. If you did not mean to accept unsolicited responses, remove `replayProtection: false`."), P || e.logger.warn("saml sso: SLO state uses an IN-PROCESS store — correct for ONE replica only. Under >1 replica a login and its logout can land on different processes. Use sloStore: { store: true } in production.");
|
|
459
226
|
})
|
|
460
227
|
});
|
|
461
228
|
};
|
|
462
229
|
//#endregion
|
|
463
|
-
export {
|
|
230
|
+
export { s as LOGOUT_SESSION_TABLE, n as REPLAY_CACHE_TABLE, f as SamlMetadataFetchError, p as SamlMetadataParseError, c as dataStoreLogoutStore, r as dataStoreReplayCache, e as extractProfile, m as fetchIdpMetadata, l as memoryLogoutStore, i as memoryReplayCache, h as parseIdpMetadata, u as samlLogoutTable, d as samlLogoutTables, a as samlReplayTable, o as samlReplayTables, k as samlSsoPlugin, t as spMetadataXml };
|