From bc0657a70243646cc538b009f9a352057e49a32a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 21 Oct 2025 07:59:15 -0400 Subject: [PATCH] Remember me tokens now nuked upon full account logout. --- .../api/account/loginController.js | 23 +++++++++++-------- .../api/account/logoutController.js | 2 +- src/schemas/user/rememberMeSchema.js | 11 +++++---- src/schemas/user/userSchema.js | 4 ++++ src/utils/sessionUtils.js | 2 +- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/controllers/api/account/loginController.js b/src/controllers/api/account/loginController.js index 293842c..d509342 100644 --- a/src/controllers/api/account/loginController.js +++ b/src/controllers/api/account/loginController.js @@ -39,7 +39,7 @@ module.exports.post = async function(req, res){ 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); + const userDB = 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){ @@ -57,18 +57,21 @@ module.exports.post = async function(req, res){ //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); + const authToken = await rememberMeModel.genToken(userDB, data.pass); - //Check config for protocol - const secure = config.protocol.toLowerCase() == "https"; + //If we properly authed + if(authToken != null){ + //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)); + //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, 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, expires}); + //Set remember me ID and token as browser-side cookies for safe-keeping + 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, expires}); + } } //Tell the browser everything is dandy diff --git a/src/controllers/api/account/logoutController.js b/src/controllers/api/account/logoutController.js index 1964edc..0688f42 100644 --- a/src/controllers/api/account/logoutController.js +++ b/src/controllers/api/account/logoutController.js @@ -34,7 +34,7 @@ module.exports.post = async function(req, res){ const data = matchedData(req); //If the user has a remember me token id they've submitted with the request - if(data.rememberme.id){ + if(data.rememberme != null && data.rememberme.id != null){ //Find the associated token and nuke it await rememberMeModel.deleteOne({id: data.rememberme.id}) } diff --git a/src/schemas/user/rememberMeSchema.js b/src/schemas/user/rememberMeSchema.js index 74fc11d..dfb925b 100644 --- a/src/schemas/user/rememberMeSchema.js +++ b/src/schemas/user/rememberMeSchema.js @@ -27,7 +27,6 @@ const crypto = require("node:crypto"); const {mongoose} = require('mongoose'); //Local Imports -const {userModel} = require('./userSchema'); const hashUtil = require('../../utils/hashUtils'); const loggerUtils = require('../../utils/loggerUtils'); @@ -88,9 +87,13 @@ rememberMeToken.methods.checkToken = async function(token){ } //statics -rememberMeToken.statics.genToken = async function(user, pass){ - //Authenticate user and pull document - const userDB = await userModel.authenticate(user, pass); +rememberMeToken.statics.genToken = async function(userDB, pass){ + //Normally I'd use userModel auth, but this saves on DB calls and keeps us from having to refrence the userModel directly + //Saving us from circular depedency hell + //Plus this is only really getting called along-side other auth, theres already going to be an error message if this is wrong XP + if(!await userDB.checkPass(pass)){ + return; + } try{ //Generate a cryptographically secure string of 32 bytes in hexidecimal diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index e9368a3..cc4d364 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -28,6 +28,7 @@ const permissionModel = require('../permissionSchema'); const emoteModel = require('../emoteSchema'); const emailChangeModel = require('./emailChangeSchema'); const playlistSchema = require('../channel/media/playlistSchema'); +const rememberMeModel = require('./rememberMeSchema'); //Utils const hashUtil = require('../../utils/hashUtils'); const mailUtil = require('../../utils/mailUtils'); @@ -807,6 +808,9 @@ userSchema.methods.tattooIPRecord = async function(ip){ * @param {String} reason - Reason to kill user sessions */ userSchema.methods.killAllSessions = async function(reason = "A full log-out from all devices was requested for your account."){ + //Nuke all related remember me tokens + await rememberMeModel.deleteMany({user: this._id}); + //get authenticated sessions var sessions = await this.getAuthenticatedSessions(); diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 7d4a94b..7c5ad33 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -145,7 +145,7 @@ module.exports.authenticateSession = async function(identifier, secret, req, use } //return user - return userDB.user; + return userDB; }catch(err){ //Failed attempts at good tokens are handled by the token schema by dropping the users effected tokens and screaming bloody murder //Failed attempts with bad tokens don't need to be handled as it's not like attacking a bad UUID is going to get you anywhere anywho