summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgumi <git@gumi.ca>2020-03-11 18:14:03 -0400
committergumi <git@gumi.ca>2020-03-11 18:14:03 -0400
commit267b900a82d7870e4bcd56857887fd3f697e6e45 (patch)
treea4211a9d3e3ff8db65c582c1bc6aac5d5b15a3ab
parent00e707bffa2157df7772e417a5d48e66229e4013 (diff)
downloadapi-267b900a82d7870e4bcd56857887fd3f697e6e45.tar.gz
api-267b900a82d7870e4bcd56857887fd3f697e6e45.tar.bz2
api-267b900a82d7870e4bcd56857887fd3f697e6e45.tar.xz
api-267b900a82d7870e4bcd56857887fd3f697e6e45.zip
refactor the authentication and validation
-rw-r--r--src/routers/vault/middlewares/account.js116
-rw-r--r--src/routers/vault/middlewares/evol/account.js185
-rw-r--r--src/routers/vault/middlewares/identity.js116
-rw-r--r--src/routers/vault/middlewares/legacy/account.js184
-rw-r--r--src/routers/vault/middlewares/session.js92
-rw-r--r--src/routers/vault/types/Session.js5
-rw-r--r--src/routers/vault/utils/validate.js204
7 files changed, 361 insertions, 541 deletions
diff --git a/src/routers/vault/middlewares/account.js b/src/routers/vault/middlewares/account.js
index 42a63a4..3c1cf52 100644
--- a/src/routers/vault/middlewares/account.js
+++ b/src/routers/vault/middlewares/account.js
@@ -1,42 +1,16 @@
"use strict";
+const validate = require("../utils/validate.js");
const regexes = {
token: /^[a-zA-Z0-9-_]{6,128}$/, // UUID
};
const get_data = async (req, res, next) => {
- const token = String(req.get("X-VAULT-SESSION") || "");
+ let session;
- if (!token.match(/^[a-zA-Z0-9-_]{6,128}$/)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
- const session = req.app.locals.session.get(token);
-
- if (session === null || session === undefined) {
- res.status(410).json({
- status: "error",
- error: "session expired",
- });
- req.app.locals.cooldown(req, 5e3);
- return;
- }
-
- if (session.authenticated !== true) {
- res.status(401).json({
- status: "error",
- error: "not authenticated",
- });
- req.app.locals.logger.warn(`Vault.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ try {
+ [, session] = validate.get_session(req, res);
+ } catch { return } // already handled
res.status(200).json({
status: "success",
@@ -45,6 +19,7 @@ const get_data = async (req, res, next) => {
primaryIdentity: session.primaryIdentity,
allowNonPrimary: session.allowNonPrimary,
strictIPCheck: session.strictIPCheck,
+ requireSecret: true,
vaultId: session.vault,
},
});
@@ -52,68 +27,26 @@ const get_data = async (req, res, next) => {
};
const update_account = async (req, res, next) => {
- const token = String(req.get("X-VAULT-SESSION") || "");
+ let session;
- if (!token.match(regexes.token)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ try {
+ [, session] = validate.get_session(req, res);
+ } catch { return } // already handled
- if (!req.body || !Reflect.has(req.body, "primary") || !Reflect.has(req.body, "allow") ||
- !Reflect.has(req.body, "strict") || !Number.isInteger(req.body.primary)) {
- res.status(400).json({
- status: "error",
- error: "invalid format",
- });
- req.app.locals.cooldown(req, 5e3);
- return;
- }
-
- const session = req.app.locals.session.get(token);
-
- if (session === null || session === undefined) {
- res.status(410).json({
- status: "error",
- error: "session expired",
- });
- req.app.locals.cooldown(req, 5e3);
- return;
- }
-
- if (session.authenticated !== true) {
- res.status(401).json({
- status: "error",
- error: "not authenticated",
- });
- req.app.locals.logger.warn(`Vault.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
- if (session.strictIPCheck && session.ip !== req.ip) {
- // the ip is not the same
- res.status(401).json({
- status: "error",
- error: "ip address mismatch",
- });
- req.app.locals.logger.warn(`Vault.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ const data = {
+ primary: +validate.get_prop(req, "primary"),
+ allow: !!validate.get_prop(req, "allow"),
+ strict: !!validate.get_prop(req, "strict"),
+ };
const update_fields = {};
- if (session.primaryIdentity !== req.body.primary) {
+ if (session.primaryIdentity !== data.primary) {
// update primary identity
let new_primary = null;
for (const ident of session.identities) {
- if (ident.id === req.body.primary) {
+ if (ident.id === data.primary) {
new_primary = ident.id;
break;
}
@@ -124,19 +57,18 @@ const update_account = async (req, res, next) => {
status: "error",
error: "not owned by you",
});
- req.app.locals.logger.warn(`Vault.account: blocked an attempt to bypass authentication [${req.ip}]`);
req.app.locals.cooldown(req, 3e5);
}
update_fields.primaryIdentity = new_primary;
}
- if (session.allowNonPrimary !== !!req.body.allow) {
+ if (session.allowNonPrimary !== data.allow) {
// update allow non-primary
- update_fields.allowNonPrimary = !!req.body.allow;
+ update_fields.allowNonPrimary = data.allow;
}
- if (session.strictIPCheck !== !!req.body.strict) {
+ if (session.strictIPCheck !== data.strict) {
// update allow non-primary
- update_fields.strictIPCheck = !!req.body.strict;
+ update_fields.strictIPCheck = data.strict;
}
// update SQL
@@ -147,9 +79,9 @@ const update_account = async (req, res, next) => {
}
// now update our cache
- session.allowNonPrimary = !!req.body.allow;
- session.strictIPCheck = !!req.body.strict;
- session.primaryIdentity = +req.body.primary;
+ session.allowNonPrimary = data.allow;
+ session.strictIPCheck = data.strict;
+ session.primaryIdentity = data.primary;
for (const ident of session.identities) {
if (ident.id === session.primaryIdentity) {
diff --git a/src/routers/vault/middlewares/evol/account.js b/src/routers/vault/middlewares/evol/account.js
index 3a22158..b89d872 100644
--- a/src/routers/vault/middlewares/evol/account.js
+++ b/src/routers/vault/middlewares/evol/account.js
@@ -1,57 +1,13 @@
"use strict";
const EvolAccount = require("../../types/EvolAccount.js");
-
-const regexes = {
- token: /^[a-zA-Z0-9-_]{6,128}$/, // UUID
- any30: /^[^\s][^\t\r\n]{6,28}[^\s]$/, // herc password (this looks scary)
- alnum23: /^[a-zA-Z0-9_]{4,23}$/, // mostly for username
- gid: /^[23][0-9]{6}$/, // account id
-};
+const validate = require("../../utils/validate.js");
const get_accounts = async (req, res, next) => {
- const token = String(req.get("X-VAULT-SESSION") || "");
-
- if (!token.match(regexes.token)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
- const session = req.app.locals.session.get(token);
-
- if (session === null || session === undefined) {
- res.status(410).json({
- status: "error",
- error: "session expired",
- });
- req.app.locals.cooldown(req, 5e3);
- return;
- }
-
- if (session.authenticated !== true) {
- res.status(401).json({
- status: "error",
- error: "not authenticated",
- });
- req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ let session;
- if (session.strictIPCheck && session.ip !== req.ip) {
- // the ip is not the same
- res.status(403).json({
- status: "error",
- error: "ip address mismatch",
- });
- req.app.locals.logger.warn(`Vault.evol.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ try {
+ [, session] = validate.get_session(req, res);
+ } catch { return } // already handled
res.status(200).json({
status: "success",
@@ -62,22 +18,18 @@ const get_accounts = async (req, res, next) => {
};
const new_account = async (req, res, next) => {
- const token = String(req.get("X-VAULT-SESSION") || "");
+ let session;
- if (!token.match(regexes.token)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ try {
+ [, session] = validate.get_session(req, res);
+ } catch { return } // already handled
- if (!req.body ||
- !Reflect.has(req.body, "username") || !Reflect.has(req.body, "password") ||
- !req.body.username.match(regexes.alnum23) ||
- !req.body.password.match(regexes.any30)) { // FIXME: this is unsafe: can cause a promise rejection if something else than a string is passed (no Number.match() exists)
+ const data = {
+ username: validate.get_prop(req, "username", validate.regexes.alnum23),
+ password: validate.get_prop(req, "password", validate.regexes.any30),
+ };
+
+ if (!data.username || !data.password) {
res.status(400).json({
status: "error",
error: "invalid format",
@@ -86,41 +38,9 @@ const new_account = async (req, res, next) => {
return;
}
- const session = req.app.locals.session.get(token);
-
- if (session === null || session === undefined) {
- res.status(410).json({
- status: "error",
- error: "session expired",
- });
- req.app.locals.cooldown(req, 5e3);
- return;
- }
-
- if (session.authenticated !== true) {
- res.status(401).json({
- status: "error",
- error: "not authenticated",
- });
- req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
- if (session.strictIPCheck && session.ip !== req.ip) {
- // the ip is not the same
- res.status(403).json({
- status: "error",
- error: "ip address mismatch",
- });
- req.app.locals.logger.warn(`Vault.evol.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
// this check is necessary because login.userid has no UNIQUE constraint
const existing = await req.app.locals.evol.login.findOne({
- where: {userid: req.body.username}
+ where: {userid: data.username}
});
if (existing !== null) {
@@ -133,8 +53,8 @@ const new_account = async (req, res, next) => {
}
const evol_acc = await req.app.locals.evol.login.create({
- userid: req.body.username,
- userPass: req.body.password,
+ userid: data.username,
+ userPass: data.password,
email: `${session.vault}@vault`, // setting an actual email is pointless
});
@@ -167,22 +87,19 @@ const new_account = async (req, res, next) => {
};
const update_account = async (req, res, next) => {
- const token = String(req.get("X-VAULT-SESSION") || "");
+ let session;
- if (!token.match(regexes.token)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ try {
+ [, session] = validate.get_session(req, res);
+ } catch { return } // already handled
+
+ const data = {
+ accountId: +validate.get_prop(req, "accountId", validate.regexes.gid),
+ username: validate.get_prop(req, "username", validate.regexes.alnum23),
+ password: validate.get_prop(req, "password", validate.regexes.any30),
+ };
- if (!req.body || !Reflect.has(req.body, "accountId") ||
- !String(req.body.accountId).match(regexes.gid) || !(
- (Reflect.has(req.body, "username") && req.body.username.match(regexes.alnum23)) ||
- (Reflect.has(req.body, "password") && req.body.password.match(regexes.any30)))) { // FIXME: this is unsafe: can cause a promise rejection if something else than a string is passed (no Number.match() exists)
+ if (!data.username && !data.password) {
res.status(400).json({
status: "error",
error: "invalid format",
@@ -191,41 +108,9 @@ const update_account = async (req, res, next) => {
return;
}
- const session = req.app.locals.session.get(token);
-
- if (session === null || session === undefined) {
- res.status(410).json({
- status: "error",
- error: "session expired",
- });
- req.app.locals.cooldown(req, 5e3);
- return;
- }
-
- if (session.authenticated !== true) {
- res.status(401).json({
- status: "error",
- error: "not authenticated",
- });
- req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
- if (session.strictIPCheck && session.ip !== req.ip) {
- // the ip is not the same
- res.status(403).json({
- status: "error",
- error: "ip address mismatch",
- });
- req.app.locals.logger.warn(`Vault.evol.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
let account = null;
for (const acc of session.gameAccounts) {
- if (acc.accountId === req.body.accountId) {
+ if (acc.accountId === data.accountId) {
account = acc;
break;
}
@@ -242,10 +127,10 @@ const update_account = async (req, res, next) => {
}
let update_fields = {};
- if (Reflect.has(req.body, "username")) {
+ if (data.username) {
// check if the name exists
const existing = await req.app.locals.evol.login.findOne({
- where: {userid: req.body.username}
+ where: {userid: data.username}
});
if (existing !== null) {
@@ -258,9 +143,9 @@ const update_account = async (req, res, next) => {
}
update_fields = {
- userid: req.body.username,
+ userid: data.username,
};
- account.name = req.body.username;
+ account.name = data.username;
req.app.locals.logger.info(`Vault.evol.account: changed username of game account ${account.accountId} <${session.vault}@vault> [${req.ip}]`);
req.app.locals.vault.account_log.create({
vaultId: session.vault,
@@ -272,7 +157,7 @@ const update_account = async (req, res, next) => {
});
} else {
update_fields = {
- userPass: req.body.password,
+ userPass: data.password,
};
req.app.locals.logger.info(`Vault.evol.account: changed password of game account ${account.accountId} <${session.vault}@vault> [${req.ip}]`);
req.app.locals.vault.account_log.create({
diff --git a/src/routers/vault/middlewares/identity.js b/src/routers/vault/middlewares/identity.js
index 158e5df..f65d757 100644
--- a/src/routers/vault/middlewares/identity.js
+++ b/src/routers/vault/middlewares/identity.js
@@ -2,6 +2,7 @@
const uuidv4 = require("uuid/v4");
const nodemailer = require("nodemailer");
const Claim = require("../utils/claim.js");
+const validate = require("../utils/validate.js");
let transporter = nodemailer.createTransport({
sendmail: true,
@@ -10,38 +11,11 @@ let transporter = nodemailer.createTransport({
});
const get_identities = async (req, res, next) => {
- const token = String(req.get("X-VAULT-SESSION") || "");
-
- if (!token.match(/^[a-zA-Z0-9-_]{6,128}$/)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.identity: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
- const session = req.app.locals.session.get(token);
-
- if (session === null || session === undefined) {
- res.status(410).json({
- status: "error",
- error: "session expired",
- });
- req.app.locals.cooldown(req, 5e3);
- return;
- }
+ let session;
- if (session.authenticated !== true) {
- res.status(401).json({
- status: "error",
- error: "not authenticated",
- });
- req.app.locals.logger.warn(`Vault.identity: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ try {
+ [, session] = validate.get_session(req, res);
+ } catch { return } // already handled
if (session.identities.length === 0) {
console.info(`Vault.identity: fetching identities <${session.vault}@vault> [${req.ip}]`);
@@ -69,19 +43,19 @@ const get_identities = async (req, res, next) => {
const add_identity = async (req, res, next) => {
const token = String(req.get("X-VAULT-SESSION") || "");
- const validate = String(req.get("X-VAULT-TOKEN") || "");
+ const secret = String(req.get("X-VAULT-TOKEN") || "");
- if (token === "" && validate !== "") {
- if (!validate.match(/^[a-zA-Z0-9-_]{6,128}$/)) {
+ if (token === "" && secret !== "") {
+ if (!secret.match(validate.regexes.uuid)) {
res.status(400).json({
status: "error",
- error: "missing token",
+ error: "missing secret",
});
req.app.locals.cooldown(req, 5e3);
return;
}
- const ident = req.app.locals.identity_pending.get(validate);
+ const ident = req.app.locals.identity_pending.get(secret);
if (ident === null || ident === undefined) {
res.status(410).json({
@@ -128,7 +102,7 @@ const add_identity = async (req, res, next) => {
}
}
- req.app.locals.identity_pending.delete(validate);
+ req.app.locals.identity_pending.delete(secret);
if (session !== null) {
console.info(`Vault.identity: added a new identity <${session.vault}@vault> [${req.ip}]`);
@@ -145,50 +119,19 @@ const add_identity = async (req, res, next) => {
// request to add
- if (!token.match(/^[a-zA-Z0-9-_]{6,128}$/)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.identity: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
- if (!req.body || !Reflect.has(req.body, "email") ||
- !req.body.email.match(/^(?:[a-zA-Z0-9.$&+=_~-]{1,255}@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,255}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,255}[a-zA-Z0-9])?){1,9})$/) ||
- req.body.email.length >= 320) {
- res.status(400).json({
- status: "error",
- error: "invalid email address",
- });
- req.app.locals.cooldown(req, 1e3);
- return;
- }
-
- const session = req.app.locals.session.get(token);
+ let session;
- if (session === null || session === undefined) {
- res.status(410).json({
- status: "error",
- error: "session expired",
- });
- req.app.locals.cooldown(req, 5e3);
- return;
- }
+ try {
+ [, session] = validate.get_session(req, res);
+ } catch { return } // already handled
- if (session.authenticated !== true) {
- res.status(401).json({
- status: "error",
- error: "not authenticated",
- });
- req.app.locals.logger.warn(`Vault.identity: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ let email;
+ try {
+ email = validate.get_email(req, res);
+ } catch { return } // already handled
for (const [key, pending] of req.app.locals.identity_pending) {
- if (pending.vault === session.vault && pending.email === req.body.email) {
+ if (pending.vault === session.vault && pending.email === email) {
res.status(425).json({
status: "error",
error: "already pending",
@@ -199,7 +142,7 @@ const add_identity = async (req, res, next) => {
}
const find = await req.app.locals.vault.identity.findOne({
- where: {email: req.body.email}
+ where: {email}
});
if (find !== null) {
@@ -232,7 +175,7 @@ const add_identity = async (req, res, next) => {
req.app.locals.identity_pending.set(uuid, {
ip: req.ip,
vault: session.vault,
- email: req.body.email,
+ email: email,
});
console.log(`Vault.session: starting identity validation <${session.vault}@vault> [${req.ip}]`);
@@ -243,7 +186,7 @@ const add_identity = async (req, res, next) => {
// TODO: limit total number of emails that can be dispatched by a single ip in an hour
transporter.sendMail({
from: process.env.VAULT__MAILER__FROM,
- to: req.body.email,
+ to: email,
subject: "The Mana World identity validation",
text: "You are receiving this email because someone (you?) has requested to link your email address "+
"to a TMW Vault account.\nIf you did not initiate this process, please ignore this email.\n\n"+
@@ -258,14 +201,6 @@ const add_identity = async (req, res, next) => {
req.app.locals.cooldown(req, 5e3);
};
-const update_identity = async (req, res, next) => {
- // TODO
-};
-
-const drop_identity = async (req, res, next) => {
- // TODO
-};
-
module.exports = exports = async (req, res, next) => {
switch(req.method) {
case "GET":
@@ -274,11 +209,8 @@ module.exports = exports = async (req, res, next) => {
case "POST":
// add identity
return await add_identity(req, res, next);
- case "PATCH":
- // set as primary
- //return await update_identity(req, res, next);
case "DELETE":
- // remove an identity
+ // TODO: remove an identity
//return await drop_identity(req, res, next);
default:
next(); // fallthrough to default endpoint (404)
diff --git a/src/routers/vault/middlewares/legacy/account.js b/src/routers/vault/middlewares/legacy/account.js
index 29da5a6..199ace3 100644
--- a/src/routers/vault/middlewares/legacy/account.js
+++ b/src/routers/vault/middlewares/legacy/account.js
@@ -5,59 +5,14 @@ const LegacyAccount = require("../../types/LegacyAccount.js");
const LegacyChar = require("../../types/LegacyChar.js");
const EvolAccount = require("../../types/EvolAccount.js");
const EvolChar = require("../../types/EvolChar.js");
-
-const regexes = {
- token: /^[a-zA-Z0-9-_]{6,128}$/, // UUID
- any23: /^[^\s][^\t\r\n]{2,21}[^\s]$/, // tmwa password (this looks scary)
- any30: /^[^\s][^\t\r\n]{6,28}[^\s]$/, // herc password (this looks scary)
- alnum23: /^[a-zA-Z0-9_]{4,23}$/, // mostly for username
- gid: /^[23][0-9]{6}$/, // account id
-};
+const validate = require("../../utils/validate.js");
const get_accounts = async (req, res, next) => {
- const token = String(req.get("X-VAULT-SESSION") || "");
-
- if (!token.match(regexes.token)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.legacy.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
- const session = req.app.locals.session.get(token);
-
- if (session === null || session === undefined) {
- res.status(410).json({
- status: "error",
- error: "session expired",
- });
- req.app.locals.cooldown(req, 5e3);
- return;
- }
-
- if (session.authenticated !== true) {
- res.status(401).json({
- status: "error",
- error: "not authenticated",
- });
- req.app.locals.logger.warn(`Vault.legacy.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ let session;
- if (session.strictIPCheck && session.ip !== req.ip) {
- // the ip is not the same
- res.status(403).json({
- status: "error",
- error: "ip address mismatch",
- });
- req.app.locals.logger.warn(`Vault.legacy.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ try {
+ [, session] = validate.get_session(req, res);
+ } catch { return } // already handled
res.status(200).json({
status: "success",
@@ -68,21 +23,18 @@ const get_accounts = async (req, res, next) => {
};
const claim_by_password = async (req, res, next) => {
- const token = String(req.get("X-VAULT-SESSION") || "");
+ let session;
- if (!token.match(regexes.token)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.legacy.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ try {
+ [, session] = validate.get_session(req, res);
+ } catch { return } // already handled
- if (!req.body || !Reflect.has(req.body, "username") || !Reflect.has(req.body, "password") ||
- !req.body.username.match(regexes.alnum23) ||
- !req.body.password.match(regexes.any23)) {
+ const data = {
+ username: validate.get_prop(req, "username", validate.regexes.alnum23),
+ password: validate.get_prop(req, "password", validate.regexes.any23),
+ };
+
+ if (!data.username || !data.password) {
res.status(400).json({
status: "error",
error: "invalid format",
@@ -91,40 +43,8 @@ const claim_by_password = async (req, res, next) => {
return;
}
- const session = req.app.locals.session.get(token);
-
- if (session === null || session === undefined) {
- res.status(410).json({
- status: "error",
- error: "session expired",
- });
- req.app.locals.cooldown(req, 5e3);
- return;
- }
-
- if (session.authenticated !== true) {
- res.status(401).json({
- status: "error",
- error: "not authenticated",
- });
- req.app.locals.logger.warn(`Vault.legacy.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
- if (session.strictIPCheck && session.ip !== req.ip) {
- // the ip is not the same
- res.status(403).json({
- status: "error",
- error: "ip address mismatch",
- });
- req.app.locals.logger.warn(`Vault.legacy.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
const legacy = await req.app.locals.legacy.login.findOne({
- where: {userid: req.body.username}
+ where: {userid: data.username}
});
if (legacy === null) {
@@ -146,14 +66,14 @@ const claim_by_password = async (req, res, next) => {
return;
}
- if (!md5saltcrypt.verify(legacy.userPass, req.body.password)) {
+ if (!md5saltcrypt.verify(legacy.userPass, data.password)) {
// check to see if the password has been updated since it was dumped to SQL
const flatfile_account = await flatfile.findAccount(legacy.accountId, legacy.userid); // this operation is costly
if (flatfile_account !== null &&
- md5saltcrypt.verify(flatfile_account.password, req.body.password)) {
+ md5saltcrypt.verify(flatfile_account.password, data.password)) {
// update the password in SQL (deferred)
console.log(`Vault.legacy.account: updating SQL password from flatfile for account ${legacy.accountId}`);
- legacy.userPass = md5saltcrypt.hash(req.body.password);
+ legacy.userPass = md5saltcrypt.hash(data.password);
legacy.save();
} else {
// the password is just plain wrong
@@ -231,23 +151,19 @@ const claim_by_password = async (req, res, next) => {
};
const migrate = async (req, res, next) => {
- const token = String(req.get("X-VAULT-SESSION") || "");
+ let session;
- if (!token.match(regexes.token)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.legacy.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ try {
+ [, session] = validate.get_session(req, res);
+ } catch { return } // already handled
+
+ const data = {
+ accountId: +validate.get_prop(req, "accountId", validate.regexes.gid),
+ username: validate.get_prop(req, "username", validate.regexes.alnum23),
+ password: validate.get_prop(req, "password", validate.regexes.any30),
+ };
- if (!req.body || !Reflect.has(req.body, "accountId") ||
- !Reflect.has(req.body, "username") || !Reflect.has(req.body, "password") ||
- !req.body.username.match(regexes.alnum23) ||
- !req.body.password.match(regexes.any30) || // FIXME: this is unsafe: can cause a promise rejection if something else than a string is passed (no Number.match() exists)
- !String(req.body.accountId).match(regexes.gid)) {
+ if (!data.username || !data.password) {
res.status(400).json({
status: "error",
error: "invalid format",
@@ -256,44 +172,12 @@ const migrate = async (req, res, next) => {
return;
}
- const session = req.app.locals.session.get(token);
-
- if (session === null || session === undefined) {
- res.status(410).json({
- status: "error",
- error: "session expired",
- });
- req.app.locals.cooldown(req, 5e3);
- return;
- }
-
- if (session.authenticated !== true) {
- res.status(401).json({
- status: "error",
- error: "not authenticated",
- });
- req.app.locals.logger.warn(`Vault.legacy.account: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
- if (session.strictIPCheck && session.ip !== req.ip) {
- // the ip is not the same
- res.status(403).json({
- status: "error",
- error: "ip address mismatch",
- });
- req.app.locals.logger.warn(`Vault.legacy.account: ip address mismatch <${session.vault}@vault> [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
-
let legacy = null;
// check if we own it
// NOTE: this cached data is never stale because we update it when operations are performed
for (const acc of session.legacyAccounts) {
- if (acc.accountId === req.body.accountId) {
+ if (acc.accountId === data.accountId) {
legacy = acc;
break;
}
@@ -323,7 +207,7 @@ const migrate = async (req, res, next) => {
// this check is necessary because login.userid has no UNIQUE constraint
const existing = await req.app.locals.evol.login.findOne({
- where: {userid: req.body.username}
+ where: {userid: data.username}
});
if (existing !== null) {
@@ -336,8 +220,8 @@ const migrate = async (req, res, next) => {
}
const evol_acc = await req.app.locals.evol.login.create({
- userid: req.body.username,
- userPass: req.body.password,
+ userid: data.username,
+ userPass: data.password,
email: `${session.vault}@vault`, // setting an actual email is pointless
});
diff --git a/src/routers/vault/middlewares/session.js b/src/routers/vault/middlewares/session.js
index d5aa521..a961840 100644
--- a/src/routers/vault/middlewares/session.js
+++ b/src/routers/vault/middlewares/session.js
@@ -4,6 +4,7 @@ const nodemailer = require("nodemailer");
const Claim = require("../utils/claim.js");
const Session = require("../types/Session.js");
const game_accounts = require("../utils/game_accounts.js");
+const validate = require("../utils/validate.js");
let transporter = nodemailer.createTransport({
sendmail: true,
@@ -11,23 +12,16 @@ let transporter = nodemailer.createTransport({
path: '/usr/sbin/sendmail'
});
-const delete_session = async (req, res, next) => {
- const token = String(req.get("X-VAULT-SESSION") || "");
+const delete_session = async (req, res) => {
+ let token, session;
- if (!token.match(/^[a-zA-Z0-9-_]{6,128}$/)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.session: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+ try {
+ [token, session] = validate.get_session(req, res);
+ } catch { return } // already handled
- const session = req.app.locals.session.get(token);
req.app.locals.cooldown(1e4); // cooldown no matter what
- if (session === null || session === undefined) {
+ if (session === null) {
// session is already expired
res.status(200).json({
status: "success",
@@ -50,22 +44,14 @@ const delete_session = async (req, res, next) => {
});
};
-const auth_session = async (req, res, next) => {
- const token = String(req.get("X-VAULT-SESSION") || "");
-
- if (!token.match(/^[a-zA-Z0-9-_]{6,128}$/)) {
- res.status(400).json({
- status: "error",
- error: "missing session key",
- });
- req.app.locals.logger.warn(`Vault.session: blocked an attempt to bypass authentication [${req.ip}]`);
- req.app.locals.cooldown(req, 3e5);
- return;
- }
+const auth_session = async (req, res) => {
+ let token, session;
- const session = req.app.locals.session.get(token);
+ try {
+ [token, session] = validate.get_raw_session(req, res);
+ } catch { return } // already handled
- if (session === null || session === undefined) {
+ if (session === null) {
res.status(410).json({
status: "error",
error: "session expired",
@@ -85,8 +71,7 @@ const auth_session = async (req, res, next) => {
return;
}
- if (session.strictIPCheck && session.ip !== req.ip) {
- // not the same ip
+ if (!validate.check_ip(req, session)) {
res.status(403).json({
status: "error",
error: "ip address mismatch",
@@ -96,6 +81,7 @@ const auth_session = async (req, res, next) => {
}
});
+ console.warn(`Vault.session: ip address mismatch <${session.vault}@vault> [${req.ip}]`);
req.app.locals.cooldown(req, 5e3);
return;
}
@@ -110,18 +96,12 @@ const auth_session = async (req, res, next) => {
return;
}
- if (!req.query || !Reflect.has(req.query, "email") ||
- !req.query.email.match(/^(?:[a-zA-Z0-9.$&+=_~-]{1,255}@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,255}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,255}[a-zA-Z0-9])?){1,9})$/) ||
- req.query.email.length >= 320) {
- res.status(400).json({
- status: "error",
- error: "invalid email address",
- });
- req.app.locals.cooldown(req, 1e3);
- return;
- }
+ let email;
+ try {
+ email = validate.get_email(req, res);
+ } catch { return } // already handled
- if (req.query.email.toLowerCase() !== session.email) {
+ if (email !== session.email) {
res.status(410).json({
status: "error",
error: "session expired",
@@ -188,6 +168,8 @@ const auth_session = async (req, res, next) => {
status: "error",
error: "illegal identity"
});
+
+ console.error(`Vault.session: dangling session [${req.ip}]`);
req.app.locals.session.delete(token);
req.app.locals.cooldown(req, 3e5);
return;
@@ -246,12 +228,13 @@ const auth_session = async (req, res, next) => {
// immediately change the session uuid
const new_uuid = uuidv4();
req.app.locals.session.set(new_uuid, session);
- req.app.locals.session.delete(token);
+ req.app.locals.session.delete(token); // revoke the old uuid
res.status(200).json({
status: "success",
session: {
key: new_uuid,
+ secret: session.secret, // give them the session secret (only shared once)
expires: session.expires,
identity: session.identity,
},
@@ -259,30 +242,25 @@ const auth_session = async (req, res, next) => {
};
const new_session = async (req, res, next) => {
- if (!req.body || !Reflect.has(req.body, "email") ||
- !req.body.email.match(/^(?:[a-zA-Z0-9.$&+=_~-]{1,255}@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,255}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,255}[a-zA-Z0-9])?){1,9})$/) ||
- req.body.email.length >= 320) {
- res.status(400).json({
- status: "error",
- error: "invalid email address",
- });
- req.app.locals.cooldown(req, 1e3);
- return;
- }
+ let email;
+ try {
+ email = validate.get_email(req, res);
+ } catch { return } // already handled
- const identity = await req.app.locals.vault.identity.findOne({where: {email: req.body.email}});
+ const identity = await req.app.locals.vault.identity.findOne({where: {email: email}});
if (identity === null) {
// never logged in with this email address
+ const confirm = validate.get_prop(req, "confirm");
- if (Reflect.has(req.body, "confirm") && req.body.confirm === true) {
+ if (confirm) {
// account creation request
let uuid;
do { // avoid collisions
uuid = uuidv4();
} while (req.app.locals.session.get(uuid));
- const session = new Session(req.ip, req.body.email);
+ const session = new Session(req.ip, email);
req.app.locals.session.set(uuid, session);
console.log(`Vault.session: starting account creation process [${req.ip}]`);
@@ -292,7 +270,7 @@ const new_session = async (req, res, next) => {
} else {
transporter.sendMail({
from: process.env.VAULT__MAILER__FROM,
- to: req.body.email,
+ to: email,
subject: "The Mana World account creation",
text: "You are receiving this email because someone (you?) has requested to link your email address "+
"to a new TMW Vault account.\nIf you did not initiate this process, please ignore this email.\n\n"+
@@ -361,7 +339,7 @@ const new_session = async (req, res, next) => {
uuid = uuidv4();
} while (req.app.locals.session.get(uuid));
- const session = new Session(req.ip, req.body.email);
+ const session = new Session(req.ip, email);
session.vault = account.id;
session.primaryIdentity = account.primaryIdentity;
session.allowNonPrimary = account.allowNonPrimary;
@@ -376,7 +354,7 @@ const new_session = async (req, res, next) => {
} else {
transporter.sendMail({
from: process.env.VAULT__MAILER__FROM,
- to: req.body.email,
+ to: email,
subject: "TMW Vault login",
text: `Here is your login link:\n${process.env.VAULT__URL__AUTH}${uuid}\n\n` +
"TMW staff members will never ask for your login link. Please do not " +
diff --git a/src/routers/vault/types/Session.js b/src/routers/vault/types/Session.js
index 9f0cd95..17c77ef 100644
--- a/src/routers/vault/types/Session.js
+++ b/src/routers/vault/types/Session.js
@@ -1,3 +1,5 @@
+const uuidv4 = require("uuid/v4");
+
/**
* holds a cache of all the user data fetched from SQL
*/
@@ -12,6 +14,8 @@ module.exports = class Session {
identity = null;
/** the email address of the identity that was used to log in */
email;
+ /** the secret that is sent after authentication */
+ secret;
/** cache holding all identities */
identities = [];
/** the main identity of the account */
@@ -30,6 +34,7 @@ module.exports = class Session {
constructor (ip, email) {
this.ip = ip;
this.email = email.toLowerCase();
+ this.secret = uuidv4();
}
/**
diff --git a/src/routers/vault/utils/validate.js b/src/routers/vault/utils/validate.js
new file mode 100644
index 0000000..a0d0ea3
--- /dev/null
+++ b/src/routers/vault/utils/validate.js
@@ -0,0 +1,204 @@
+"use strict";
+const Session = require("../types/Session.js");
+
+/** thrown when the user attempts to bypass security measures */
+class BypassAttempt extends Error {};
+/** thrown when the received data does not match the expected format */
+class ValidationError extends Error {};
+
+/** the patterns used for parsing */
+const regexes = {
+ /** a Universally Unique Identifier */
+ uuid: /^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i,
+ /** tmwa password */
+ any23: /^[^\s][^\t\r\n]{2,21}[^\s]$/,
+ /** hercules password */
+ any30: /^[^\s][^\t\r\n]{6,28}[^\s]$/,
+ /** username */
+ alnum23: /^\w{4,23}$/i,
+ /** tmwa/hercules GID */
+ gid: /^[23][0-9]{6}$/,
+ /** RFC 5322 email, but must also have a TLD */
+ email: /^(?:[a-zA-Z0-9.$&+=_~-]{1,255}@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,255}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,255}[a-zA-Z0-9])?){1,9})$/i,
+};
+
+/**
+ * gets the name of the endpoint for pretty-printing
+ * @param {Request} req
+ * @returns {string} the pretty name
+ */
+const get_endpoint = req => `Vault.${req.path.slice(1).split("/").join(".")}`;
+
+/**
+ * log a bad action
+ * @param {Request} req
+ * @param {string} msg
+ */
+const warn = (req, msg) =>
+ req.app.locals.logger.warn(`${get_endpoint(req)}: ${msg} [${req.ip}]`);
+
+/**
+ * check the request ip against the session settings
+ * @param {Request}
+ * @param {Session} session
+ * @returns {boolean} whether the ip is allowed to use this session
+ */
+const check_ip = (req, session) => !session.strictIPCheck || req.ip === session.ip;
+
+/**
+ * gets a property from the body or from the query
+ * @param {Request} req
+ * @param {string} prop - the name of the property to get
+ * @returns {string} the property value
+ */
+const get_prop = (req, prop, regex = null) => {
+ try {
+ let value = "";
+
+ if (req.body && Reflect.has(req.body, prop)) {
+ value = String(req.body[prop]);
+ } else if (req.query && Reflect.has(req.query, prop)) {
+ value = String(req.query[prop]);
+ }
+
+ if (regex !== null && !value.match(regex)) {
+ return ""; // does not match the provided regex
+ }
+
+ return value;
+ } catch {
+ return ""; // couldn't convert to string
+ }
+};
+
+/**
+ * get the secret
+ * @param {Request}
+ * @returns {string} the session secret
+ */
+const get_secret = (req) => {
+ const token = req.get("X-VAULT-TOKEN") || "";
+
+ if (!token.match(regexes.uuid)) {
+ res.status(400).json({
+ status: "error",
+ error: "missing secret key",
+ });
+ warn(req, "blocked an attempt to bypass authentication (missing secret)");
+ req.app.locals.cooldown(req, 3e5);
+ throw new BypassAttempt("missing secret");
+ }
+
+ return token;
+};
+
+/**
+ * get the session without authenticating
+ * @param {Request} req
+ * @param {Response} res
+ * @returns {[string, Session]} the session token and session
+ */
+const get_raw_session = (req, res) => {
+ const token = String(req.get("X-VAULT-SESSION") || "");
+
+ if (!token.match(regexes.uuid)) {
+ res.status(400).json({
+ status: "error",
+ error: "missing session key",
+ });
+ warn(req, "blocked an attempt to bypass authentication (missing key)");
+ req.app.locals.cooldown(req, 3e5);
+ throw new BypassAttempt("missing session key");
+ }
+
+ return [token, req.app.locals.session.get(token) || null];
+};
+
+/**
+ * check authentication and get the session
+ * @param {Request} req
+ * @param {Response} res
+ * @returns {[string, Session]} the session token and session
+ */
+const get_session = (req, res) => {
+ const [token, session] = get_raw_session(req, res);
+
+ if (session === null) {
+ res.status(410).json({
+ status: "error",
+ error: "session expired",
+ });
+ req.app.locals.cooldown(req, 5e3); // XXX: maybe a lower cooldown here
+ throw new BypassAttempt("session not found");
+ }
+
+ if (get_secret(req) !== session.secret) {
+ res.status(410).json({
+ status: "error",
+ error: "session expired", // yes, we lie to them
+ });
+
+ // max 3 attempts per 15 minutes
+ if (req.app.locals.brute.consume(req, 3, 9e5)) {
+ req.app.locals.cooldown(req, 5e3);
+ } else {
+ warn(req, "blocked an attempt to bypass authentication (wrong secret)");
+ req.app.locals.cooldown(req, 3.6e6);
+ }
+
+ throw new BypassAttempt("wrong secret");
+ }
+
+ if (!session.authenticated) {
+ // this should not be possible because they cannot know the secret
+ // before authenticating, but we check just to be safe
+
+ res.status(401).json({
+ status: "error",
+ error: "not authenticated",
+ });
+ warn(req, "blocked an attempt to bypass authentication (not authed)");
+ req.app.locals.cooldown(req, 3e5);
+ throw new BypassAttempt("session not authenticated");
+ }
+
+ if (!check_ip(req, session)) {
+ // ip address has changed
+ res.status(403).json({
+ status: "error",
+ error: "ip address mismatch",
+ });
+ req.app.locals.logger.warn(`${get_endpoint(req)}: ip address mismatch <${session.vault}@vault> [${req.ip}]`);
+ req.app.locals.cooldown(req, 3e5);
+ throw new ValidationError("ip mismatch");
+ }
+
+ return [token, session];
+};
+
+const get_email = (req) => {
+ const email = get_prop(req, "email");
+
+ if (!email.match(regexes.email) || email.length >= 320) {
+ res.status(400).json({
+ status: "error",
+ error: "invalid email address",
+ });
+ warn(req, "blocked an attempt to bypass authentication (invalid email)");
+ req.app.locals.cooldown(req, 3e5);
+ throw new BypassAttempt("invalid email format");
+ }
+
+ return email;
+};
+
+
+module.exports = {
+ regexes,
+ check_ip,
+ get_prop,
+ get_email,
+ get_endpoint,
+ get_raw_session,
+ get_session,
+};