@saltcorn/server 0.6.2 → 0.6.3-beta.3

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/app.js CHANGED
@@ -38,7 +38,7 @@ const { h1 } = require("@saltcorn/markup/tags");
38
38
  const is = require("contractis/is");
39
39
  const Trigger = require("@saltcorn/data/models/trigger");
40
40
  const s3storage = require("./s3storage");
41
-
41
+ const TotpStrategy = require("passport-totp").Strategy;
42
42
  const locales = Object.keys(available_languages);
43
43
  // i18n configuration
44
44
  const i18n = new I18n({
@@ -148,7 +148,9 @@ const getApp = async (opts = {}) => {
148
148
  req.flash("danger", req.__("Incorrect user or password"))
149
149
  );
150
150
  const mu = await User.authenticate(userobj);
151
- if (mu) return done(null, mu.session_object);
151
+ if (mu && mu._attributes.totp_enabled)
152
+ return done(null, { pending_user: mu.session_object });
153
+ else if (mu) return done(null, mu.session_object);
152
154
  else {
153
155
  const { password, ...nopw } = userobj;
154
156
  Trigger.emitEvent("LoginFailed", null, null, nopw);
@@ -188,6 +190,14 @@ const getApp = async (opts = {}) => {
188
190
  }
189
191
  })
190
192
  );
193
+ passport.use(
194
+ new TotpStrategy(function (user, done) {
195
+ // setup function, supply key and period to done callback
196
+ User.findOne({ id: user.pending_user.id }).then((u) => {
197
+ return done(null, u._attributes.totp_key, 30);
198
+ });
199
+ })
200
+ );
191
201
  passport.serializeUser(function (user, done) {
192
202
  done(null, user);
193
203
  });
package/auth/roleadmin.js CHANGED
@@ -85,6 +85,28 @@ const editRoleLayoutForm = (role, layouts, layout_by_role, req) =>
85
85
  )
86
86
  );
87
87
 
88
+ /**
89
+ * @param {Role} role
90
+ * @param {Layout[]} layouts
91
+ * @param {*} layout_by_role
92
+ * @param {object} req
93
+ * @returns {Form}
94
+ */
95
+ const editRole2FAPolicyForm = (role, twofa_policy_by_role, req) =>
96
+ form(
97
+ {
98
+ action: `/roleadmin/setrole2fapolicy/${role.id}`,
99
+ method: "post",
100
+ },
101
+ csrfField(req),
102
+ select(
103
+ { name: "policy", onchange: "form.submit()" },
104
+ ["Optional", "Disabled", "Mandatory"].map((p) =>
105
+ option({ selected: twofa_policy_by_role[role.id] === p }, p)
106
+ )
107
+ )
108
+ );
109
+
88
110
  /**
89
111
  * @param {object} req
90
112
  * @returns {Form}
@@ -125,6 +147,7 @@ router.get(
125
147
  (l) => l !== "emergency"
126
148
  );
127
149
  const layout_by_role = getState().getConfig("layout_by_role");
150
+ const twofa_policy_by_role = getState().getConfig("twofa_policy_by_role");
128
151
  send_users_page({
129
152
  res,
130
153
  req,
@@ -142,6 +165,13 @@ router.get(
142
165
  key: (role) =>
143
166
  editRoleLayoutForm(role, layouts, layout_by_role, req),
144
167
  },
168
+ {
169
+ label: req.__("2FA policy"),
170
+ key: (role) =>
171
+ role.id === 10
172
+ ? ""
173
+ : editRole2FAPolicyForm(role, twofa_policy_by_role, req),
174
+ },
145
175
  {
146
176
  label: req.__("Delete"),
147
177
  key: (r) =>
@@ -240,6 +270,26 @@ router.post(
240
270
  })
241
271
  );
242
272
 
273
+ /**
274
+ * @name post/setrolelayout/:id
275
+ * @function
276
+ * @memberof module:auth/roleadmin~roleadminRouter
277
+ */
278
+ router.post(
279
+ "/setrole2fapolicy/:id",
280
+ isAdmin,
281
+ error_catcher(async (req, res) => {
282
+ const { id } = req.params;
283
+ const twofa_policy_by_role = getState().getConfigCopy(
284
+ "twofa_policy_by_role"
285
+ );
286
+ twofa_policy_by_role[+id] = req.body.policy;
287
+ await getState().setConfig("twofa_policy_by_role", twofa_policy_by_role);
288
+ req.flash("success", req.__(`Saved 2FA policy for role`));
289
+
290
+ res.redirect(`/roleadmin`);
291
+ })
292
+ );
243
293
  const unDeletableRoles = [1, 8, 10];
