From ba758e43710de09b30ddd64b16c59075b0ef3494 Mon Sep 17 00:00:00 2001 From: MattPlayGamez <62027538+MattPlayGamez@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:53:09 +0000 Subject: [PATCH 1/2] updated constructor to make it easier --- file.js | 37 +++++++++++++++---------------------- file.test.js | 8 ++++---- memory.js | 29 +++++++++-------------------- memory.test.js | 5 ++--- mongodb.js | 25 +++++++++++-------------- mongodb.test.js | 6 +++--- package-lock.json | 6 +++--- package.json | 2 +- 8 files changed, 48 insertions(+), 70 deletions(-) diff --git a/file.js b/file.js index b2617f7..2213791 100644 --- a/file.js +++ b/file.js @@ -48,25 +48,17 @@ function loadUsersFromFile(filePath, password) { class Authenticator { - /** - * Constructor for the Authenticator class - * @param {string} QR_LABEL - label for the QR code - * @param {number} rounds - number of rounds for bcrypt - * @param {string} JWT_SECRET_KEY - secret key for signing JWTs - * @param {object} JWT_OPTIONS - options for JWTs such as expiresIn - * @param {number} maxLoginAttempts - maximum number of login attempts - * @param {string} DB_FILE_PATH - path to the file where the users are stored - * @param {string} DB_PASSWORD - password to decrypt the file - */ - constructor(QR_LABEL, rounds, JWT_SECRET_KEY, JWT_OPTIONS, maxLoginAttempts, DB_FILE_PATH, DB_PASSWORD) { - this.QR_LABEL = QR_LABEL; - this.rounds = rounds; - this.JWT_SECRET_KEY = JWT_SECRET_KEY; - this.JWT_OPTIONS = JWT_OPTIONS; - this.maxLoginAttempts = maxLoginAttempts - 2; - this.users = loadUsersFromFile(DB_FILE_PATH, DB_PASSWORD); - this.DB_FILE_PATH = DB_FILE_PATH - this.DB_PASSWORD = DB_PASSWORD + + constructor() { + this.QR_LABEL = "Authenticator"; + this.rounds = 12; + this.JWT_SECRET_KEY = "changeme"; + this.JWT_OPTIONS = { expiresIn: "1h" }; + this.maxLoginAttempts = 13 + this.maxLoginAttempts = this.maxLoginAttempts - 2; + this.DB_FILE_PATH = "./users.db" + this.DB_PASSWORD = "changeme" + this.users = loadUsersFromFile(this.DB_FILE_PATH, this.DB_PASSWORD); this.OTP_ENCODING = 'base32' this.lockedText = "User is locked" this.OTP_WINDOW = 1 // How many OTP codes can be used before and after the current one (usefull for slower people, recommended 1) @@ -77,6 +69,7 @@ class Authenticator { // Override methods to update file when users array changes const originalPush = this.users.push; + this.users.push = (...args) => { const result = originalPush.apply(this.users, args); saveUsersToFile(this.users, this.DB_FILE_PATH, this.DB_PASSWORD); @@ -142,11 +135,11 @@ class Authenticator { try { const result = await bcrypt.compare(password, account.password); - + if (!result) { - + (account.loginAttempts >= this.maxLoginAttempts) ? this.lockUser(account.id) : await this.changeLoginAttempts(account._id, account.loginAttempts + 1) - + return null }; if (account) { diff --git a/file.test.js b/file.test.js index d34329f..ebe5de7 100644 --- a/file.test.js +++ b/file.test.js @@ -28,10 +28,10 @@ describe('Authenticator Class Tests', () => { let emailCode = "" beforeAll(async () => { - authenticator = new Authenticator( - 'TestApp', 10, JWT_SECRET, { expiresIn: '1h' }, 3, "app.db", "password123" - ); + authenticator = new Authenticator(); + authenticator.rounds = 10 authenticator.ALLOW_DB_DUMP = true + authenticator.JWT_SECRET_KEY = JWT_SECRET }); @@ -216,7 +216,7 @@ describe('Authenticator Class Tests', () => { afterAll(async () => { console.log(await authenticator.dumpDB()) - fs.unlinkSync("./app.db") + fs.unlinkSync(authenticator.DB_FILE_PATH) }); }); diff --git a/memory.js b/memory.js index 7d305bc..97ee9c2 100644 --- a/memory.js +++ b/memory.js @@ -8,27 +8,17 @@ const fs = require('fs'); const Crypto = require('node:crypto') -const algorithm = 'aes-256-ctr'; - class Authenticator { - /** - * Constructor for the Authenticator class - * @param {string} QR_LABEL - label for the QR code - * @param {number} rounds - number of rounds for bcrypt - * @param {string} JWT_SECRET_KEY - secret key for signing JWTs - * @param {object} JWT_OPTIONS - options for JWTs such as expiresIn - * @param {number} maxLoginAttempts - maximum number of login attempts - * @param {string} DB_FILE_PATH - path to the file where the users are stored - * @param {string} DB_PASSWORD - password to decrypt the file - */ - constructor(QR_LABEL, rounds, JWT_SECRET_KEY, JWT_OPTIONS, maxLoginAttempts, USER_ARRAY) { - this.QR_LABEL = QR_LABEL; - this.rounds = rounds; - this.JWT_SECRET_KEY = JWT_SECRET_KEY; - this.JWT_OPTIONS = JWT_OPTIONS; - this.maxLoginAttempts = maxLoginAttempts - 2; - this.users = USER_ARRAY; + constructor() { + + this.QR_LABEL = "Authenticator"; + this.rounds = 12; + this.JWT_SECRET_KEY = "changeme"; + this.JWT_OPTIONS = { expiresIn: "1h" }; + this.maxLoginAttempts = 13 + this.maxLoginAttempts = this.maxLoginAttempts - 2; + this.users = [] this.OTP_ENCODING = 'base32' this.lockedText = "User is locked" this.OTP_WINDOW = 1 // How many OTP codes can be used before and after the current one (usefull for slower people, recommended 1) @@ -37,7 +27,6 @@ class Authenticator { this.USER_ALREADY_EXISTS_TEXT = "User already exists" this.ALLOW_DB_DUMP = false // Allowing DB Dumping is disabled by default can be enabled by setting ALLOW_DB_DUMP to true after initializing your class - } diff --git a/memory.test.js b/memory.test.js index 681407f..ea020b0 100644 --- a/memory.test.js +++ b/memory.test.js @@ -27,9 +27,8 @@ describe('Authenticator Class Tests', () => { let emailCode = "" beforeAll(async () => { - authenticator = new Authenticator( - 'TestApp', 10, JWT_SECRET, { expiresIn: '1h' }, 3, [] - ); + authenticator = new Authenticator() + authenticator.JWT_SECRET_KEY = JWT_SECRET authenticator.ALLOW_DB_DUMP = true }); diff --git a/mongodb.js b/mongodb.js index 3f18059..e50bf4c 100644 --- a/mongodb.js +++ b/mongodb.js @@ -8,22 +8,18 @@ const Crypto = require('node:crypto') // Creƫer het gebruikersmodel class Authenticator { + /** * Constructor for the Authenticator class - * @param {string} QR_LABEL - label for the QR code - * @param {number} salt - salt for hashing passwords - * @param {string} JWT_SECRET_KEY - secret key for signing JWTs - * @param {object} JWT_OPTIONS - options for JWTs such as expiresIn - * @param {number} maxLoginAttempts - maximum number of login attempts * @param {string} MONGODB_CONNECTION_STRING - connection string for MongoDB * @param {mongoose.Schema} userSchema - schema for the User model */ - constructor(QR_LABEL, salt, JWT_SECRET_KEY, JWT_OPTIONS, maxLoginAttempts, MONGODB_CONNECTION_STRING, userSchema) { - this.QR_LABEL = QR_LABEL; - this.salt = salt; - this.JWT_SECRET_KEY = JWT_SECRET_KEY; - this.JWT_OPTIONS = JWT_OPTIONS; - this.maxLoginAttempts = maxLoginAttempts; + constructor(MONGODB_CONNECTION_STRING, userSchema) { + this.QR_LABEL = "Authenticator"; + this.rounds = 12; + this.JWT_SECRET_KEY = "changeme"; + this.JWT_OPTIONS = { expiresIn: "1h" }; + this.maxLoginAttempts = 3; mongoose.connect(MONGODB_CONNECTION_STRING); this.User = mongoose.model('User', userSchema) this.OTP_ENCODING = 'base32' @@ -35,6 +31,7 @@ class Authenticator { this.ALLOW_DB_DUMP = false // Allowing DB Dumping is disabled by default can be enabled by setting ALLOW_DB_DUMP to true after initializing your class } + /** * Registers a new user * @param {object} userObject - object with required keys: email, password, wants2FA, you can add custom keys too @@ -43,7 +40,7 @@ class Authenticator { */ async register(userObject) { try { - const hash = await bcrypt.hash(userObject.password, this.salt); + const hash = await bcrypt.hashSync(userObject.password, this.rounds); let newUser = new this.User({ ...userObject, password: hash, @@ -93,7 +90,7 @@ class Authenticator { try { const result = await bcrypt.compare(password, user.password); if (!result) { - + if (user.loginAttempts >= this.maxLoginAttempts) { this.lockUser(user._id); @@ -244,7 +241,7 @@ class Authenticator { */ async resetPassword(userId, newPassword) { this.revokeUserTokens(userId) - const hash = await bcrypt.hash(newPassword, this.salt); + const hash = await bcrypt.hashSync(newPassword, this.rounds); return await this.User.findOneAndUpdate({ _id: userId }, { password: hash }, { new: true }) } diff --git a/mongodb.test.js b/mongodb.test.js index d9e04e8..2c7b8d7 100644 --- a/mongodb.test.js +++ b/mongodb.test.js @@ -44,9 +44,9 @@ describe('Authenticator Class Tests', () => { let emailCode = "" beforeAll(async () => { - authenticator = new Authenticator( - 'TestApp', 10, JWT_SECRET, { expiresIn: '1h' }, 3, MONGODB_CONNECTION_STRING, userSchema - ); + authenticator = new Authenticator(MONGODB_CONNECTION_STRING, userSchema) + authenticator.rounds = 10 + authenticator.JWT_SECRET_KEY = JWT_SECRET authenticator.ALLOW_DB_DUMP = true }); diff --git a/package-lock.json b/package-lock.json index eb7936a..a027278 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3039,9 +3039,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", diff --git a/package.json b/package.json index 1ca7974..bdd6e82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "seamless-auth", - "version": "3.8.4", + "version": "3.8.5", "description": "A full fledged authentication system...", "type": "commonjs", "main": "memory.js", From 61bb06b742248a5f227e57e36d1ca6a6367640b0 Mon Sep 17 00:00:00 2001 From: MattPlayGamez <62027538+MattPlayGamez@users.noreply.github.com> Date: Mon, 23 Dec 2024 22:53:54 +0100 Subject: [PATCH 2/2] made using it easier, by lowering the initialisating protocol --- README.md | 45 +++++++++++++++------ file.js | 64 ++++++++++++++++++------------ file.test.js | 81 ++++++++++++++++++++++++++------------ memory.js | 71 ++++++++++++++++++++------------- memory.test.js | 50 ++++++++++++++---------- mongodb.js | 99 +++++++++++++++++++++++++++++------------------ mongodb.test.js | 33 +++++++++------- package-lock.json | 4 +- package.json | 2 +- 9 files changed, 288 insertions(+), 161 deletions(-) diff --git a/README.md b/README.md index 12baeaa..a8d9929 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ You can add as many fields as you need. (e.g., phone number, address) ```javascript const DB_SCHEMA = { + username: { type: String, required: true, unique: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, loginAttempts: { type: Number, default: 0 }, @@ -54,32 +55,52 @@ const DB_SCHEMA = { Initialize the authenticator with the required parameters: ```javascript +// File / Memory Storage +const auth = new Authenticator(); +// MongoDB Storage const auth = new Authenticator( -QR_LABEL, -SALT, -JWT_SECRET_KEY, -JWT_OPTIONS, -MAX_LOGIN_ATTEMPTS, -USER_OBJECT // Only for memory authentication -DB_CONNECTION_STRING, //for MONGODB or DB_FILE_PATH for file storage -DB_SCHEMA, // for MONGODB schema -DB_PASSWORD // only for file storage -); -``` + MONGODB_STRING, + USER_SCHEMA +) +// There are a lot more options available below which are not required. +``` +## Options +These contain the default inputs and CAN be changed by `auth.QR_LABEL = "something else";` +- `this.QR_LABEL = "Authenticator";` +- `this.rounds = 12;` +- `this.JWT_SECRET_KEY = "changeme";` +- `this.JWT_OPTIONS = { expiresIn: "1h" };` +- `this.maxLoginAttempts = 13;` +- `this.maxLoginAttempts = this.maxLoginAttempts - 2;` +- `this.DB_FILE_PATH = "./users.db";` +- `this.DB_PASSWORD = "changeme";` +- `this.users = [];` +- `this.OTP_ENCODING = 'base32';` +- `this.lockedText = "User is locked";` +- `this.OTP_WINDOW = 1;` // How many OTP codes can be used before and after the current one (usefull for slower people, recommended 1) +- `this.INVALID_2FA_CODE_TEXT = "Invalid 2FA code";` +- `this.REMOVED_USER_TEXT = "User has been removed";` +- `this.USERNAME_ALREADY_EXISTS_TEXT = "This username already exists";` +- `this.EMAIL_ALREADY_EXISTS_TEXT = "This email already exists";` +- `this.USERNAME_IS_REQUIRED="Username is required";` +- `this.ALLOW_DB_DUMP = false;` // Allowing DB Dumping is disabled by default can be enabled by setting ALLOW_DB_DUMP to true after initializing your class ## API ### `register(userObject)` Registers a new user. -### `login(email, password, twoFactorCode || null)` +### `login(username, password, twoFactorCode || null)` Logs in a user. ### `getInfoFromUser(userId)` Retrieves user information. +### `getInfoFromCustom(searchType, value)` +Retrieves user information based on a custom search criteria (like email, username,...) + ### `verifyToken(token)` Verifies a JWT token. diff --git a/file.js b/file.js index 2213791..e147efe 100644 --- a/file.js +++ b/file.js @@ -1,4 +1,4 @@ -// Local file is not written to disk +// Local file is written to disk const bcrypt = require('bcrypt') const jwt = require('jsonwebtoken') const uuid = require('uuid') @@ -64,7 +64,9 @@ class Authenticator { this.OTP_WINDOW = 1 // How many OTP codes can be used before and after the current one (usefull for slower people, recommended 1) this.INVALID_2FA_CODE_TEXT = "Invalid 2FA code" this.REMOVED_USER_TEXT = "User has been removed" - this.USER_ALREADY_EXISTS_TEXT = "User already exists" + this.USERNAME_ALREADY_EXISTS_TEXT = "This username already exists" + this.EMAIL_ALREADY_EXISTS_TEXT = "This email already exists" + this.USERNAME_IS_REQUIRED="Username is required" this.ALLOW_DB_DUMP = false // Allowing DB Dumping is disabled by default can be enabled by setting ALLOW_DB_DUMP to true after initializing your class // Override methods to update file when users array changes @@ -80,12 +82,22 @@ class Authenticator { - /** - * Registers a new user - * @param {object} userObject - object with required keys: email, password, wants2FA, you can add custom keys too - * @returns {object} - registered user object, or "User already exists" if user already exists - * @throws {Error} - any other error - */ + +/** + * Registers a new user. + * + * Initializes user object with default values if not provided, including login attempts, + * locked status, and unique ID. ashes the password and optionally generates a 2FA secret + * and QR code if 2FA is requested. Checks for existing user by email and returns an + * appropriate message if user already exists. Updates users list and returns the + * registered user object. + * + * @param {object} userObject - The user details containing required keys: + * username, email, password, wants2FA. Custom keys can be added like. + * If email is null or undefined, they can't use login by email. + * @returns {object|string} - The registered user object or a string "User already exists". + * @throws {Error} - Logs any error encountered during registration process. + */ async register(userObject) { if (!userObject.loginAttempts) userObject.loginAttempts = 0 if (!userObject.locked) userObject.locked = false @@ -110,27 +122,30 @@ class Authenticator { userObject.password = hash; userObject.jwt_version = 1 + if (!userObject.username) return this.USERNAME_IS_REQUIRED - if (this.users.find(u => u.email === userObject.email)) return this.USER_ALREADY_EXISTS_TEXT + if (this.users.find(u => u.username === userObject.username)) return this.USERNAME_ALREADY_EXISTS_TEXT + if (this.users.find(u => u.email === userObject.email)) return this.EMAIL_ALREADY_EXISTS_TEXT this.users.push(userObject); return returnedUser; } catch (err) { console.log(err) + } } /** * Logs in a user - * @param {string} email - email address of user + * @param {string} username - Username of user * @param {string} password - password of user * @param {number} twoFactorCode - 2FA code of user or put null if user didn't provide a 2FA * @returns {object} - user object with jwt_token, or null if login was unsuccessful, or "User is locked" if user is locked * @throws {Error} - any other error */ - async login(email, password, twoFactorCode) { - const account = this.users.find(u => u.email === email); - if (!email) return null; + async login(username, password, twoFactorCode) { + const account = this.users.find(u => u.username === username); + if (!username) return null; if (!password) return null; try { @@ -138,10 +153,10 @@ class Authenticator { if (!result) { - (account.loginAttempts >= this.maxLoginAttempts) ? this.lockUser(account.id) : await this.changeLoginAttempts(account._id, account.loginAttempts + 1) + (account.loginAttempts >= this.maxLoginAttempts) ? await this.lockUser(account.id) : await this.changeLoginAttempts(account._id, account.loginAttempts + 1) return null - }; + } if (account) { if (account.locked) return this.lockedText if (account.wants2FA) { @@ -160,7 +175,7 @@ class Authenticator { } const jwt_token = jwt.sign({ _id: account._id, version: account.jwt_version }, this.JWT_SECRET_KEY, this.JWT_OPTIONS); - this.changeLoginAttempts(account._id, 0) + await this.changeLoginAttempts(account._id, 0) return { ...account, jwt_token }; } @@ -199,7 +214,7 @@ class Authenticator { */ async verifyEmailSignin(emailCode) { if (emailCode === null) return null - const user = await this.users.find(user => user.emailCode == emailCode); + const user = await this.users.find(user => user.emailCode === emailCode); if (!user) return null; const userIndex = this.users.findIndex(u => u.emailCode === emailCode); if (userIndex !== -1) { @@ -220,15 +235,16 @@ class Authenticator { if (!user) return null; return user } + /** - * Retrieves user information based on the user email - * @param {string} email - the email to retrieve information - * @returns {object} - an object with the user information - * @throws {Error} - any error that occurs during the process + * Retrieves user information based on a custom search criteria + * @param {string} searchType - the field name to search by (e.g. username, email, etc.). + * It will only find the first element that corresponds to the specified value + * @param {string} value - the value to match in the specified field + * @returns {object} - an object with the user information or null if not found */ - - getInfoFromEmail(email) { - const user = this.users.find(u => u.email === email); + getInfoFromCustom(searchType, value) { + const user = this.users.find(u => u[searchType] === value); if (!user) return null; return user } diff --git a/file.test.js b/file.test.js index ebe5de7..e27e675 100644 --- a/file.test.js +++ b/file.test.js @@ -5,12 +5,14 @@ const speakeasy = require('speakeasy'); const fs = require('fs'); const mockUser = { + username: "test", email: "test@example.com", password: "password123", wants2FA: false, }; const mockUser2FA = { + username: "test2", email: "test2@example.com", password: "password123", wants2FA: true, @@ -39,20 +41,24 @@ describe('Authenticator Class Tests', () => { test('User Registration without 2FA', async () => { const result = await authenticator.register({ + username: "test", email: "test@example.com", password: "password123", wants2FA: false, }); + expect(result.username).toBe("test"); expect(result.email).toBe(mockUser.email); expect(result.jwt_version).toBe(1); expect(result.wants2FA).toBe(false); }); test('User Registration with 2FA', async () => { const result = await authenticator.register({ + username: "test2", email: "test2@example.com", password: "password123", wants2FA: true, }); + expect(result.username).toBe("test2"); expect(result.email).toBe(mockUser2FA.email); expect(result.jwt_version).toBe(1); expect(result.wants2FA).toBe(true); @@ -62,7 +68,7 @@ describe('Authenticator Class Tests', () => { }); test('User Login', async () => { - const loginResult = await authenticator.login(mockUser.email, mockUser.password); + const loginResult = await authenticator.login(mockUser.username, mockUser.password); userID = loginResult._id expect(loginResult.jwt_token).toBeDefined(); expect(jwt.verify(loginResult.jwt_token, JWT_SECRET)).toBeTruthy(); @@ -74,38 +80,38 @@ describe('Authenticator Class Tests', () => { secret: SECRET2FA, encoding: 'base32', }) - const loginResult = await authenticator.login(mockUser2FA.email, mockUser2FA.password, twoFactorCode); + const loginResult = await authenticator.login(mockUser2FA.username, mockUser2FA.password, twoFactorCode); userID2FA = loginResult._id expect(loginResult.jwt_token).toBeDefined(); expect(jwt.verify(loginResult.jwt_token, JWT_SECRET)).toBeTruthy(); }); test('User Login with invalid 2FA ', async () => { - const loginResult = await authenticator.login(mockUser2FA.email, mockUser2FA.password, 100000); + const loginResult = await authenticator.login(mockUser2FA.username, mockUser2FA.password, 100000); expect(loginResult.jwt_token).not.toBeDefined(); }); test('User Login with no 2FA (for a 2FA user) ', async () => { - const loginResult = await authenticator.login(mockUser2FA.email, mockUser2FA.password, 100000); + const loginResult = await authenticator.login(mockUser2FA.username, mockUser2FA.password, 100000); expect(loginResult.jwt_token).not.toBeDefined(); }); test('Login with incorrect password', async () => { - const result = await authenticator.login(mockUser.email, 'wrongpassword'); + const result = await authenticator.login(mockUser.username, 'wrongpassword'); expect(result).toBe(null); }); test('Get Info From User', async () => { const info = await authenticator.getInfoFromUser(userID) - expect(info.email).toBe(mockUser.email); + expect(info.username).toBe(mockUser.username); }) - test('Get Info From Email', async () => { - const info = await authenticator.getInfoFromEmail(mockUser.email) + test('Get Info From Custom Property', async () => { + const info = await authenticator.getInfoFromCustom("email", mockUser.email) expect(info.email).toBe(mockUser.email); }) test('Verify JWT Token', async () => { - const loginResult = await authenticator.login(mockUser.email, mockUser.password); + const loginResult = await authenticator.login(mockUser.username, mockUser.password); const tokenVerification = await authenticator.verifyToken(loginResult.jwt_token); expect(tokenVerification).toBeDefined() }); @@ -144,9 +150,9 @@ describe('Authenticator Class Tests', () => { }) test('Lock user after max login attempts', async () => { - await authenticator.login(mockUser.email, 'wrongpassword'); - await authenticator.login(mockUser.email, 'wrongpassword'); - const result = await authenticator.login(mockUser.email, 'wrongpassword'); + await authenticator.login(mockUser.username, 'wrongpassword'); + await authenticator.login(mockUser.username, 'wrongpassword'); + const result = await authenticator.login(mockUser.username, 'wrongpassword'); if (result === 'User is locked') { expect(result).toBe('User is locked'); } else { @@ -187,19 +193,44 @@ describe('Authenticator Class Tests', () => { }) - test('Check if user is authenticated', async () => { - await authenticator.register({ - email: "test@test.test", - password: "test", - wants2FA: false, + test('Check if user is authenticated', + async () => { + await authenticator.register({ + username: "test3", + email: "test3@test.test", + password: "test3", + wants2FA: false, + }) + let user = await authenticator.login("test3", "test3") + console.log(user) + + let req = { + headers: { + "host": "127.0.0.1:3000", + "connection": "keep-alive", + "cache-control": "max-age=0", + "sec-ch-ua": "\"Chromium\";v=\"130\", \"Brave\";v=\"130\", \"Not?A_Brand\";v=\"99\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"Windows\"", + "dnt": "1", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", + "sec-gpc": "1", + "accept-language": "nl-NL,nl", + "sec-fetch-site": "same-origin", + "sec-fetch-mode": "navigate", + "sec-fetch-user": "?1", + "sec-fetch-dest": "document", + "referer": "http://127.0.0.1:3000/login", + "accept-encoding": "gzip, deflate, br, zstd", + "cookie": `token=${user.jwt_token}`, + "if-none-match": "W/\"14-VDnz0WejlS4iemsxsVhn1S8IIDE\"" + } + } + let response = await authenticator.isAuthenticated(req) + expect(response).toBe(true) }) - let user = await authenticator.login("test@test.test", "test") - console.log(user) - - let req = { headers: { "host": "127.0.0.1:3000", "connection": "keep-alive", "cache-control": "max-age=0", "sec-ch-ua": "\"Chromium\";v=\"130\", \"Brave\";v=\"130\", \"Not?A_Brand\";v=\"99\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "dnt": "1", "upgrade-insecure-requests": "1", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "sec-gpc": "1", "accept-language": "nl-NL,nl", "sec-fetch-site": "same-origin", "sec-fetch-mode": "navigate", "sec-fetch-user": "?1", "sec-fetch-dest": "document", "referer": "http://127.0.0.1:3000/login", "accept-encoding": "gzip, deflate, br, zstd", "cookie": `token=${user.jwt_token}`, "if-none-match": "W/\"14-VDnz0WejlS4iemsxsVhn1S8IIDE\"" } } - let response = await authenticator.isAuthenticated(req) - expect(response).toBe(true) - }) test('Revoke All User Tokens', async () => { await authenticator.revokeUserTokens(userID) @@ -215,7 +246,7 @@ describe('Authenticator Class Tests', () => { afterAll(async () => { - console.log(await authenticator.dumpDB()) + //console.log(await authenticator.dumpDB()) fs.unlinkSync(authenticator.DB_FILE_PATH) }); diff --git a/memory.js b/memory.js index 97ee9c2..a3b7587 100644 --- a/memory.js +++ b/memory.js @@ -4,38 +4,53 @@ const jwt = require('jsonwebtoken') const uuid = require('uuid') const speakeasy = require('speakeasy') const QRCode = require('qrcode') -const fs = require('fs'); const Crypto = require('node:crypto') + class Authenticator { - constructor() { + constructor() { this.QR_LABEL = "Authenticator"; this.rounds = 12; this.JWT_SECRET_KEY = "changeme"; this.JWT_OPTIONS = { expiresIn: "1h" }; this.maxLoginAttempts = 13 this.maxLoginAttempts = this.maxLoginAttempts - 2; + this.DB_FILE_PATH = "./users.db" + this.DB_PASSWORD = "changeme" this.users = [] this.OTP_ENCODING = 'base32' this.lockedText = "User is locked" this.OTP_WINDOW = 1 // How many OTP codes can be used before and after the current one (usefull for slower people, recommended 1) this.INVALID_2FA_CODE_TEXT = "Invalid 2FA code" this.REMOVED_USER_TEXT = "User has been removed" - this.USER_ALREADY_EXISTS_TEXT = "User already exists" + this.USERNAME_ALREADY_EXISTS_TEXT = "This username already exists" + this.EMAIL_ALREADY_EXISTS_TEXT = "This email already exists" + this.USERNAME_IS_REQUIRED="Username is required" this.ALLOW_DB_DUMP = false // Allowing DB Dumping is disabled by default can be enabled by setting ALLOW_DB_DUMP to true after initializing your class + } + /** - * Registers a new user - * @param {object} userObject - object with required keys: email, password, wants2FA, you can add custom keys too - * @returns {object} - registered user object, or "User already exists" if user already exists - * @throws {Error} - any other error + * Registers a new user. + * + * Initializes user object with default values if not provided, including login attempts, + * locked status, and unique ID. ashes the password and optionally generates a 2FA secret + * and QR code if 2FA is requested. Checks for existing user by email and returns an + * appropriate message if user already exists. Updates users list and returns the + * registered user object. + * + * @param {object} userObject - The user details containing required keys: + * username, email, password, wants2FA. Custom keys can be added like. + * If email is null or undefined, they can't use login by email. + * @returns {object|string} - The registered user object or a string "User already exists". + * @throws {Error} - Logs any error encountered during registration process. */ async register(userObject) { if (!userObject.loginAttempts) userObject.loginAttempts = 0 @@ -61,38 +76,41 @@ class Authenticator { userObject.password = hash; userObject.jwt_version = 1 + if (!userObject.username) return this.USERNAME_IS_REQUIRED - if (this.users.find(u => u.email === userObject.email)) return this.USER_ALREADY_EXISTS_TEXT + if (this.users.find(u => u.username === userObject.username)) return this.USERNAME_ALREADY_EXISTS_TEXT + if (this.users.find(u => u.email === userObject.email)) return this.EMAIL_ALREADY_EXISTS_TEXT this.users.push(userObject); return returnedUser; } catch (err) { console.log(err) + } } /** * Logs in a user - * @param {string} email - email address of user + * @param {string} username - Username of user * @param {string} password - password of user * @param {number} twoFactorCode - 2FA code of user or put null if user didn't provide a 2FA * @returns {object} - user object with jwt_token, or null if login was unsuccessful, or "User is locked" if user is locked * @throws {Error} - any other error */ - async login(email, password, twoFactorCode) { - const account = this.users.find(u => u.email === email); - if (!email) return null; + async login(username, password, twoFactorCode) { + const account = this.users.find(u => u.username === username); + if (!username) return null; if (!password) return null; try { const result = await bcrypt.compare(password, account.password); - + if (!result) { - - (account.loginAttempts >= this.maxLoginAttempts) ? this.lockUser(account.id) : await this.changeLoginAttempts(account._id, account.loginAttempts + 1) - + + (account.loginAttempts >= this.maxLoginAttempts) ? await this.lockUser(account.id) : await this.changeLoginAttempts(account._id, account.loginAttempts + 1) + return null - }; + } if (account) { if (account.locked) return this.lockedText if (account.wants2FA) { @@ -111,7 +129,7 @@ class Authenticator { } const jwt_token = jwt.sign({ _id: account._id, version: account.jwt_version }, this.JWT_SECRET_KEY, this.JWT_OPTIONS); - this.changeLoginAttempts(account._id, 0) + await this.changeLoginAttempts(account._id, 0) return { ...account, jwt_token }; } @@ -150,7 +168,7 @@ class Authenticator { */ async verifyEmailSignin(emailCode) { if (emailCode === null) return null - const user = await this.users.find(user => user.emailCode == emailCode); + const user = await this.users.find(user => user.emailCode === emailCode); if (!user) return null; const userIndex = this.users.findIndex(u => u.emailCode === emailCode); if (userIndex !== -1) { @@ -171,15 +189,16 @@ class Authenticator { if (!user) return null; return user } + /** - * Retrieves user information based on the user email - * @param {string} email - the email to retrieve information - * @returns {object} - an object with the user information - * @throws {Error} - any error that occurs during the process + * Retrieves user information based on a custom search criteria + * @param {string} searchType - the field name to search by (e.g. username, email, etc.). + * It will only find the first element that corresponds to the specified value + * @param {string} value - the value to match in the specified field + * @returns {object} - an object with the user information or null if not found */ - - getInfoFromEmail(email) { - const user = this.users.find(u => u.email === email); + getInfoFromCustom(searchType, value) { + const user = this.users.find(u => u[searchType] === value); if (!user) return null; return user } diff --git a/memory.test.js b/memory.test.js index ea020b0..95ffb01 100644 --- a/memory.test.js +++ b/memory.test.js @@ -3,13 +3,16 @@ const Authenticator = require('./memory.js') const jwt = require('jsonwebtoken'); const speakeasy = require('speakeasy'); + const mockUser = { + username: "test", email: "test@example.com", password: "password123", wants2FA: false, }; const mockUser2FA = { + username: "test2", email: "test2@example.com", password: "password123", wants2FA: true, @@ -27,9 +30,10 @@ describe('Authenticator Class Tests', () => { let emailCode = "" beforeAll(async () => { - authenticator = new Authenticator() - authenticator.JWT_SECRET_KEY = JWT_SECRET + authenticator = new Authenticator(); + authenticator.rounds = 10 authenticator.ALLOW_DB_DUMP = true + authenticator.JWT_SECRET_KEY = JWT_SECRET }); @@ -37,20 +41,24 @@ describe('Authenticator Class Tests', () => { test('User Registration without 2FA', async () => { const result = await authenticator.register({ + username: "test", email: "test@example.com", password: "password123", wants2FA: false, }); + expect(result.username).toBe("test"); expect(result.email).toBe(mockUser.email); expect(result.jwt_version).toBe(1); expect(result.wants2FA).toBe(false); }); test('User Registration with 2FA', async () => { const result = await authenticator.register({ + username: "test2", email: "test2@example.com", password: "password123", wants2FA: true, }); + expect(result.username).toBe("test2"); expect(result.email).toBe(mockUser2FA.email); expect(result.jwt_version).toBe(1); expect(result.wants2FA).toBe(true); @@ -60,7 +68,7 @@ describe('Authenticator Class Tests', () => { }); test('User Login', async () => { - const loginResult = await authenticator.login(mockUser.email, mockUser.password); + const loginResult = await authenticator.login(mockUser.username, mockUser.password); userID = loginResult._id expect(loginResult.jwt_token).toBeDefined(); expect(jwt.verify(loginResult.jwt_token, JWT_SECRET)).toBeTruthy(); @@ -72,38 +80,38 @@ describe('Authenticator Class Tests', () => { secret: SECRET2FA, encoding: 'base32', }) - const loginResult = await authenticator.login(mockUser2FA.email, mockUser2FA.password, twoFactorCode); + const loginResult = await authenticator.login(mockUser2FA.username, mockUser2FA.password, twoFactorCode); userID2FA = loginResult._id expect(loginResult.jwt_token).toBeDefined(); expect(jwt.verify(loginResult.jwt_token, JWT_SECRET)).toBeTruthy(); }); test('User Login with invalid 2FA ', async () => { - const loginResult = await authenticator.login(mockUser2FA.email, mockUser2FA.password, 100000); + const loginResult = await authenticator.login(mockUser2FA.username, mockUser2FA.password, 100000); expect(loginResult.jwt_token).not.toBeDefined(); }); test('User Login with no 2FA (for a 2FA user) ', async () => { - const loginResult = await authenticator.login(mockUser2FA.email, mockUser2FA.password, 100000); + const loginResult = await authenticator.login(mockUser2FA.username, mockUser2FA.password, 100000); expect(loginResult.jwt_token).not.toBeDefined(); }); test('Login with incorrect password', async () => { - const result = await authenticator.login(mockUser.email, 'wrongpassword'); + const result = await authenticator.login(mockUser.username, 'wrongpassword'); expect(result).toBe(null); }); test('Get Info From User', async () => { const info = await authenticator.getInfoFromUser(userID) - expect(info.email).toBe(mockUser.email); + expect(info.username).toBe(mockUser.username); }) - test('Get Info From Email', async () => { - const info = await authenticator.getInfoFromEmail(mockUser.email) + test('Get Info From Custom Property', async () => { + const info = await authenticator.getInfoFromCustom("email", mockUser.email) expect(info.email).toBe(mockUser.email); }) test('Verify JWT Token', async () => { - const loginResult = await authenticator.login(mockUser.email, mockUser.password); + const loginResult = await authenticator.login(mockUser.username, mockUser.password); const tokenVerification = await authenticator.verifyToken(loginResult.jwt_token); expect(tokenVerification).toBeDefined() }); @@ -142,9 +150,9 @@ describe('Authenticator Class Tests', () => { }) test('Lock user after max login attempts', async () => { - await authenticator.login(mockUser.email, 'wrongpassword'); - await authenticator.login(mockUser.email, 'wrongpassword'); - const result = await authenticator.login(mockUser.email, 'wrongpassword'); + await authenticator.login(mockUser.username, 'wrongpassword'); + await authenticator.login(mockUser.username, 'wrongpassword'); + const result = await authenticator.login(mockUser.username, 'wrongpassword'); if (result === 'User is locked') { expect(result).toBe('User is locked'); } else { @@ -187,13 +195,14 @@ describe('Authenticator Class Tests', () => { test('Check if user is authenticated', async () => { await authenticator.register({ - email: "test@test.test", - password: "test", + username: "test3", + email: "test3@test.test", + password: "test3", wants2FA: false, }) - let user = await authenticator.login("test@test.test", "test") + let user = await authenticator.login("test3", "test3") console.log(user) - + let req = { headers: { "host": "127.0.0.1:3000", "connection": "keep-alive", "cache-control": "max-age=0", "sec-ch-ua": "\"Chromium\";v=\"130\", \"Brave\";v=\"130\", \"Not?A_Brand\";v=\"99\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "dnt": "1", "upgrade-insecure-requests": "1", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "sec-gpc": "1", "accept-language": "nl-NL,nl", "sec-fetch-site": "same-origin", "sec-fetch-mode": "navigate", "sec-fetch-user": "?1", "sec-fetch-dest": "document", "referer": "http://127.0.0.1:3000/login", "accept-encoding": "gzip, deflate, br, zstd", "cookie": `token=${user.jwt_token}`, "if-none-match": "W/\"14-VDnz0WejlS4iemsxsVhn1S8IIDE\"" } } let response = await authenticator.isAuthenticated(req) expect(response).toBe(true) @@ -210,10 +219,11 @@ describe('Authenticator Class Tests', () => { expect(response).toBe("User has been removed") }); - + afterAll(async () => { - console.log(await authenticator.dumpDB()) + authenticator.users = []; + console.log("Done") }); }); diff --git a/mongodb.js b/mongodb.js index e50bf4c..8aaaa34 100644 --- a/mongodb.js +++ b/mongodb.js @@ -18,7 +18,7 @@ class Authenticator { this.QR_LABEL = "Authenticator"; this.rounds = 12; this.JWT_SECRET_KEY = "changeme"; - this.JWT_OPTIONS = { expiresIn: "1h" }; + this.JWT_OPTIONS = {expiresIn: "1h"}; this.maxLoginAttempts = 3; mongoose.connect(MONGODB_CONNECTION_STRING); this.User = mongoose.model('User', userSchema) @@ -28,6 +28,9 @@ class Authenticator { this.INVALID_2FA_CODE_TEXT = "Invalid 2FA code" this.REMOVED_USER_TEXT = "User has been removed" this.USER_ALREADY_EXISTS_TEXT = "User already exists" + this.USERNAME_ALREADY_EXISTS_TEXT = "This username already exists" + this.EMAIL_ALREADY_EXISTS_TEXT = "This email already exists" + this.USERNAME_IS_REQUIRED = "Username is required" this.ALLOW_DB_DUMP = false // Allowing DB Dumping is disabled by default can be enabled by setting ALLOW_DB_DUMP to true after initializing your class } @@ -39,6 +42,9 @@ class Authenticator { * @throws {Error} - any other error */ async register(userObject) { + if (!userObject.username) return this.USERNAME_IS_REQUIRED + const existingUser = await this.User.findOne({username: userObject.username}); + if (existingUser) return this.USERNAME_ALREADY_EXISTS_TEXT try { const hash = await bcrypt.hashSync(userObject.password, this.rounds); let newUser = new this.User({ @@ -48,7 +54,7 @@ class Authenticator { }); if (userObject.wants2FA) { - const secret = speakeasy.generateSecret({ name: this.QR_LABEL }); + const secret = speakeasy.generateSecret({name: this.QR_LABEL}); const otpauth_url = speakeasy.otpauthURL({ secret: secret.base32, label: this.QR_LABEL, @@ -77,14 +83,14 @@ class Authenticator { /** * Logs in a user - * @param {string} email - email address of the user + * @param {string} username - username of the user * @param {string} password - password of the user * @param {number} twoFactorCode - 2FA code if user has 2FA enabled - * @returns {object} - logged in user object with JWT or qrCode if user has 2FA enabled + * @returns {object} - logged-in user object with JWT or qrCode if user has 2FA enabled * @throws {Error} - any other error */ - async login(email, password, twoFactorCode) { - const user = await this.User.findOne({ email }); + async login(username, password, twoFactorCode) { + const user = await this.User.findOne({username: username}); if (!user) return null; try { @@ -99,7 +105,8 @@ class Authenticator { await this.changeLoginAttempts(user._id, newAttempts); } return null; - }; + } + ; if (user) { if (user.locked) return this.lockedText if (user.wants2FA) { @@ -111,7 +118,7 @@ class Authenticator { encoding: this.OTP_ENCODING }); const qrCode = await QRCode.toDataURL(otpauth_url); - return { qrCode }; + return {qrCode}; } const verified = speakeasy.totp.verify({ @@ -123,16 +130,21 @@ class Authenticator { if (!verified) return this.INVALID_2FA_CODE_TEXT; } - const jwt_token = jwt.sign({ _id: user._id, version: user.jwt_version }, this.JWT_SECRET_KEY, this.JWT_OPTIONS); + const jwt_token = jwt.sign({ + _id: user._id, + version: user.jwt_version + }, this.JWT_SECRET_KEY, this.JWT_OPTIONS); this.changeLoginAttempts(user._id, 0) + console.log({...user.toObject(), jwt_token}) - return { ...user.toObject(), jwt_token }; + return {...user.toObject(), jwt_token}; } } catch (err) { throw err; } } + /** * Generates a one time password and stores it in the user record. This is used * for passwordless login, where the user will receive this code and can login @@ -145,14 +157,15 @@ class Authenticator { async registerEmailSignin(email) { let emailCode = Crypto.randomUUID() try { - if (await this.User.findOne({ email: email }).locked) return this.lockedText - await this.User.findOneAndUpdate({ email: email }, { emailCode: emailCode }) - return { emailCode } + if (await this.User.findOne({email: email}).locked) return this.lockedText + await this.User.findOneAndUpdate({email: email}, {emailCode: emailCode}) + return {emailCode} } catch (error) { console.error(error) } } + /** * Verifies a emailCode and returns a valid JWT token for the user * @param {string} emailCode - the emailCode to verify @@ -162,19 +175,20 @@ class Authenticator { async verifyEmailSignin(emailCode) { if (!emailCode || typeof emailCode !== 'string') return null; - const user = await this.User.findOne({ emailCode }); + const user = await this.User.findOne({emailCode}); if (!user) return null; - await this.User.findOneAndUpdate({ emailCode }, { emailCode: "" }); + await this.User.findOneAndUpdate({emailCode}, {emailCode: ""}); const jwt_token = jwt.sign( - { _id: user._id, version: user.jwt_version }, + {_id: user._id, version: user.jwt_version}, this.JWT_SECRET_KEY, this.JWT_OPTIONS ); - return { ...user.toObject(), jwt_token }; + return {...user.toObject(), jwt_token}; } + /** * Retrieves user information based on the user ID * @param {string} userId - the user ID to retrieve information @@ -182,16 +196,18 @@ class Authenticator { * @throws {Error} - any error that occurs during the process */ async getInfoFromUser(userId) { - return await this.User.findOne({ _id: userId }); + return await this.User.findOne({_id: userId}); } + /** * Retrieves user information based on the user's email address - * @param {string} email - the email address to retrieve information + * @param {string} searchType -Type type to search for + * @param {string} value - The value to search for * @returns {object} - an object with the user information * @throws {Error} - any error that occurs during the process */ - async getInfoFromEmail(email) { - return await this.User.findOne({ email: email }); + async getInfoFromCustom(searchType, value) { + return this.User.findOne({[searchType]: value}); } /** @@ -214,6 +230,7 @@ class Authenticator { } } + /** * Verifies a 2FA code for a user * @param {string} userId - the user ID to verify the 2FA code for @@ -221,7 +238,7 @@ class Authenticator { * @returns {boolean} - true if the code is valid, false otherwise */ async verify2FA(userId, twofactorcode) { - let user = await this.User.findOne({ _id: userId }) + let user = await this.User.findOne({_id: userId}) if (!user) return null const verified = speakeasy.totp.verify({ secret: user.secret2FA, @@ -232,6 +249,7 @@ class Authenticator { return verified; } + /** * Resets the password of a user * @param {string} userId - the user ID to reset the password for @@ -242,9 +260,10 @@ class Authenticator { async resetPassword(userId, newPassword) { this.revokeUserTokens(userId) const hash = await bcrypt.hashSync(newPassword, this.rounds); - return await this.User.findOneAndUpdate({ _id: userId }, { password: hash }, { new: true }) + return await this.User.findOneAndUpdate({_id: userId}, {password: hash}, {new: true}) } + /** * Changes the number of login attempts for a user * @param {string} userId - the user ID to change the login attempts for @@ -253,9 +272,10 @@ class Authenticator { * @throws {Error} - any error that occurs during the process */ async changeLoginAttempts(userId, attempts) { - return await this.User.findOneAndUpdate({ _id: userId }, { loginAttempts: attempts }, { new: true }); + return await this.User.findOneAndUpdate({_id: userId}, {loginAttempts: attempts}, {new: true}); } + /** * Locks a user from logging in * @param {string} userId - the user ID to lock @@ -263,8 +283,9 @@ class Authenticator { * @throws {Error} - any error that occurs during the process */ async lockUser(userId) { - return await this.User.findOneAndUpdate({ _id: userId }, { locked: true }, { new: true }); + return await this.User.findOneAndUpdate({_id: userId}, {locked: true}, {new: true}); } + /** * Unlocks a user from logging in * @param {string} userId - the user ID to unlock @@ -272,7 +293,7 @@ class Authenticator { * @throws {Error} - any error that occurs during the process */ async unlockUser(userId) { - return await this.User.findOneAndUpdate({ _id: userId }, { locked: false }, { new: true }); + return await this.User.findOneAndUpdate({_id: userId}, {locked: false}, {new: true}); } /** @@ -282,9 +303,10 @@ class Authenticator { * @throws {Error} - any error that occurs during the process */ async revokeUserTokens(userId) { - let newVersion = (await this.User.findOne({ _id: userId })).jwt_version + 1 - return await this.User.findOneAndUpdate({ _id: userId }, { jwt_version: newVersion }, { new: false }); + let newVersion = (await this.User.findOne({_id: userId})).jwt_version + 1 + return await this.User.findOneAndUpdate({_id: userId}, {jwt_version: newVersion}, {new: false}); } + /** * Removes 2FA for a user * @param {string} userId - the user ID to remove 2FA for @@ -293,11 +315,12 @@ class Authenticator { */ async remove2FA(userId) { return await this.User.findOneAndUpdate( - { _id: userId }, - { wants2FA: false, secret2FA: "", qrCode: "" }, - { new: true } + {_id: userId}, + {wants2FA: false, secret2FA: "", qrCode: ""}, + {new: true} ); } + /** * Adds 2FA for a user * @param {string} userId - the user ID to add 2FA for @@ -305,10 +328,10 @@ class Authenticator { * @throws {Error} - any error that occurs during the process */ async add2FA(userId) { - const user = await this.User.findOne({ _id: userId }); + const user = await this.User.findOne({_id: userId}); if (!user) return null; - const secret = speakeasy.generateSecret({ name: this.QR_LABEL }); + const secret = speakeasy.generateSecret({name: this.QR_LABEL}); const otpauth_url = speakeasy.otpauthURL({ secret: secret.base32, @@ -318,11 +341,12 @@ class Authenticator { const qrCode = await QRCode.toDataURL(otpauth_url); return await this.User.findOneAndUpdate( - { _id: userId }, - { wants2FA: true, secret2FA: secret.base32, qrCode }, - { new: true } + {_id: userId}, + {wants2FA: true, secret2FA: secret.base32, qrCode}, + {new: true} ); } + /** * Removes a user from the database * @param {string} userId - the user ID to remove @@ -331,12 +355,13 @@ class Authenticator { */ async removeUser(userId) { try { - await this.User.findOneAndDelete({ _id: userId }); + await this.User.findOneAndDelete({_id: userId}); return this.REMOVED_USER_TEXT } catch (error) { return `User with ID ${userId} couldn't be removed` } } + /** * Retrieves all users from the database * @returns {object[]} - an array of user objects diff --git a/mongodb.test.js b/mongodb.test.js index 2c7b8d7..a425948 100644 --- a/mongodb.test.js +++ b/mongodb.test.js @@ -7,6 +7,7 @@ const speakeasy = require('speakeasy'); // Mock the user schema and Mongoose model const userSchema = new mongoose.Schema({ + username: String, email: String, password: String, jwt_version: Number, @@ -18,12 +19,14 @@ const userSchema = new mongoose.Schema({ }); const mockUser = { + username: "test", email: "test@example.com", password: "password123", wants2FA: false, }; const mockUser2FA = { + username: "test2", email: "test2@example.com", password: "password123", wants2FA: true, @@ -43,6 +46,7 @@ describe('Authenticator Class Tests', () => { let userToken1 = "" let emailCode = "" + beforeAll(async () => { authenticator = new Authenticator(MONGODB_CONNECTION_STRING, userSchema) authenticator.rounds = 10 @@ -70,7 +74,7 @@ describe('Authenticator Class Tests', () => { }); test('User Login', async () => { - const loginResult = await authenticator.login(mockUser.email, mockUser.password); + const loginResult = await authenticator.login(mockUser.username, mockUser.password); userID = loginResult._id expect(loginResult.jwt_token).toBeDefined(); expect(jwt.verify(loginResult.jwt_token, JWT_SECRET)).toBeTruthy(); @@ -82,23 +86,23 @@ describe('Authenticator Class Tests', () => { secret: SECRET2FA, encoding: 'base32', }) - const loginResult = await authenticator.login(mockUser2FA.email, mockUser2FA.password, twoFactorCode); + const loginResult = await authenticator.login(mockUser2FA.username, mockUser2FA.password, twoFactorCode); userID2FA = loginResult._id expect(loginResult.jwt_token).toBeDefined(); expect(jwt.verify(loginResult.jwt_token, JWT_SECRET)).toBeTruthy(); }); test('User Login with invalid 2FA ', async () => { - const loginResult = await authenticator.login(mockUser2FA.email, mockUser2FA.password, 100000); + const loginResult = await authenticator.login(mockUser2FA.username, mockUser2FA.password, 100000); expect(loginResult.jwt_token).not.toBeDefined(); }); test('User Login with no 2FA (for a 2FA user) ', async () => { - const loginResult = await authenticator.login(mockUser2FA.email, mockUser2FA.password, 100000); + const loginResult = await authenticator.login(mockUser2FA.username, mockUser2FA.password, 100000); expect(loginResult.jwt_token).not.toBeDefined(); }); test('Login with incorrect password', async () => { - const result = await authenticator.login(mockUser.email, 'wrongpassword'); + const result = await authenticator.login(mockUser.username, 'wrongpassword'); expect(result).toBe(null); }); @@ -107,13 +111,13 @@ describe('Authenticator Class Tests', () => { expect(info.email).toBe(mockUser.email); }) - test('Get Info From Email', async () => { - const info = await authenticator.getInfoFromEmail(mockUser.email) + test('Get Info From Custom Field (e.g. email)', async () => { + const info = await authenticator.getInfoFromCustom("email", mockUser.email) expect(info.email).toBe(mockUser.email); }) test('Verify JWT Token', async () => { - const loginResult = await authenticator.login(mockUser.email, mockUser.password); + const loginResult = await authenticator.login(mockUser.username, mockUser.password); const tokenVerification = await authenticator.verifyToken(loginResult.jwt_token); expect(tokenVerification).toBeDefined() }); @@ -152,9 +156,9 @@ describe('Authenticator Class Tests', () => { }) test('Lock user after max login attempts', async () => { - await authenticator.login(mockUser.email, 'wrongpassword'); - await authenticator.login(mockUser.email, 'wrongpassword'); - const result = await authenticator.login(mockUser.email, 'wrongpassword'); + await authenticator.login(mockUser.username, 'wrongpassword'); + await authenticator.login(mockUser.username, 'wrongpassword'); + const result = await authenticator.login(mockUser.username, 'wrongpassword'); if (result === 'User is locked') { expect(result).toBe('User is locked'); } else { @@ -210,11 +214,12 @@ describe('Authenticator Class Tests', () => { test('Check if user is authenticated', async () => { await authenticator.register({ - email: "test@test.test", - password: "test", + username: "test3", + email: "test3@test.test", + password: "test3", wants2FA: false, }) - let user = await authenticator.login("test@test.test", "test") + let user = await authenticator.login("test3", "test3") console.log(user) let req = { headers: { "host": "127.0.0.1:3000", "connection": "keep-alive", "cache-control": "max-age=0", "sec-ch-ua": "\"Chromium\";v=\"130\", \"Brave\";v=\"130\", \"Not?A_Brand\";v=\"99\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "dnt": "1", "upgrade-insecure-requests": "1", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "sec-gpc": "1", "accept-language": "nl-NL,nl", "sec-fetch-site": "same-origin", "sec-fetch-mode": "navigate", "sec-fetch-user": "?1", "sec-fetch-dest": "document", "referer": "http://127.0.0.1:3000/login", "accept-encoding": "gzip, deflate, br, zstd", "cookie": `token=${user.jwt_token}`, "if-none-match": "W/\"14-VDnz0WejlS4iemsxsVhn1S8IIDE\"" } } diff --git a/package-lock.json b/package-lock.json index a027278..16b2e64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "seamless-auth", - "version": "3.8.4", + "version": "3.8.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "seamless-auth", - "version": "3.8.4", + "version": "3.8.5", "dependencies": { "bcrypt": "^5.0.1", "dotenv": "^16.4.5", diff --git a/package.json b/package.json index bdd6e82..7570eeb 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "3.8.5", "description": "A full fledged authentication system...", "type": "commonjs", - "main": "memory.js", + "main": "file.js", "scripts": { "start": "node test/test.js", "test": "npx jest --forceExit *.test.js"