diff --git a/src/controllers/api/account/loginController.js b/src/controllers/api/account/loginController.js index e7c5345..910ff5e 100644 --- a/src/controllers/api/account/loginController.js +++ b/src/controllers/api/account/loginController.js @@ -62,10 +62,13 @@ module.exports.post = async function(req, res){ //Check config for protocol const secure = config.protocol.toLowerCase() == "https"; + //Create expiration date for cookies (180 days) + const expires = new Date(Date.now() + (1000 * 60 * 60 * 24 * 180)) + //Set remember me ID and token as browser-side cookies for safe-keeping - res.cookie("rememberme.id", authToken.id, {sameSite: 'strict', httpOnly: true, secure}); + res.cookie("rememberme.id", authToken.id, {sameSite: 'strict', httpOnly: true, secure, expires}); //This should be the servers last interaction with the plaintext token before saving the hashed copy, and dropping it out of RAM - res.cookie("rememberme.token", authToken.token, {sameSite: 'strict', httpOnly: true, secure}); + res.cookie("rememberme.token", authToken.token, {sameSite: 'strict', httpOnly: true, secure, expires}); } //Tell the browser everything is dandy diff --git a/src/schemas/user/rememberMeSchema.js b/src/schemas/user/rememberMeSchema.js index 3ac78ef..74fc11d 100644 --- a/src/schemas/user/rememberMeSchema.js +++ b/src/schemas/user/rememberMeSchema.js @@ -81,6 +81,12 @@ rememberMeToken.pre('save', async function (next){ next(); }); +//Methods +rememberMeToken.methods.checkToken = async function(token){ + //Compare ingested token to saved hash + return await hashUtil.compareRememberMeToken(token, this.token); +} + //statics rememberMeToken.statics.genToken = async function(user, pass){ //Authenticate user and pull document @@ -104,4 +110,45 @@ rememberMeToken.statics.genToken = async function(user, pass){ } } +/** + * Authenticates an id and token pair + * @param {String} id - id of token auth against + * @param {String} token - token string to auth against + * @param {String} failLine - Line to paste into custom error upon login failure + * @returns {Mongoose.Document} - User DB Document upon success + */ +rememberMeToken.statics.authenticate = async function(id, token, failLine = "Bad Username or Password."){ + //check for missing pass + if(!id || !token){ + throw loggerUtils.exceptionSmith("Missing id/token.", "validation"); + } + + //get the token if it exists + const tokenDB = await this.findOne({id}); + + //if not scream and shout + if(!tokenDB){ + badLogin(); + } + + //Check our password is correct + if(await tokenDB.checkToken(token)){ + //Populate the user field + await tokenDB.populate('user'); + + //Return the user doc + return tokenDB.user; + }else{ + //Nuke the token for security + await tokenDB.deleteOne(); + //if not scream and shout + badLogin(); + } + + //standardize bad login response so it's unknown which is bad for security reasons. + function badLogin(){ + throw loggerUtils.exceptionSmith(failLine, "unauthorized"); + } +} + module.exports = mongoose.model("rememberMe", rememberMeToken); \ No newline at end of file diff --git a/src/server.js b/src/server.js index 87472a2..3f47c43 100644 --- a/src/server.js +++ b/src/server.js @@ -39,6 +39,7 @@ const pmHandler = require('./app/pm/pmHandler'); const configCheck = require('./utils/configCheck'); const scheduler = require('./utils/scheduler'); const {errorMiddleware} = require('./utils/loggerUtils'); +const sessionUtils = require('./utils/sessionUtils'); //Validator const accountValidator = require('./validators/accountValidator'); //DB Model @@ -143,6 +144,14 @@ mongoose.set("sanitizeFilter", true).connect(dbUrl).then(() => { process.exit(); }); +//Static File Server, set this up first to avoid middleware running on top of it +//Serve client-side libraries +app.use('/lib/bootstrap-icons',express.static(path.join(__dirname, '../node_modules/bootstrap-icons'))); //Icon set +app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altcha/dist_external'))); //Self-Hosted PoW-based Captcha +app.use('/lib/hls.js',express.static(path.join(__dirname, '../node_modules/hls.js/dist'))); //HLS Media Handler +//Server public 'www' folder +app.use(express.static(path.join(__dirname, '../www'))); + //Set View Engine app.set('view engine', 'ejs'); app.set('views', __dirname + '/views'); @@ -164,6 +173,9 @@ io.engine.use(sessionMiddleware); app.use(accountValidator.rememberMeID()); app.use(accountValidator.rememberMeToken()); +//Use remember me middleware +app.use(sessionUtils.rememberMeMiddleware); + //Routes //Humie-Friendly app.use('/', indexRouter); @@ -183,14 +195,6 @@ app.use('/tooltip', tooltipRouter); //Bot-Ready app.use('/api', apiRouter); -//Static File Server -//Serve client-side libraries -app.use('/lib/bootstrap-icons',express.static(path.join(__dirname, '../node_modules/bootstrap-icons'))); //Icon set -app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altcha/dist_external'))); //Self-Hosted PoW-based Captcha -app.use('/lib/hls.js',express.static(path.join(__dirname, '../node_modules/hls.js/dist'))); //HLS Media Handler -//Server public 'www' folder -app.use(express.static(path.join(__dirname, '../www'))); - //Handle error checking app.use(errorMiddleware); diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 970718b..fd166a9 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -14,6 +14,9 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ +//npm imports +const {validationResult, matchedData} = require('express-validator'); + //Local Imports const config = require('../../config.json'); const {userModel} = require('../schemas/user/userSchema.js'); @@ -101,7 +104,7 @@ module.exports.authenticateSession = async function(identifier, secret, req, use //If we're using remember me tokens if(useRememberMeToken){ - + userDB = await rememberMeModel.authenticate(identifier, secret); //Otherwise }else{ //Fallback on to username/password authentication @@ -211,5 +214,44 @@ module.exports.processExpiredAttempts = function(){ } } +module.exports.rememberMeMiddleware = function(req, res, next){ + //if we have an un-authenticated user + if(req.session.user == null || req.session.user == ""){ + //Check validation result + const validResult = validationResult(req); + + //if we don't have errors + if(validResult.isEmpty()){ + //Pull verified data from request + const data = matchedData(req); + + //If we have a valid remember me id and token + if(data.rememberme != null && data.rememberme.id != null && data.rememberme.token != null){ + //Authenticate against standard auth function in remember me mode + module.exports.authenticateSession(data.rememberme.id, data.rememberme.token, req, true).then((userDB)=>{ + //Jump to next middleware + next(); + }).catch((err)=>{ + //Clear out remember me fields + res.clearCookie('rememberme.id'); + res.clearCookie('rememberme.token'); + + //Bitch, Moan, and guess what? That's fuckin' right! COMPLAIN!!!! + return loggerUtils.exceptionHandler(res, err); + }); + }else{ + //Jump to next middleware, this looks gross but it's only because they made me use .then like a bunch of fucking dicks + next(); + } + }else{ + //Jump to next middleware + next(); + } + }else{ + //Jump to next middleware + next(); + } +} + module.exports.throttleAttempts = throttleAttempts; module.exports.maxAttempts = maxAttempts; \ No newline at end of file