Continued work on remember me tokens.

This commit is contained in:
rainbow napkin 2025-10-20 07:49:41 -04:00
parent 95ed2fa403
commit e00e5a608b
11 changed files with 113 additions and 36 deletions

View file

@ -10,6 +10,7 @@
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"connect-mongo": "^5.1.0", "connect-mongo": "^5.1.0",
"cookie-parser": "^1.4.7",
"csrf-sync": "^4.0.3", "csrf-sync": "^4.0.3",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^4.18.2", "express": "^4.18.2",

View file

@ -22,6 +22,7 @@ const {validationResult, matchedData} = require('express-validator');
//local imports //local imports
const migrationModel = require('../../../schemas/user/migrationSchema.js'); const migrationModel = require('../../../schemas/user/migrationSchema.js');
const rememberMeModel = require('../../../schemas/user/rememberMeSchema.js');
const sessionUtils = require('../../../utils/sessionUtils'); const sessionUtils = require('../../../utils/sessionUtils');
const hashUtils = require('../../../utils/hashUtils.js'); const hashUtils = require('../../../utils/hashUtils.js');
const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils');
@ -35,10 +36,39 @@ module.exports.post = async function(req, res){
//if we don't have errors //if we don't have errors
if(validResult.isEmpty()){ if(validResult.isEmpty()){
//Pull sanatzied/validated data //Pull sanatzied/validated data
const {user, pass} = matchedData(req); const data = matchedData(req);
//try to authenticate the session, and return a successful code if it works //try to authenticate the session, throwing an error and breaking the current code block if user is un-authorized
await sessionUtils.authenticateSession(user, pass, req); 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); return res.sendStatus(200);
}else{ }else{
res.status(400); res.status(400);
@ -64,22 +94,22 @@ module.exports.post = async function(req, res){
return res.sendStatus(301); return res.sendStatus(301);
} }
} }
//Get login attempts //Get login attempts
const attempts = sessionUtils.getLoginAttempts(user) const attempts = sessionUtils.getLoginAttempts(user)
//if we've gone over max attempts //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 //tell client it needs a captcha
return res.sendStatus(429); return res.sendStatus(429);
}else{
//Scream about any un-caught errors
return exceptionHandler(res, err);
} }
}else{ }else{
res.status(400); res.status(400);
return res.send({errors: validResult.array()}) return res.send({errors: validResult.array()})
} }
//Scream about any un-caught errors
return exceptionHandler(res, err);
} }
} }

View file

