From 895a8201a5a00a07675b4931e19807951f1c4454 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 18 Oct 2025 09:15:00 -0400 Subject: [PATCH] Started work on Remember Me Tokens. --- src/schemas/user/rememberMeSchema.js | 103 +++++++++++++++++++++++++++ src/utils/hashUtils.js | 21 ++++++ 2 files changed, 124 insertions(+) create mode 100644 src/schemas/user/rememberMeSchema.js diff --git a/src/schemas/user/rememberMeSchema.js b/src/schemas/user/rememberMeSchema.js new file mode 100644 index 0000000..6b63942 --- /dev/null +++ b/src/schemas/user/rememberMeSchema.js @@ -0,0 +1,103 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024-2025 Rainbownapkin and the TTN Community + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see .*/ + +//You could make an argument for making this part of the userModel +//However, this is so rarely used the preformance benefits aren't worth the extra clutter + +//Config +const config = require('../../../config.json'); + +//Node Imports +const crypto = require("node:crypto"); + +//NPM Imports +const {mongoose} = require('mongoose'); + +//Local Imports +const userSchema = require('./userSchema'); +const hashUtil = require('../../utils/hashUtils'); +const loggerUtils = require('../../utils/loggerUtils'); + +/** + * Password reset token retention time + * + * Lasts about half a year + */ +const daysToExpire = 182; + +/** + * DB Schema for documents containing a single expiring password reset token + */ +const rememberMeToken = new mongoose.Schema({ + id: { + type: mongoose.SchemaTypes.UUID, + required: true, + default: crypto.randomUUID() + }, + user: { + type: mongoose.SchemaTypes.ObjectID, + ref: "user", + required: true + }, + token: { + type: mongoose.SchemaTypes.String, + required: true + }, + date: { + type: mongoose.SchemaTypes.Date, + required: true, + default: new Date() + } +}); + +/** + * Pre-Save function for rememberMeSchema + */ +rememberMeToken.pre('save', async function (next){ + //If the token was changed + if(this.isModified("token")){ + //Hash that sunnovabitch, no questions asked. + this.token = hashUtil.hashRememberMeToken(this.token); + } + + //All is good, continue on saving. + next(); +}); + +//statics +rememberMeToken.statics.genToken = async function(user, pass){ + try{ + //Authenticate user and pull document + const userDB = await userSchema.authenticate(user, pass); + + //Generate a cryptographically secure string of 32 bytes in hexidecimal + const token = crypto.randomBytes(32).toString('hex'); + + //Create token document off of user and token string + const tokenDB = await this.create({user: userDB._id, token}); + + //Return token document UUID w/ plaintext token for browser consumption + return { + id: tokenDB.id, + token + }; + //If we failed (most likely for bad login) + }catch(err){ + return loggerUtils.localExceptionHandler(err); + } +} + +module.exports = mongoose.model("rememberMe", rememberMeToken); \ No newline at end of file diff --git a/src/utils/hashUtils.js b/src/utils/hashUtils.js index 95aaf71..b9086cc 100644 --- a/src/utils/hashUtils.js +++ b/src/utils/hashUtils.js @@ -21,6 +21,7 @@ const config = require('../../config.json'); const crypto = require('node:crypto'); //NPM Imports +const argon2 = require('argon2'); const bcrypt = require('bcrypt'); /** @@ -69,4 +70,24 @@ module.exports.hashIP = function(ip){ //return the IP hash as a string return hashObj.digest('hex'); +} + +/** + * Site-wide remember-me token hashing function + * @param {String} token - Token to hash + * @returns {String} - Hashed token + */ +module.exports.hashRememberMeToken = async function(token){ + return await argon2.hash(token); +} + +/** + * Site-wide remember-me token hash comparison function + * @param {String} token - Token to compare + * @param {String} hash - Hash to compare + * @returns {String} - Comparison results + */ +module.exports.compareRememberMeToken = async function(token, hash){ + //Compare hash and return result + return await argon2.verify(hash, token); } \ No newline at end of file