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