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 b2617f7..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') @@ -48,35 +48,30 @@ 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) 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 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); @@ -87,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 @@ -117,38 +122,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) { @@ -167,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 }; } @@ -206,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) { @@ -227,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 d34329f..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, @@ -28,10 +30,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 }); @@ -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,8 +246,8 @@ describe('Authenticator Class Tests', () => { afterAll(async () => { - console.log(await authenticator.dumpDB()) - fs.unlinkSync("./app.db") + //console.log(await authenticator.dumpDB()) + fs.unlinkSync(authenticator.DB_FILE_PATH) }); }); diff --git a/memory.js b/memory.js index 7d305bc..a3b7587 100644 --- a/memory.js +++ b/memory.js @@ -4,37 +4,31 @@ 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') -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.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 @@ -42,11 +36,21 @@ 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 @@ -72,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) { @@ -122,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 }; } @@ -161,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) { @@ -182,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 681407f..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,10 +30,10 @@ describe('Authenticator Class Tests', () => { let emailCode = "" beforeAll(async () => { - authenticator = new Authenticator( - 'TestApp', 10, JWT_SECRET, { expiresIn: '1h' }, 3, [] - ); + authenticator = new Authenticator(); + authenticator.rounds = 10 authenticator.ALLOW_DB_DUMP = true + authenticator.JWT_SECRET_KEY = JWT_SECRET }); @@ -38,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); @@ -61,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(); @@ -73,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() }); @@ -143,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 { @@ -188,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) @@ -211,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 3f18059..8aaaa34 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' @@ -32,9 +28,13 @@ 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 } + /** * Registers a new user * @param {object} userObject - object with required keys: email, password, wants2FA, you can add custom keys too @@ -42,8 +42,11 @@ 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.hash(userObject.password, this.salt); + const hash = await bcrypt.hashSync(userObject.password, this.rounds); let newUser = new this.User({ ...userObject, password: hash, @@ -51,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, @@ -80,20 +83,20 @@ 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 { const result = await bcrypt.compare(password, user.password); if (!result) { - + if (user.loginAttempts >= this.maxLoginAttempts) { this.lockUser(user._id); @@ -102,7 +105,8 @@ class Authenticator { await this.changeLoginAttempts(user._id, newAttempts); } return null; - }; + } + ; if (user) { if (user.locked) return this.lockedText if (user.wants2FA) { @@ -114,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({ @@ -126,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 @@ -148,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 @@ -165,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 @@ -185,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}); } /** @@ -217,6 +230,7 @@ class Authenticator { } } + /** * Verifies a 2FA code for a user * @param {string} userId - the user ID to verify the 2FA code for @@ -224,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, @@ -235,6 +249,7 @@ class Authenticator { return verified; } + /** * Resets the password of a user * @param {string} userId - the user ID to reset the password for @@ -244,10 +259,11 @@ class Authenticator { */ async resetPassword(userId, newPassword) { this.revokeUserTokens(userId) - const hash = await bcrypt.hash(newPassword, this.salt); - return await this.User.findOneAndUpdate({ _id: userId }, { password: hash }, { new: true }) + const hash = await bcrypt.hashSync(newPassword, this.rounds); + 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 @@ -256,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 @@ -266,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 @@ -275,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}); } /** @@ -285,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 @@ -296,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 @@ -308,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, @@ -321,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 @@ -334,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 d9e04e8..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,10 +46,11 @@ describe('Authenticator Class Tests', () => { let userToken1 = "" 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 }); @@ -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 eb7936a..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", @@ -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..7570eeb 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "seamless-auth", - "version": "3.8.4", + "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"