@ -27,7 +27,7 @@ const crypto = require("node:crypto");
const {mongoose} = require('mongoose'); const {mongoose} = require('mongoose');
//Local Imports //Local Imports
const userSchema = require('./userSchema'); const {userModel} = require('./userSchema');
const hashUtil = require('../../utils/hashUtils'); const hashUtil = require('../../utils/hashUtils');
const loggerUtils = require('../../utils/loggerUtils'); const loggerUtils = require('../../utils/loggerUtils');
@ -67,10 +67,14 @@ const rememberMeToken = new mongoose.Schema({
* Pre-Save function for rememberMeSchema * Pre-Save function for rememberMeSchema
*/ */
rememberMeToken.pre('save', async function (next){ 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 the token was changed
if(this.isModified("token")){ if(this.isModified("token")){
//Hash that sunnovabitch, no questions asked. //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. //All is good, continue on saving.
@ -79,10 +83,10 @@ rememberMeToken.pre('save', async function (next){
//statics //statics
rememberMeToken.statics.genToken = async function(user, pass){ rememberMeToken.statics.genToken = async function(user, pass){
try{ //Authenticate user and pull document
//Authenticate user and pull document const userDB = await userModel.authenticate(user, pass);
const userDB = await userSchema.authenticate(user, pass);
try{
//Generate a cryptographically secure string of 32 bytes in hexidecimal //Generate a cryptographically secure string of 32 bytes in hexidecimal
const token = crypto.randomBytes(32).toString('hex'); const token = crypto.randomBytes(32).toString('hex');
@ -94,7 +98,7 @@ rememberMeToken.statics.genToken = async function(user, pass){
id: tokenDB.id, id: tokenDB.id,
token token
}; };
//If we failed (most likely for bad login) //If we failed for a non-login reason
}catch(err){ }catch(err){
return loggerUtils.localExceptionHandler(err); return loggerUtils.localExceptionHandler(err);
} }

View file

@ -25,6 +25,7 @@ const fs = require('fs');
const express = require('express'); const express = require('express');
const session = require('express-session'); const session = require('express-session');
const {createServer } = require('http'); const {createServer } = require('http');
const cookieParser = require('cookie-parser');
const { Server } = require('socket.io'); const { Server } = require('socket.io');
const path = require('path'); const path = require('path');
const mongoStore = require('connect-mongo'); const mongoStore = require('connect-mongo');
@ -38,6 +39,8 @@ const pmHandler = require('./app/pm/pmHandler');
const configCheck = require('./utils/configCheck'); const configCheck = require('./utils/configCheck');
const scheduler = require('./utils/scheduler'); const scheduler = require('./utils/scheduler');
const {errorMiddleware} = require('./utils/loggerUtils'); const {errorMiddleware} = require('./utils/loggerUtils');
//Validator
const accountValidator = require('./validators/accountValidator');
//DB Model //DB Model
const statModel = require('./schemas/statSchema'); const statModel = require('./schemas/statSchema');
const flairModel = require('./schemas/flairSchema'); const flairModel = require('./schemas/flairSchema');
@ -87,7 +90,11 @@ const sessionMiddleware = session({
secret: config.secrets.sessionSecret, secret: config.secrets.sessionSecret,
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
store: module.exports.store store: module.exports.store,
cookie: {
sameSite: "strict",
secure: config.protocol.toLowerCase() == "https"
}
}); });
//Declare web server //Declare web server
@ -143,7 +150,9 @@ app.set('views', __dirname + '/views');
//Middlware //Middlware
//Enable Express //Enable Express
app.use(express.json()); app.use(express.json());
//app.use(express.urlencoded());
//Enable Express Ccokie-Parser
app.use(cookieParser());
//Enable Express-Sessions //Enable Express-Sessions
app.use(sessionMiddleware); app.use(sessionMiddleware);
@ -151,6 +160,10 @@ app.use(sessionMiddleware);
//Enable Express-Session w/ Socket.IO //Enable Express-Session w/ Socket.IO
io.engine.use(sessionMiddleware); io.engine.use(sessionMiddleware);
//Use rememberMe validators accross all requests.
app.use(accountValidator.rememberMeID());
app.use(accountValidator.rememberMeToken());
//Routes //Routes
//Humie-Friendly //Humie-Friendly
app.use('/', indexRouter); app.use('/', indexRouter);

View file

@ -18,6 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const config = require('../../config.json'); const config = require('../../config.json');
const {userModel} = require('../schemas/user/userSchema.js'); const {userModel} = require('../schemas/user/userSchema.js');
const userBanModel = require('../schemas/user/userBanSchema.js'); const userBanModel = require('../schemas/user/userBanSchema.js');
const rememberMeModel = require('../schemas/user/rememberMeSchema.js');
const altchaUtils = require('../utils/altchaUtils.js'); const altchaUtils = require('../utils/altchaUtils.js');
const loggerUtils = require('../utils/loggerUtils.js'); const loggerUtils = require('../utils/loggerUtils.js');

View file

@ -177,19 +177,41 @@ module.exports.rank = function(field = 'rank'){
}); });
} }
module.exports.securityToken = function(field = 'token'){ const securityTokenSchema = {
return checkSchema({ escape: true,
[field]: { trim: true,
escape: true, isHexadecimal: true,
trim: true, isLength: {
isHexadecimal: true, options: {
isLength: { min: 64,
options: { max: 64
min: 64,
max: 64
}
},
errorMessage: "Invalid security token."
} }
}); },
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});
} }

View file