244
294
  /**
245
295
  * @name post/delete/:id
package/auth/routes.js CHANGED
@@ -24,12 +24,14 @@ const { renderForm, post_btn } = require("@saltcorn/markup");
24
24
  const passport = require("passport");
25
25
  const {
26
26
  a,
27
+ img,
27
28
  text,
28
29
  table,
29
30
  tbody,
30
31
  th,
31
32
  td,
32
33
  tr,
34
+ h4,
33
35
  form,
34
36
  select,
35
37
  option,
@@ -37,6 +39,8 @@ const {
37
39
  i,
38
40
  div,
39
41
  code,
42
+ pre,
43
+ p,
40
44
  } = require("@saltcorn/markup/tags");
41
45
  const {
42
46
  available_languages,
@@ -52,7 +56,9 @@ const { restore_backup } = require("../markup/admin.js");
52
56
  const { restore } = require("@saltcorn/data/models/backup");
53
57
  const load_plugins = require("../load_plugins");
54
58
  const fs = require("fs");
55
-
59
+ const base32 = require("thirty-two");
60
+ const qrcode = require("qrcode");
61
+ const totp = require("notp").totp;
56
62
  /**
57
63
  * @type {object}
58
64
  * @const
@@ -599,6 +605,8 @@ const signup_login_with_user = (u, req, res) =>
599
605
  if (!err) {
600
606
  Trigger.emitEvent("Login", null, u);
601
607
  if (getState().verifier) res.redirect("/auth/verification-flow");
608
+ else if (getState().get2FApolicy(u) === "Mandatory")
609
+ res.redirect("/auth/twofa/setup/totp");
602
610
  else res.redirect("/");
603
611
  } else {
604
612
  req.flash("danger", err);
@@ -920,6 +928,11 @@ router.post(
920
928
  error_catcher(async (req, res) => {
921
929
  ipLimiter.resetKey(req.ip);
922
930
  userLimiter.resetKey(userIdKey(req.body));
931
+ if (req.user.pending_user) {
932
+ res.redirect("/auth/twofa/login/totp");
933
+ return;
934
+ }
935
+
923
936
  if (req.session.cookie)
924
937
  if (req.body.remember) {
925
938
  const setDur = +getState().getConfig("cookie_duration_remember", 0);
@@ -932,7 +945,9 @@ router.post(
932
945
  }
933
946
  Trigger.emitEvent("Login", null, req.user);
934
947
  req.flash("success", req.__("Welcome, %s!", req.user.email));
935
- res.redirect("/");
948
+ if (getState().get2FApolicy(req.user) === "Mandatory") {
949
+ res.redirect("/auth/twofa/setup/totp");
950
+ } else res.redirect("/");
936
951
  })
937
952
  );
938
953
 
@@ -1095,6 +1110,9 @@ const userSettings = async ({ req, res, pwform, user }) => {
1095
1110
  }
1096
1111
  let apikeycard;
1097
1112
  const min_role_apikeygen = +getState().getConfig("min_role_apikeygen", 1);
1113
+ const twoFaPolicy = getState().get2FApolicy(user);
1114
+ const show2FAPolicy =
1115
+ twoFaPolicy !== "Disabled" || user._attributes.totp_enabled;
1098
1116
  if (user.role_id <= min_role_apikeygen)
1099
1117
  apikeycard = {
1100
1118
  type: "card",
@@ -1163,6 +1181,40 @@ const userSettings = async ({ req, res, pwform, user }) => {
1163
1181
  title: req.__("Change password"),
1164
1182
  contents: renderForm(pwform, req.csrfToken()),
1165
1183
  },
1184
+ ...(show2FAPolicy
1185
+ ? [
1186
+ {
1187
+ type: "card",
1188
+ title: req.__("Two-factor authentication"),
1189
+ contents: [
1190
+ div(
1191
+ user._attributes.totp_enabled
1192
+ ? req.__("Two-factor authentication is enabled")
1193
+ : req.__("Two-factor authentication is disabled")
1194
+ ),
1195
+ div(
1196
+ user._attributes.totp_enabled
1197
+ ? post_btn(
1198
+ "/auth/twofa/disable/totp",
1199
+ "Disable",
1200
+ req.csrfToken(),
1201
+ {
1202
+ btnClass: "btn-danger mt-2",
1203
+ req,
1204
+ }
1205
+ )
1206
+ : a(
1207
+ {
1208
+ href: "/auth/twofa/setup/totp",
1209
+ class: "btn btn-primary mt-2",
1210
+ },
1211
+ "Enable"
1212
+ )
1213
+ ),
1214
+ ],
1215
+ },
1216
+ ]
1217
+ : []),
1166
1218
  ...(apikeycard ? [apikeycard] : []),
1167
1219
  ],
1168
1220
  };
@@ -1253,6 +1305,12 @@ router.get(
1253
1305
  loggedIn,
1254
1306
  error_catcher(async (req, res) => {
1255
1307
  const user = await User.findOne({ id: req.user.id });
1308
+ if (!user) {
1309
+ req.logout();
1310
+ req.flash("danger", req.__("Must be logged in first"));
1311
+ res.redirect("/auth/login");
1312
+ return;
1313
+ }
1256
1314
  res.sendWrap(
1257
1315
  req.__("User settings"),
1258
1316
  await userSettings({ req, res, pwform: changPwForm(req), user })
@@ -1436,3 +1494,171 @@ router.all(
1436
1494
  res.redirect(wfres.redirect || "/");
1437
1495
  })
1438
1496
  );
1497
+
1498
+ /**
1499
+ * @name get/settings
1500
+ * @function
1501
+ * @memberof module:auth/routes~routesRouter
1502
+ */
1503
+ router.get(
1504
+ "/twofa/setup/totp",
1505
+ loggedIn,
1506
+ error_catcher(async (req, res) => {
1507
+ const user = await User.findOne({ id: req.user.id });
1508
+ let key;
1509
+ if (user._attributes.totp_key) key = user._attributes.totp_key;
1510
+ else {
1511
+ key = randomKey(10);
1512
+ user._attributes.totp_key = key;
1513
+ await user.update({ _attributes: user._attributes });
1514
+ }
1515
+
1516
+ const encodedKey = base32.encode(key);
1517
+
1518
+ // generate QR code for scanning into Google Authenticator
1519
+ // reference: https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
1520
+ const site_name = getState().getConfig("site_name");
1521
+ const otpUrl = `otpauth://totp/${
1522
+ user.email
1523
+ }?secret=${encodedKey}&period=30&issuer=${encodeURIComponent(site_name)}`;
1524
+ const image = await qrcode.toDataURL(otpUrl);
1525
+ res.sendWrap(req.__("Setup two-factor authentication"), {
1526
+ type: "card",
1527
+ title: req.__(
1528
+ "Setup two-factor authentication with Time-based One-Time Password (TOTP)"
1529
+ ),
1530
+ contents: [
1531
+ h4(req.__("1. Scan this QR code in your Authenticator app")),
1532
+ img({ src: image }),
1533
+ p("Or enter this code:"),
1534
+ code(pre(encodedKey.toString())),
1535
+ h4(
1536
+ req.__(
1537
+ "2. Enter the six-digit code generated in your Authenticator app"
1538
+ )
1539
+ ),
1540
+ renderForm(totpForm(req), req.csrfToken()),
1541
+ ],
1542
+ });
1543
+ })
1544
+ );
1545
+
1546
+ router.post(
1547
+ "/twofa/setup/totp",
1548
+ loggedIn,
1549
+ error_catcher(async (req, res) => {
1550
+ const user = await User.findOne({ id: req.user.id });
1551
+
1552
+ if (!user._attributes.totp_key) {
1553
+ //key not set
1554
+ req.flash("danger", req.__("2FA TOTP Key not set"));
1555
+ res.redirect("/auth/twofa/setup/totp");
1556
+ return;
1557
+ }
1558
+
1559
+ const form = totpForm(req);
1560
+ form.validate(req.body);
1561
+ if (form.hasErrors) {
1562
+ req.flash("danger", req.__("Error processing form"));
1563
+ res.redirect("/auth/twofa/setup/totp");
1564
+ return;
1565
+ }
1566
+ const code = `${form.values.totpCode}`;
1567
+ const rv = totp.verify(code, user._attributes.totp_key, {
1568
+ time: 30,
1569
+ });
1570
+ if (!rv) {
1571
+ req.flash("danger", req.__("Could not verify code"));
1572
+ res.redirect("/auth/twofa/setup/totp");
1573
+ return;
1574
+ }
1575
+ user._attributes.totp_enabled = true;
1576
+ await user.update({ _attributes: user._attributes });
1577
+ req.flash(
1578
+ "success",
1579
+ req.__(
1580
+ "Two-factor authentication with Time-based One-Time Password enabled"
1581
+ )
1582
+ );
1583
+
1584
+ res.redirect("/auth/settings");
1585
+ })
1586
+ );
1587
+
1588
+ router.post(
1589
+ "/twofa/disable/totp",
1590
+ loggedIn,
1591
+ error_catcher(async (req, res) => {
1592
+ const user = await User.findOne({ id: req.user.id });
1593
+ user._attributes.totp_enabled = false;
1594
+ delete user._attributes.totp_key;
1595
+ await user.update({ _attributes: user._attributes });
1596
+ req.flash(
1597
+ "success",
1598
+ req.__(
1599
+ "Two-factor authentication with Time-based One-Time Password disabled"
1600
+ )
1601
+ );
1602
+ res.redirect("/auth/settings");
1603
+ })
1604
+ );
1605
+ const totpForm = (req) =>
1606
+ new Form({
1607
+ action: "/auth/twofa/setup/totp",
1608
+ fields: [
1609
+ {
1610
+ name: "totpCode",
1611
+ label: req.__("Code"),
1612
+ type: "Integer",
1613
+ required: true,
1614
+ },
1615
+ ],
1616
+ });
1617
+
1618
+ const randomKey = function (len) {
1619
+ function getRandomInt(min, max) {
1620
+ return Math.floor(Math.random() * (max - min + 1)) + min;
1621
+ }
1622
+ var buf = [],
1623
+ chars = "abcdefghijklmnopqrstuvwxyz0123456789",
1624
+ charlen = chars.length;
1625
+
1626
+ for (var i = 0; i < len; ++i) {
1627
+ buf.push(chars[getRandomInt(0, charlen - 1)]);
1628
+ }
1629
+
1630
+ return buf.join("");
1631
+ };
1632
+
1633
+ router.get(
1634
+ "/twofa/login/totp",
1635
+ error_catcher(async (req, res) => {
1636
+ const form = new Form({
1637
+ action: "/auth/twofa/login/totp",
1638
+ submitLabel: "Verify",
1639
+ fields: [
1640
+ {
1641
+ name: "code",
1642
+ label: req.__("Code"),
1643
+ type: "Integer",
1644
+ required: true,
1645
+ },
1646
+ ],
1647
+ });
1648
+ res.sendAuthWrap(req.__(`Two-factor authentication`), form, {});
1649
+ })
1650
+ );
1651
+
1652
+ router.post(
1653
+ "/twofa/login/totp",
1654
+ passport.authenticate("totp", {
1655
+ failureRedirect: "/auth/twofa/login/totp",
1656
+ failureFlash: true,
1657
+ }),
1658
+ error_catcher(async (req, res) => {
1659
+ const user = await User.findOne({ id: req.user.pending_user.id });
1660
+ user.relogin(req);
1661
+ Trigger.emitEvent("Login", null, user);
1662
+ res.redirect("/");
1663
+ })
1664
+ );