From e00e5a608be09f4955340ad6e6b67b9d31d3a5fa Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 20 Oct 2025 07:49:41 -0400 Subject: [PATCH] Continued work on remember me tokens. --- package.json | 1 + .../api/account/loginController.js | 48 ++++++++++++++---- src/schemas/user/rememberMeSchema.js | 16 +++--- src/server.js | 17 ++++++- src/utils/sessionUtils.js | 1 + src/validators/accountValidator.js | 50 +++++++++++++------ src/views/login.ejs | 1 + src/views/partial/navbar.ejs | 2 + www/js/login.js | 6 ++- www/js/navbar.js | 3 +- www/js/utils.js | 4 +- 11 files changed, 113 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 86045a3..0076162 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "bcrypt": "^5.1.1", "bootstrap-icons": "^1.11.3", "connect-mongo": "^5.1.0", + "cookie-parser": "^1.4.7", "csrf-sync": "^4.0.3", "ejs": "^3.1.10", "express": "^4.18.2", diff --git a/src/controllers/api/account/loginController.js b/src/controllers/api/account/loginController.js index f5f2130..e7c5345 100644 --- a/src/controllers/api/account/loginController.js +++ b/src/controllers/api/account/loginController.js @@ -22,6 +22,7 @@ const {validationResult, matchedData} = require('express-validator'); //local imports const migrationModel = require('../../../schemas/user/migrationSchema.js'); +const rememberMeModel = require('../../../schemas/user/rememberMeSchema.js'); const sessionUtils = require('../../../utils/sessionUtils'); const hashUtils = require('../../../utils/hashUtils.js'); const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); @@ -35,10 +36,39 @@ module.exports.post = async function(req, res){ //if we don't have errors if(validResult.isEmpty()){ //Pull sanatzied/validated data - const {user, pass} = matchedData(req); - - //try to authenticate the session, and return a successful code if it works - await sessionUtils.authenticateSession(user, pass, req); + const data = matchedData(req); + + //try to authenticate the session, throwing an error and breaking the current code block if user is un-authorized + await sessionUtils.authenticateSession(data.user, data.pass, req); + + //If the user already has a remember me token + if(data.rememberme != null && data.rememberme.id != null){ + //Fucking nuke the bitch + await rememberMeModel.deleteOne({id: data.rememberme.id}) + + //Tell the client to drop the token + res.clearCookie("rememberme.id"); + res.clearCookie("rememberme.token"); + } + + //If the user requested a rememberMe token (I'm not validation checking a fucking boolean) + if(req.body.rememberMe){ + //Gen user token + //requires second DB call, but this enforces password requirement for toke generation while ensuring we only + //need one function in the userModel for authentication, even if the second woulda just been a wrapper. + //Less attack surface is less attack surface, and this isn't something thats going to be getting constantly called + const authToken = await rememberMeModel.genToken(data.user, data.pass); + + //Check config for protocol + const secure = config.protocol.toLowerCase() == "https"; + + //Set remember me ID and token as browser-side cookies for safe-keeping + res.cookie("rememberme.id", authToken.id, {sameSite: 'strict', httpOnly: true, secure}); + //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}); + } + + //Tell the browser everything is dandy return res.sendStatus(200); }else{ res.status(400); @@ -64,22 +94,22 @@ module.exports.post = async function(req, res){ return res.sendStatus(301); } } - + //Get login attempts const attempts = sessionUtils.getLoginAttempts(user) //if we've gone over max attempts - if(attempts.count > sessionUtils.throttleAttempts){ + if(attempts != null && attempts.count > sessionUtils.throttleAttempts){ //tell client it needs a captcha return res.sendStatus(429); + }else{ + //Scream about any un-caught errors + return exceptionHandler(res, err); } }else{ res.status(400); return res.send({errors: validResult.array()}) } - - //Scream about any un-caught errors - return exceptionHandler(res, err); } } \ No newline at end of file diff --git a/src/schemas/user/rememberMeSchema.js b/src/schemas/user/rememberMeSchema.js index 6b63942..3ac78ef 100644 --- a/src/schemas/user/rememberMeSchema.js +++ b/src/schemas/user/rememberMeSchema.js @@ -27,7 +27,7 @@ const crypto = require("node:crypto"); const {mongoose} = require('mongoose'); //Local Imports -const userSchema = require('./userSchema'); +const {userModel} = require('./userSchema'); const hashUtil = require('../../utils/hashUtils'); const loggerUtils = require('../../utils/loggerUtils'); @@ -67,10 +67,14 @@ const rememberMeToken = new mongoose.Schema({ * Pre-Save function for rememberMeSchema */ rememberMeToken.pre('save', async function (next){ + //Ensure tokens ALWAYS get a new UUID and creation date + this.id = crypto.randomUUID(); + this.date = new Date(); + //If the token was changed if(this.isModified("token")){ //Hash that sunnovabitch, no questions asked. - this.token = hashUtil.hashRememberMeToken(this.token); + this.token = await hashUtil.hashRememberMeToken(this.token); } //All is good, continue on saving. @@ -79,10 +83,10 @@ rememberMeToken.pre('save', async function (next){ //statics rememberMeToken.statics.genToken = async function(user, pass){ - try{ - //Authenticate user and pull document - const userDB = await userSchema.authenticate(user, pass); + //Authenticate user and pull document + const userDB = await userModel.authenticate(user, pass); + try{ //Generate a cryptographically secure string of 32 bytes in hexidecimal const token = crypto.randomBytes(32).toString('hex'); @@ -94,7 +98,7 @@ rememberMeToken.statics.genToken = async function(user, pass){ id: tokenDB.id, token }; - //If we failed (most likely for bad login) + //If we failed for a non-login reason }catch(err){ return loggerUtils.localExceptionHandler(err); } diff --git a/src/server.js b/src/server.js index 8aedf91..87472a2 100644 --- a/src/server.js +++ b/src/server.js @@ -25,6 +25,7 @@ const fs = require('fs'); const express = require('express'); const session = require('express-session'); const {createServer } = require('http'); +const cookieParser = require('cookie-parser'); const { Server } = require('socket.io'); const path = require('path'); const mongoStore = require('connect-mongo'); @@ -38,6 +39,8 @@ const pmHandler = require('./app/pm/pmHandler'); const configCheck = require('./utils/configCheck'); const scheduler = require('./utils/scheduler'); const {errorMiddleware} = require('./utils/loggerUtils'); +//Validator +const accountValidator = require('./validators/accountValidator'); //DB Model const statModel = require('./schemas/statSchema'); const flairModel = require('./schemas/flairSchema'); @@ -87,7 +90,11 @@ const sessionMiddleware = session({ secret: config.secrets.sessionSecret, resave: false, saveUninitialized: false, - store: module.exports.store + store: module.exports.store, + cookie: { + sameSite: "strict", + secure: config.protocol.toLowerCase() == "https" + } }); //Declare web server @@ -143,7 +150,9 @@ app.set('views', __dirname + '/views'); //Middlware //Enable Express app.use(express.json()); -//app.use(express.urlencoded()); + +//Enable Express Ccokie-Parser +app.use(cookieParser()); //Enable Express-Sessions app.use(sessionMiddleware); @@ -151,6 +160,10 @@ app.use(sessionMiddleware); //Enable Express-Session w/ Socket.IO io.engine.use(sessionMiddleware); +//Use rememberMe validators accross all requests. +app.use(accountValidator.rememberMeID()); +app.use(accountValidator.rememberMeToken()); + //Routes //Humie-Friendly app.use('/', indexRouter); diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 584411d..970718b 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -18,6 +18,7 @@ along with this program. If not, see .*/ const config = require('../../config.json'); const {userModel} = require('../schemas/user/userSchema.js'); const userBanModel = require('../schemas/user/userBanSchema.js'); +const rememberMeModel = require('../schemas/user/rememberMeSchema.js'); const altchaUtils = require('../utils/altchaUtils.js'); const loggerUtils = require('../utils/loggerUtils.js'); diff --git a/src/validators/accountValidator.js b/src/validators/accountValidator.js index 4e031d3..806aa43 100644 --- a/src/validators/accountValidator.js +++ b/src/validators/accountValidator.js @@ -177,19 +177,41 @@ module.exports.rank = function(field = 'rank'){ }); } -module.exports.securityToken = function(field = 'token'){ - return checkSchema({ - [field]: { - escape: true, - trim: true, - isHexadecimal: true, - isLength: { - options: { - min: 64, - max: 64 - } - }, - errorMessage: "Invalid security token." +const securityTokenSchema = { + escape: true, + trim: true, + isHexadecimal: true, + isLength: { + options: { + min: 64, + max: 64 } - }); + }, + errorMessage: "Invalid security token." +} + +module.exports.securityToken = function(field = 'token'){ + return checkSchema({[field]:securityTokenSchema}); +} + +module.exports.rememberMeID = function(field = 'rememberme.id'){ + return checkSchema({ + [field]:{ + in: ['cookies'], + optional: true, + isUUID: true + } + }) +} + +module.exports.rememberMeToken = function(field = 'rememberme.token'){ + //Create our own schema with blackjack and hookers + const tokenSchema = structuredClone(securityTokenSchema); + + //Modify as needed + tokenSchema.in = ['cookies']; + tokenSchema.optional = true; + + //Return the validator + return checkSchema({[field]:tokenSchema}); } \ No newline at end of file diff --git a/src/views/login.ejs b/src/views/login.ejs index 1a5e63f..12e8e78 100644 --- a/src/views/login.ejs +++ b/src/views/login.ejs @@ -38,6 +38,7 @@ along with this program. If not, see . %> <% if(challenge != null){ %> <% } %> + Create New Account Forgot Password diff --git a/src/views/partial/navbar.ejs b/src/views/partial/navbar.ejs index 924efba..3dc3065 100644 --- a/src/views/partial/navbar.ejs +++ b/src/views/partial/navbar.ejs @@ -19,6 +19,8 @@ along with this program. If not, see . %> <% if(user){ %> <% }else{ %> + + diff --git a/www/js/login.js b/www/js/login.js index c5a3408..cb93172 100644 --- a/www/js/login.js +++ b/www/js/login.js @@ -21,6 +21,8 @@ class registerPrompt{ this.user.value = window.location.search.replace("?user=",''); //Grab pass prompts this.pass = document.querySelector("#login-page-password"); + //Remember me checkbox + this.rememberMe = document.querySelector("#login-page-remember-me"); //Grab register button this.button = document.querySelector("#login-page-button"); //Grab altcha widget @@ -58,10 +60,10 @@ class registerPrompt{ } //login with verification - utils.ajax.login(this.user.value , this.pass.value, this.verification); + utils.ajax.login(this.user.value , this.pass.value, this.rememberMe.checked, this.verification); }else{ //login - utils.ajax.login(this.user.value, this.pass.value); + utils.ajax.login(this.user.value, this.pass.value, this.rememberMe.checked); } } } diff --git a/www/js/navbar.js b/www/js/navbar.js index 69697fd..ce9bac3 100644 --- a/www/js/navbar.js +++ b/www/js/navbar.js @@ -19,6 +19,7 @@ async function navbarLogin(event){ if(!event || !event.key || event.key == "Enter"){ var user = document.querySelector("#username-prompt").value; var pass = document.querySelector("#password-prompt").value; + var rememberMe = document.querySelector("#remember-me").checked; //If no user or pass is presented if(user == "" || pass == ""){ @@ -26,7 +27,7 @@ async function navbarLogin(event){ window.location = '/login' } - utils.ajax.login(user, pass); + utils.ajax.login(user, pass, rememberMe); } } diff --git a/www/js/utils.js b/www/js/utils.js index 9e023f1..2db077a 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -755,14 +755,14 @@ class canopyAjaxUtils{ } } - async login(user, pass, verification){ + async login(user, pass, rememberMe, verification){ const response = await fetch(`/api/account/login`,{ method: "POST", headers: { "Content-Type": "application/json", "x-csrf-token": utils.ajax.getCSRFToken() }, - body: JSON.stringify(verification ? {user, pass, verification} : {user, pass}) + body: JSON.stringify(verification ? {user, pass, rememberMe, verification} : {user, rememberMe, pass}) }); if(response.ok){