const fs = require("fs-extra"); const dayjs = require("dayjs"); //const path=require('path'); const glob = require("glob"); // To debug it could be easier with source code: // const openpgp = require("/media/phil/usbfarm/apxtri/node_modules/openpgp/dist/node/openpgp.js"); const openpgp = require("openpgp"); const l=require('../tools/log.js'); l.showlog= false; // force log as well in prod and dev l.context="isAuthenticated"; /** * @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 * * apxtri 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) => { /* console.log(__dirname) console.log(path.resolve('../tmp/tokens')) if (fs.existsSync('../tmp/tokens')) console.log('pass A') if (fs.existsSync('../tmp/tokens')) console.log('pass B') */ const currentday = dayjs().date(); fs.ensureDirSync(`../tmp/tokens`); let menagedone = fs.existsSync( `../tmp/tokens/menagedone${currentday}` ); l.og(`menagedone${currentday} was it done today?:${menagedone}`); if (!menagedone) { // clean oldest const tsday = dayjs().valueOf(); // now in timestamp format glob.sync(`../tmp/tokens/menagedone*`).forEach((f) => { fs.removeSync(f); }); glob.sync(`../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); } }); //clean tmp glob.sync(`../tmp/*.txt`).forEach((f) => { fs.remove(f); }); fs.outputFile( `../tmp/tokens/menagedone${currentday}`, "done by middleware/isAUthenticated" ); } //Check register in tmp/tokens/ l.og("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" ) { l.og("alias anonymous means not auth"); resnotauth.status = 401; return res.status(resnotauth.status).json(resnotauth); } let tmpfs = `../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 = `../tmp/tokens/${alias}.json`; if (action == "clean") { //to reinit bruteforce checker l.og("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); l.og("penalty:", stamp); await sleep(stamp.numberfail * 100); //increase of 0,1 second the answer time per fail l.og("time out penalty"); } }; if (!fs.existsSync(tmpfs)) { // need to check detached sign let publickey = ""; l.og(process.cwd()); l.og(process.env.PWD); l.og(__dirname); const aliasinfo = `../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 == "") { l.og("alias unknown"); resnotauth.status = 404; resnotauth.data.xaliasexists = false; return res.status(resnotauth.status).send(resnotauth); } l.og("publickey", publickey); if (publickey.substring(0, 31) !== "-----BEGIN PGP PUBLIC KEY BLOCK") { l.og("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") { l.og("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); } l.og("clearmsg", clearmsg); let signedMessage="" const pubkey = await openpgp.readKey({ armoredKey: publickey }); try{ signedMessage = await openpgp.readCleartextMessage({ cleartextMessage: clearmsg, }); }catch(err){ return res.status(422).send({status:422,ref:"Middleware",msg:"unconsistentcleartextmessage",data:{xhash:req.session.header.xhash,clearmsg}}) } const verificationResult = await openpgp.verify({ message: signedMessage, verificationKeys: pubkey, }); l.og(verificationResult); l.og(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"; l.og( `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"; l.og("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`; l.og("Profils tribe/app management"); l.og("person", person); if (fs.existsSync(person)) { const infoperson = fs.readJSONSync(person); l.og(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"); l.og(`${req.session.header.xalias} Authenticated`); next(); }; module.exports = isAuthenticated;