Skip to content

Commit

Permalink
made using it easier, by lowering the initialisating protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
MattPlayGamez committed Dec 23, 2024
1 parent ba758e4 commit 61bb06b
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 161 deletions.
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
64 changes: 40 additions & 24 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 @@ -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
Expand All @@ -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
Expand All @@ -110,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 @@ -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 };
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand Down
81 changes: 56 additions & 25 deletions file.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ const speakeasy = require('speakeasy');
const fs = require('fs');

const mockUser = {
username: "test",
email: "[email protected]",
password: "password123",
wants2FA: false,
};

const mockUser2FA = {
username: "test2",
email: "[email protected]",
password: "password123",
wants2FA: true,
Expand Down Expand Up @@ -39,20 +41,24 @@ describe('Authenticator Class Tests', () => {

test('User Registration without 2FA', async () => {
const result = await authenticator.register({
username: "test",
email: "[email protected]",
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: "[email protected]",
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);
Expand All @@ -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();
Expand All @@ -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()
});
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -187,19 +193,44 @@ describe('Authenticator Class Tests', () => {

})

test('Check if user is authenticated', async () => {
await authenticator.register({
email: "[email protected]",
password: "test",
wants2FA: false,
test('Check if user is authenticated',
async () => {
await authenticator.register({
username: "test3",
email: "[email protected]",
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("[email protected]", "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)
Expand All @@ -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)
});

Expand Down
Loading

0 comments on commit 61bb06b

Please sign in to comment.