Continued work on remember me tokens.
This commit is contained in:
parent
95ed2fa403
commit
e00e5a608b
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -69,17 +99,17 @@ module.exports.post = async function(req, res){
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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 userSchema.authenticate(user, pass);
|
const userDB = await userModel.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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,9 +177,7 @@ module.exports.rank = function(field = 'rank'){
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.securityToken = function(field = 'token'){
|
const securityTokenSchema = {
|
||||||
return checkSchema({
|
|
||||||
[field]: {
|
|
||||||
escape: true,
|
escape: true,
|
||||||
trim: true,
|
trim: true,
|
||||||
isHexadecimal: true,
|
isHexadecimal: true,
|
||||||
|
|
@ -191,5 +189,29 @@ module.exports.securityToken = function(field = 'token'){
|
||||||
},
|
},
|
||||||
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});
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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){
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue