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",
"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",

View file

@ -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);
const data = matchedData(req);
//try to authenticate the session, and return a successful code if it works
await sessionUtils.authenticateSession(user, pass, 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);
@ -69,17 +99,17 @@ module.exports.post = async function(req, res){
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);
}
}

View file

@ -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);
}

View file

@ -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);

View file

@ -18,6 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
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');

View file

@ -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});
}

View file

@ -38,6 +38,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<% if(challenge != null){ %>
<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="/passwordReset">Forgot Password</a>
<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){ %>
<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{ %>
<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="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>

View file

@ -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);
}
}
}

View file

@ -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);
}
}

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`,{
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){