@ -38,6 +38,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<% if(challenge != null){ %> <% if(challenge != null){ %>
<altcha-widget challengejson="<%= JSON.stringify(challenge) %>"></altcha-widget> <altcha-widget challengejson="<%= JSON.stringify(challenge) %>"></altcha-widget>
<% } %> <% } %>
<span><label>Remember Me:</label><input class="login-page" id="login-page-remember-me" type="checkbox"></span>
<a href="/register">Create New Account</a> <a href="/register">Create New Account</a>
<a href="/passwordReset">Forgot Password</a> <a href="/passwordReset">Forgot Password</a>
<button id="login-page-button" class='positive-button'>Login</button> <button id="login-page-button" class='positive-button'>Login</button>

View file

@ -19,6 +19,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<% if(user){ %> <% if(user){ %>
<p class="navbar-item">Welcome, <a class="navbar-item" id="username" href="/profile"><%= user.user %></a> - <% if(user.rank == "admin"){ %><a href="/adminPanel" title="Admin Panel" class="bi bi-server navbar-item"></a> <% } %><a class="navbar-item" href="javascript:" id="logout-button">logout</a></p> <p class="navbar-item">Welcome, <a class="navbar-item" id="username" href="/profile"><%= user.user %></a> - <% if(user.rank == "admin"){ %><a href="/adminPanel" title="Admin Panel" class="bi bi-server navbar-item"></a> <% } %><a class="navbar-item" href="javascript:" id="logout-button">logout</a></p>
<% }else{ %> <% }else{ %>
<p class="navbar-item">Remember Me:</p>
<input class="navbar-item login-prompt" id="remember-me" type="checkbox">
<input class="navbar-item login-prompt" id="username-prompt" placeholder="username"> <input class="navbar-item login-prompt" id="username-prompt" placeholder="username">
<input class="navbar-item login-prompt" id="password-prompt" placeholder="password" type="password"> <input class="navbar-item login-prompt" id="password-prompt" placeholder="password" type="password">
<p class="navbar-item"><a class="navbar-item" href="javascript:" id="login-button">Login</a> - <a class="navbar-item" href="/passwordReset">Forgot Password</a> - <a class="navbar-item" href="/register">Register</a></p> <p class="navbar-item"><a class="navbar-item" href="javascript:" id="login-button">Login</a> - <a class="navbar-item" href="/passwordReset">Forgot Password</a> - <a class="navbar-item" href="/register">Register</a></p>

View file

@ -21,6 +21,8 @@ class registerPrompt{
this.user.value = window.location.search.replace("?user=",''); this.user.value = window.location.search.replace("?user=",'');
//Grab pass prompts //Grab pass prompts
this.pass = document.querySelector("#login-page-password"); this.pass = document.querySelector("#login-page-password");
//Remember me checkbox
this.rememberMe = document.querySelector("#login-page-remember-me");
//Grab register button //Grab register button
this.button = document.querySelector("#login-page-button"); this.button = document.querySelector("#login-page-button");
//Grab altcha widget //Grab altcha widget
@ -58,10 +60,10 @@ class registerPrompt{
} }
//login with verification //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{ }else{
//login //login
utils.ajax.login(this.user.value, this.pass.value); utils.ajax.login(this.user.value, this.pass.value, this.rememberMe.checked);
} }
} }
} }

View file

@ -19,6 +19,7 @@ async function navbarLogin(event){
if(!event || !event.key || event.key == "Enter"){ if(!event || !event.key || event.key == "Enter"){
var user = document.querySelector("#username-prompt").value; var user = document.querySelector("#username-prompt").value;
var pass = document.querySelector("#password-prompt").value; var pass = document.querySelector("#password-prompt").value;
var rememberMe = document.querySelector("#remember-me").checked;
//If no user or pass is presented //If no user or pass is presented
if(user == "" || pass == ""){ if(user == "" || pass == ""){
@ -26,7 +27,7 @@ async function navbarLogin(event){
window.location = '/login' window.location = '/login'
} }
utils.ajax.login(user, pass); utils.ajax.login(user, pass, rememberMe);
} }
} }

View file

@ -755,14 +755,14 @@ class canopyAjaxUtils{
} }
} }
async login(user, pass, verification){ async login(user, pass, rememberMe, verification){
const response = await fetch(`/api/account/login`,{ const response = await fetch(`/api/account/login`,{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken() "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){ if(response.ok){