const fs = require("fs-extra"); const dayjs = require("dayjs"); const glob = require("glob"); // To debug it could be easier with source code: // const openpgp = require("/media/phil/usbfarm/apxtrib/node_modules/openpgp/dist/node/openpgp.js"); const openpgp = require("openpgp"); /** * @api {get} http://header/istauthenticated - isAuthenticated * @apiGroup Middlewares * @apiName isAuthenticated * @apiDescription - valid if exist xalias_xdays_xhash.substr(20,200) in town/tmp/tokens/ * - if not, * - valid if xhash signature sign xalias_xdays with alias's publickey. * - if not valid => not allowed * - If valid => * - store a xalias_xdays_xhash.substr (20,200) into /tmp/tokens with xprofils array from person. * - update header.xprofils from this token * * apXtrib profils are anonymous, pagans, mayor (on a node server), druid (on a tribe like smatchit). * * pagan identity is independant of domain (tribe), by default profils are :['anonymous','pagans']. if this alias exist in a tribe domain as a person then his profils come from /tribes/{tribeId}/objects/person/itm/{alias}.json profils:['anonymous','pagans','person','seeker'] any profils allowed to act on tribe objects. * * Each profil have CRUD accessright on object managed in schema in apxaccessrights:{owner,profil:{"C":[],"R":[properties],"U":[properties],"D":[]}}, see Odmdb for details. * * A process run once each day to clean up all xhash tmp/tokens oldest than 24 hours. * **/ const isAuthenticated = async (req, res, next) => { const withlog = false; const currentday = dayjs().date(); fs.ensureDirSync(`${process.env.dirtown}/tmp/tokens`); let menagedone = fs.existsSync( `${process.env.dirtown}/tmp/tokens/menagedone${currentday}` ); if (withlog) console.log(`menagedone${currentday} was it done today?:${menagedone}`); if (!menagedone) { // clean oldest const tsday = dayjs().valueOf(); // now in timestamp format glob.sync(`${process.env.dirtown}/tmp/tokens/menagedone*`).forEach((f) => { fs.removeSync(f); }); glob.sync(`${process.env.dirtown}/tmp/tokens/*.json`).forEach((f) => { const fsplit = f.split("_"); const elapse = tsday - parseInt(fsplit[2]); //24h 86400000 milliseconde 15mn 900000 if (elapse && elapse > 86400000) { fs.remove(f); } }); fs.outputFile( `${process.env.dirtown}/tmp/tokens/menagedone${currentday}`, "done by middleware/isAUthenticated" ); } //Check register in tmp/tokens/ if (withlog) console.log("isAuthenticate?", req.session.header, req.body); const resnotauth = { ref: "middlewares", msg: "notauthenticated", data: { xalias: req.session.header.xalias, xaliasexists: true, }, }; if ( req.session.header.xalias == "anonymous" || req.session.header.xhash == "anonymous" ) { if (withlog) console.log("alias anonymous means not auth"); resnotauth.status = 401; return res.status(resnotauth.status).json(resnotauth); } let tmpfs = `${process.env.dirtown}/tmp/tokens/${req.session.header.xalias}_${req.session.header.xtribe}_${req.session.header.xdays}`; //max filename in ext4: 255 characters tmpfs += `_${req.session.header.xhash.substring( 150, 150 + tmpfs.length - 249 )}.json`; const bruteforcepenalty = async (alias, action) => { const sleep = (ms) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; const failstamp = `${process.env.dirtown}/tmp/tokens/${alias}.json`; if (action == "clean") { //to reinit bruteforce checker if (withlog) console.log("try to clean penalty file ", failstamp); fs.remove(failstamp, (err) => { if (err) console.log("Check forcebrut ", err); }); } else if (action == "penalty") { const stamp = fs.existsSync(failstamp) ? fs.readJSONSync(failstamp) : { lastfail: dayjs().format(), numberfail: 0 }; stamp.lastfail = dayjs().format(); stamp.numberfail += 1; fs.outputJSON(failstamp, stamp); if (withlog) console.log("penalty:", stamp); await sleep(stamp.numberfail * 100); //increase of 0,1 second the answer time per fail if (withlog) console.log("time out penalty"); } }; if (!fs.existsSync(tmpfs)) { // need to check detached sign let publickey = ""; console.log(process.cwd()); console.log(process.env.PWD); console.log(__dirname); const aliasinfo = `${process.env.PWD}/nationchains/pagans/itm/${req.session.header.xalias}.json`; if (fs.existsSync(aliasinfo)) { publickey = fs.readJsonSync(aliasinfo).publickey; } else if (req.body.publickey) { resnotauth.data.xaliasexists = false; publickey = req.body.publickey; } if (publickey == "") { if (withlog) console.log("alias unknown"); resnotauth.status = 404; resnotauth.data.xaliasexists = false; return res.status(resnotauth.status).send(resnotauth); } if (withlog) console.log("publickey", publickey); if (publickey.substring(0, 31) !== "-----BEGIN PGP PUBLIC KEY BLOCK") { if (withlog) console.log("Publickey is not valid as armored key:", publickey); await bruteforcepenalty(req.session.header.xalias, "penalty"); resnotauth.status = 404; return res.status(resnotauth.status).send(resnotauth); } const clearmsg = Buffer.from(req.session.header.xhash, "base64").toString(); if (clearmsg.substring(0, 10) !== "-----BEGIN") { if (withlog) console.log("xhash conv is not valid as armored key:", clearmsg); await bruteforcepenalty(req.session.header.xalias, "penalty"); resnotauth.status = 404; return res.status(resnotauth.status).send(resnotauth); } if (withlog) console.log("clearmsg", clearmsg); const pubkey = await openpgp.readKey({ armoredKey: publickey }); const signedMessage = await openpgp.readCleartextMessage({ cleartextMessage: clearmsg, }); const verificationResult = await openpgp.verify({ message: signedMessage, verificationKeys: pubkey, }); if (withlog) console.log(verificationResult); if (withlog) console.log(verificationResult.signatures[0].keyID.toHex()); try { await verificationResult.signatures[0].verified; if ( verificationResult.data != `${req.session.header.xalias}_${req.session.header.xdays}` ) { resnotauth.msg = "signaturefailled"; if (withlog) console.log( `message recu:${verificationResult.data} , message attendu:${req.session.header.xalias}_${req.session.header.xdays}` ); await bruteforcepenalty(req.session.header.xalias, "penalty"); resnotauth.status = 401; return res.status(resnotauth.status).send(resnotauth); } } catch (e) { resnotauth.msg = "signaturefailled"; if (withlog) console.log("erreur", e); await bruteforcepenalty(req.session.header.xalias, "penalty"); resnotauth.status = 401; return res.status(resnotauth.status).send(resnotauth); } // authenticated then get person profils (person = pagan for a xtrib) const person = `${process.env.dirtown}/tribes/${req.session.header.xtribe}/objects/persons/itm/${req.session.header.xalias}.json`; if (withlog) { console.log("Profils tribe/app management"); console.log("person", person); } if (fs.existsSync(person)) { const infoperson = fs.readJSONSync(person); console.log(infoperson); infoperson.profils.forEach((p) => { if (!req.session.header.xprofils.includes(p)) req.session.header.xprofils.push(p); }) }else{ if (!req.session.header.xprofils.includes('pagans')) req.session.header.xprofils.push("pagans"); } fs.outputJSONSync(tmpfs, req.session.header.xprofils); } else { //tmpfs exist get profils from identification process req.session.header.xprofils = fs.readJSONSync(tmpfs); } bruteforcepenalty(req.session.header.xalias, "clean"); console.log(`${req.session.header.xalias} Authenticated`); next(); }; module.exports = isAuthenticated;