From 8a1302edf0506c92bd9a8e83ed7c1e145b246513 Mon Sep 17 00:00:00 2001 From: gumi Date: Sun, 15 Mar 2020 21:56:57 -0400 Subject: add an Identity type --- src/routers/vault/index.js | 5 +- src/routers/vault/middlewares/account.js | 32 +++++-------- src/routers/vault/middlewares/identity.js | 74 +++++++++++++++-------------- src/routers/vault/middlewares/session.js | 46 ++++++++++-------- src/routers/vault/types/Identity.js | 58 +++++++++++++++++++++++ src/routers/vault/types/Session.js | 78 +++++++++++++++++++++++++------ src/routers/vault/types/SessionStore.js | 4 +- 7 files changed, 207 insertions(+), 90 deletions(-) create mode 100644 src/routers/vault/types/Identity.js diff --git a/src/routers/vault/index.js b/src/routers/vault/index.js index 831ec93..c0bc788 100644 --- a/src/routers/vault/index.js +++ b/src/routers/vault/index.js @@ -6,7 +6,7 @@ const SessionStore = require("./types/SessionStore.js"); const models = { vault: [ "login", "login_log", - "identity", "identity_log", + /*"identity",*/ "identity_log", "claimed_game_accounts", "claimed_legacy_accounts", "account_log", @@ -106,6 +106,9 @@ module.exports = exports = class Vault { console.info(`Vault: loaded models for ${DB}`); } + const Identity = require("./types/Identity.js"); + this.api.locals.vault.identity = Identity.define(this.sequelize.vault); + await this.sequelize.vault.sync({alter: {drop: false}}); // update SQL tables this.api.locals.sequelize = this.sequelize; // for access to sequelize.fn diff --git a/src/routers/vault/middlewares/account.js b/src/routers/vault/middlewares/account.js index 3c1cf52..5a5fa85 100644 --- a/src/routers/vault/middlewares/account.js +++ b/src/routers/vault/middlewares/account.js @@ -1,11 +1,9 @@ "use strict"; const validate = require("../utils/validate.js"); - -const regexes = { - token: /^[a-zA-Z0-9-_]{6,128}$/, // UUID -}; +const Session = require("../types/Session.js"); const get_data = async (req, res, next) => { + /** @type {Session} */ let session; try { @@ -14,14 +12,7 @@ const get_data = async (req, res, next) => { res.status(200).json({ status: "success", - data: { - // TODO: make this a method of Session - primaryIdentity: session.primaryIdentity, - allowNonPrimary: session.allowNonPrimary, - strictIPCheck: session.strictIPCheck, - requireSecret: true, - vaultId: session.vault, - }, + data: session.getAccountData(), }); req.app.locals.cooldown(req, 1e3); }; @@ -35,19 +26,20 @@ const update_account = async (req, res, next) => { const data = { primary: +validate.get_prop(req, "primary"), - allow: !!validate.get_prop(req, "allow"), - strict: !!validate.get_prop(req, "strict"), + allow: validate.get_prop(req, "allow") === "true", + strict: validate.get_prop(req, "strict") === "true", }; const update_fields = {}; - if (session.primaryIdentity !== data.primary) { + if (session.primaryIdentity.id !== data.primary) { // update primary identity let new_primary = null; for (const ident of session.identities) { if (ident.id === data.primary) { new_primary = ident.id; + session.primaryIdentity = ident; break; } } @@ -81,13 +73,13 @@ const update_account = async (req, res, next) => { // now update our cache session.allowNonPrimary = data.allow; session.strictIPCheck = data.strict; - session.primaryIdentity = data.primary; for (const ident of session.identities) { - if (ident.id === session.primaryIdentity) { - ident.primary = true; - } else if (ident.primary === true) { - ident.primary = false; + if (ident.id === session.primaryIdentity.id) { + ident.isPrimary = true; + session.primaryIdentity = ident; + } else if (ident.isPrimary === true) { + ident.isPrimary = false; } } diff --git a/src/routers/vault/middlewares/identity.js b/src/routers/vault/middlewares/identity.js index f65d757..3cae9e5 100644 --- a/src/routers/vault/middlewares/identity.js +++ b/src/routers/vault/middlewares/identity.js @@ -3,6 +3,7 @@ const uuidv4 = require("uuid/v4"); const nodemailer = require("nodemailer"); const Claim = require("../utils/claim.js"); const validate = require("../utils/validate.js"); +const Identity = require("../types/Identity.js"); let transporter = nodemailer.createTransport({ sendmail: true, @@ -19,18 +20,14 @@ const get_identities = async (req, res, next) => { if (session.identities.length === 0) { console.info(`Vault.identity: fetching identities <${session.vault}@vault> [${req.ip}]`); + /** @type {Identity[]} */ const rows = await req.app.locals.vault.identity.findAll({ where: {userId: session.vault} }); - for (const row of rows) { - session.identities.push({ - // TODO: make this a class! - id: row.id, - email: row.email, - added: row.addedDate, - primary: session.primaryIdentity === row.id, - }); + for (const ident of rows) { + ident.isPrimary = session.primaryIdentity.id === ident.id; + session.identities.push(ident); } } @@ -55,6 +52,7 @@ const add_identity = async (req, res, next) => { return; } + // TODO: make an IdentityStore type similar to SessionStore and get rid of Ephemeral const ident = req.app.locals.identity_pending.get(secret); if (ident === null || ident === undefined) { @@ -73,6 +71,7 @@ const add_identity = async (req, res, next) => { return; } + /** @type {Identity} */ const newIdent = await req.app.locals.vault.identity.create({ userId: ident.vault, email: ident.email, @@ -87,16 +86,11 @@ const add_identity = async (req, res, next) => { await Claim.claim_accounts(req, ident.email, ident.vault); + /** @type {Session} */ let session = null; - for (const [key, sess] of req.app.locals.session) { + for (const [, sess] of req.app.locals.session) { if (sess.vault === ident.vault && sess.authenticated) { - sess.identities.push({ - // TODO: make this a class! - id: newIdent.id, - email: newIdent.email, - added: newIdent.addedDate, - primary: false, - }); + sess.identities.push(newIdent); session = sess; break; } @@ -119,18 +113,14 @@ const add_identity = async (req, res, next) => { // request to add - let session; + let session, email; try { [, session] = validate.get_session(req, res); - } catch { return } // already handled - - let email; - try { email = validate.get_email(req, res); } catch { return } // already handled - for (const [key, pending] of req.app.locals.identity_pending) { + for (const [, pending] of req.app.locals.identity_pending) { if (pending.vault === session.vault && pending.email === email) { res.status(425).json({ status: "error", @@ -141,24 +131,15 @@ const add_identity = async (req, res, next) => { } } - const find = await req.app.locals.vault.identity.findOne({ - where: {email} - }); - - if (find !== null) { + if (session.identities.length === 0) { + // we did not have enough time to fetch, so cowardly refuse res.status(409).json({ status: "error", error: "already assigned", }); req.app.locals.cooldown(req, 5e3); return; - } - - const count = await req.app.locals.vault.identity.count({ - where: {userId: session.vault} - }); - - if (count >= 20) { + } else if (session.identities.length >= 20) { res.status(416).json({ status: "error", error: "too many identities", @@ -167,6 +148,31 @@ const add_identity = async (req, res, next) => { return; } + /** @type {Identity} */ + let find = null; + + for (const ident of session.identities) { + if (ident.email === email) { + find = ident; + break; + } + } + + if (find === null) { + find = await req.app.locals.vault.identity.findOne({ + where: {email} + }); + } + + if (find !== null) { + res.status(409).json({ + status: "error", + error: "already assigned", + }); + req.app.locals.cooldown(req, 5e3); + return; + } + let uuid; do { // avoid collisions uuid = uuidv4(); diff --git a/src/routers/vault/middlewares/session.js b/src/routers/vault/middlewares/session.js index a961840..c9b7e13 100644 --- a/src/routers/vault/middlewares/session.js +++ b/src/routers/vault/middlewares/session.js @@ -5,6 +5,7 @@ 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"); +const Identity = require("../types/Identity.js"); let transporter = nodemailer.createTransport({ sendmail: true, @@ -131,6 +132,7 @@ const auth_session = async (req, res) => { ip: req.app.locals.sequelize.vault.fn("INET6_ATON", req.ip), }); + /** @type {Identity} */ const ident = await req.app.locals.vault.identity.create({ userId: user.id, email: session.email, @@ -151,16 +153,11 @@ const auth_session = async (req, res) => { // update current session session.vault = user.id; - session.identity = ident.id; - session.primaryIdentity = ident.id; + session.identity = ident; + session.primaryIdentity = ident; session.allowNonPrimary = user.allowNonPrimary; session.strictIPCheck = user.strictIPCheck; - session.identities = [{ - // TODO: make this a class! - email: ident.email, - added: ident.addedDate, - primary: true, - }]; + session.identities.push(ident); } else { if (session.identity !== session.primaryIdentity && !session.allowNonPrimary) { // unexpected: a session was created when it shouldn't have been @@ -202,13 +199,11 @@ const auth_session = async (req, res) => { if (session.identity !== session.primaryIdentity) { // user did not log in with their primary identity - const primary = await req.app.locals.vault.identity.findByPk(session.primaryIdentity); - - if (primary === null || primary === undefined) { + if (session.primaryIdentity === null || session.primaryIdentity === undefined) { // the vault account has no primary identity (bug): let's fix this console.warn(`Vault.session: fixing account with a deleted primary identity <${session.vault}@vault> [${req.ip}]`); await req.app.locals.vault.login.update({ - primaryIdentity: session.identity, + primaryIdentity: session.identity.id, }, {where: { id: session.vault, }}); @@ -216,7 +211,7 @@ const auth_session = async (req, res) => { } else { transporter.sendMail({ from: process.env.VAULT__MAILER__FROM, - to: primary.email, + to: session.primaryIdentity.email, subject: "The Mana World security notice", text: "Someone has logged in to your Vault account using an email address that " + "is not your primary address. If this wasn't you, please contact us immediately.\n\n" + @@ -236,7 +231,7 @@ const auth_session = async (req, res) => { key: new_uuid, secret: session.secret, // give them the session secret (only shared once) expires: session.expires, - identity: session.identity, + identity: session.identity.id, }, }); }; @@ -247,6 +242,7 @@ const new_session = async (req, res, next) => { email = validate.get_email(req, res); } catch { return } // already handled + /** @type {Identity} */ const identity = await req.app.locals.vault.identity.findOne({where: {email: email}}); if (identity === null) { @@ -305,7 +301,7 @@ const new_session = async (req, res, next) => { return; } } else { - const account = await req.app.locals.vault.login.findOne({where: {id: identity.userId}}); + const account = await req.app.locals.vault.login.findByPk(identity.userId); if (account === null) { // unexpected: the account was deleted but not its identities console.log(`Vault.session: removing dangling identity [${req.ip}]`); @@ -317,13 +313,25 @@ const new_session = async (req, res, next) => { req.app.locals.cooldown(req, 3e5); return; } else { + /** @type {Identity} */ + let primary = null; + + if (identity.id !== account.primaryIdentity) { + try { + primary = await req.app.locals.vault.identity.findByPk(account.primaryIdentity); + } catch {} + } else { + primary = identity; + } + // auth flow - if (account.primaryIdentity === null || account.primaryIdentity === undefined) { + if (primary === null) { // the vault account has no primary identity (bug): let's fix this console.warn(`Vault.session: fixing account with no primary identity <${account.id}@vault> [${req.ip}]`); account.primaryIdentity = identity.id; + primary = identity; await account.save(); - } else if (identity.id !== account.primaryIdentity && !account.allowNonPrimary) { + } else if (identity.id !== primary.id && !account.allowNonPrimary) { res.status(423).json({ status: "error", error: "non-primary login is disabled", @@ -341,10 +349,10 @@ const new_session = async (req, res, next) => { const session = new Session(req.ip, email); session.vault = account.id; - session.primaryIdentity = account.primaryIdentity; + session.primaryIdentity = primary; session.allowNonPrimary = account.allowNonPrimary; session.strictIPCheck = account.strictIPCheck; - session.identity = identity.id; + session.identity = identity; req.app.locals.session.set(uuid, session); console.log(`Vault.session: starting authentication with identity ${identity.id} [${req.ip}]`); diff --git a/src/routers/vault/types/Identity.js b/src/routers/vault/types/Identity.js new file mode 100644 index 0000000..fb5171f --- /dev/null +++ b/src/routers/vault/types/Identity.js @@ -0,0 +1,58 @@ +"use strict"; + +const { Sequelize, Model } = require("sequelize"); + +class Identity extends Model { + /** + * primary key + * @type {number} + */ + //id; + /** + * the Date when the Identity was confirmed + * @type {Date} + */ + //addedDate; + /** + * the email address of the identity + * @type {string} + */ + //email; + /** + * the Vault user id + * @type {number} + */ + //userId; + + /** + * whether it is the primary identity of the vault account + * @virtual + */ + isPrimary = false; + + + /** + * initialize the model (must be called prior to first use) + * @param {Sequelize} sequelize - the Sequelize instance + */ + static define (sequelize) { + const {fields, options} = require("../models/vault/identity.js"); + Identity.init(fields, { sequelize, ...options }); + + return Identity; // the instantiated Model + } + + /** + * serialize for sending over the network + */ + toJSON () { + return { + id: this.id, + email: this.email, + added: this.addedDate, + primary: this.isPrimary, + }; + } +} + +module.exports = Identity; diff --git a/src/routers/vault/types/Session.js b/src/routers/vault/types/Session.js index 17c77ef..59737b3 100644 --- a/src/routers/vault/types/Session.js +++ b/src/routers/vault/types/Session.js @@ -1,34 +1,72 @@ const uuidv4 = require("uuid/v4"); +const Identity = require("./Identity.js"); +const EvolAccount = require("./EvolAccount.js"); +const LegacyAccount = require("./LegacyAccount.js"); /** * holds a cache of all the user data fetched from SQL */ module.exports = class Session { - /** expiry Date */ + /** + * expiry date + */ expires = new Date(); - /** Vault account id */ + /** + * Vault account id + * @type {number} + */ vault = null; - /** whether the user logged in */ + /** + * whether the user is properly authenticated + */ authenticated = false; - /** the identity that was used to log in */ + /** + * the identity that was used to log in + * @type {Identity} + */ identity = null; - /** the email address of the identity that was used to log in */ + /** + * the email address of the identity that was used to log in + * @type {string} + */ email; - /** the secret that is sent after authentication */ + /** + * the secret that is sent once to the client after authentication + * @type {string} + */ secret; - /** cache holding all identities */ + /** + * cache holding all identities + * @type {Identity[]} + */ identities = []; - /** the main identity of the account */ + /** + * id of the main identity of the account + * @type {number} + */ primaryIdentity = null; - /** whether to allow logging in with a non-primary ident */ + /** + * whether to allow logging in with a non-primary ident + */ allowNonPrimary = true; - /** LegacyAccount[] cache holding all legacy game accounts */ + /** + * cache holding all legacy game accounts + * @type {LegacyAccount[]} + */ legacyAccounts = []; - /** EvolAccount[] cache holding all evol game accounts */ + /** + * cache holding all evol game accounts + * @type {EvolAccount[]} + */ gameAccounts = []; - /** ip that was used to init the session */ + /** + * ip that was used to init the session + * @type {string} + */ ip; - /** refuse to authenticate a session with a different IP */ + /** + * refuse to authenticate a session with a different IP + */ strictIPCheck = true; constructor (ip, email) { @@ -44,7 +82,19 @@ module.exports = class Session { toJSON (key) { return { expires: this.expires, - identity: this.identity, + identity: this.identity.id, } } + + /** + * serialize the account settings for sending over the network + */ + getAccountData () { + return { + primaryIdentity: this.primaryIdentity.id, + allowNonPrimary: this.allowNonPrimary, + strictIPCheck: this.strictIPCheck, + vaultId: this.vault, + }; + } } diff --git a/src/routers/vault/types/SessionStore.js b/src/routers/vault/types/SessionStore.js index daa71c3..2657551 100644 --- a/src/routers/vault/types/SessionStore.js +++ b/src/routers/vault/types/SessionStore.js @@ -3,7 +3,7 @@ const Session = require("./Session.js"); /** * we store the timeout directly in Session instances - * @type Symbol("session timeout") + * @type {Symbol("session timeout")} */ const timeout_symbol = Symbol("session timeout"); @@ -13,7 +13,7 @@ const timeout_symbol = Symbol("session timeout"); class SessionStore { /** * a Map of all Session instances - * @type Map + * @type {Map} */ sessions = new Map(); /** lifetime of an unauthenticated Session, in minutes */ -- cgit v1.2.3-60-g2f50