Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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.

Expand Down
101 changes: 55 additions & 46 deletions file.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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 };
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand Down
Loading
Loading