Continued work on remember me tokens.
This commit is contained in:
parent
95ed2fa403
commit
e00e5a608b
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -177,9 +177,7 @@ module.exports.rank = function(field = 'rank'){
|
|||
});
|
||||
}
|
||||
|
||||
module.exports.securityToken = function(field = 'token'){
|
||||
return checkSchema({
|
||||
[field]: {
|
||||
const securityTokenSchema = {
|
||||
escape: true,
|
||||
trim: true,
|
||||
isHexadecimal: true,
|
||||
|
|
@ -190,6 +188,30 @@ module.exports.securityToken = function(field = '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});
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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){
|
||||
|
|
|
|||
Loading…
Reference in a new issue