now typescript

This commit is contained in:
2024-02-03 00:35:29 +01:00
parent 3298f85120
commit a8cce75725
24 changed files with 4801 additions and 269 deletions
+7
View File
@@ -0,0 +1,7 @@
import PassCheck from "./passwordcheck";
import JwtAuth from "jsonwebtoken";
import PassPolicy from "./passpolicy";
import RateLimit from "./ratelimit";
import PasswordGenerator from "./passgen";
export { PassCheck, JwtAuth, PassPolicy, RateLimit, PasswordGenerator };
+138
View File
@@ -0,0 +1,138 @@
import JwtAuth from "./jwt";
describe('JWT', () => {
const jwt = new JwtAuth('secret');
const user: {username: string, password: string} = {
username: 'test',
password: 'test'
};
const settings: {expiresIn: string} = {
expiresIn: '1h'
};
it('should generate a JWT', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
expect(token).toBeDefined();
});
it('should verify a JWT', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
const verified = jwt.verifyJWT(token);
//check if username inside the token is equal to the username of the user check this in two lines of code
expect(verified).toBeDefined();
if (typeof verified !== 'string') {
// verified is JwtPayload
if(verified == null) fail('Token verification failed');
expect(verified.username).toEqual(user.username);
} else {
// Handle the case where verified is a string (token is blacklisted or invalid)
fail('Token verification failed');
}
});
it('should decode a JWT', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
const decoded = jwt.decodeJWT(token);
expect(decoded).toBeDefined();
if (typeof decoded !== 'string' && decoded !== null) {
// verified is JwtPayload
expect(decoded.username).toEqual(user.username);
} else {
// Handle the case where verified is a string (token is blacklisted or invalid)
fail('Token verification failed');
}
});
it('should get the expiration date of a JWT', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password }, settings);
const expirationDate = jwt.getJWTExpirationDate(token);
expect(expirationDate).toBeDefined();
});
it('should check if a JWT is expired', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password }, settings);
const isExpired = jwt.isJWTExpired(token);
expect(isExpired).toBeDefined();
});
it('should refresh a JWT', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
const newToken = jwt.refreshJWT(token);
expect(newToken).toBeDefined();
});
it('should not verify a JWT with invalid secret key', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
const verified = jwt.verifyJWT(token, 'invalid');
expect(verified).toEqual({ valid: false, message: "Token is invalid."});
});
it('should not refresh a JWT with invalid secret key', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password }, settings, 'test');
const newToken = jwt.refreshJWT(token, {}, 'invalid');
expect(newToken).toEqual({ valid: false, message: "Token is invalid."});
});
it('should blacklist a JWT', () => {
const token = jwt.generateJWT({ username: 'test12', password: user.password });
const blacklisted = jwt.BlackListJWT(token);
expect(blacklisted).toBeDefined();
});
it('should not verify a blacklisted JWT', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
jwt.BlackListJWT(token);
const verified = jwt.verifyJWT(token);
expect(verified).toBeDefined();
});
it('should not decode a blacklisted JWT', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
jwt.BlackListJWT(token);
const decoded = jwt.decodeJWT(token);
expect(decoded).toBeDefined();
});
it('should not get the expiration date of a blacklisted JWT', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
jwt.BlackListJWT(token);
const expirationDate = jwt.getJWTExpirationDate(token);
expect(expirationDate).toBeDefined();
});
it('should not check if a blacklisted JWT is expired', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
jwt.BlackListJWT(token);
const isExpired = jwt.isJWTExpired(token);
expect(isExpired).toBeDefined();
});
it('should not refresh a blacklisted JWT', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
jwt.BlackListJWT(token);
const newToken = jwt.refreshJWT(token);
expect(newToken).toBeDefined();
});
it('should not blacklist a blacklisted JWT', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
jwt.BlackListJWT(token);
const blacklisted = jwt.BlackListJWT(token);
expect(blacklisted).toBeDefined();
});
it('should not blacklist a blacklisted JWT', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
const verify = jwt.verifyJWT(token, 'invalid');
const blacklisted = jwt.IsBlackListed(token);
expect(blacklisted).toEqual({ valid: true, message: "Token is blacklisted." })
});
it('should remove a JWT from the blacklist', () => {
const token = jwt.generateJWT({ username: user.username, password: user.password });
jwt.BlackListJWT(token);
const removed = jwt.RemoveFromBlackList(token);
expect(removed).toEqual({ valid: true, message: "Token successfully removed from blacklist." });
});
});
+106
View File
@@ -0,0 +1,106 @@
import * as jwt from 'jsonwebtoken';
export default class JwtAuth{
private JWTSecretKey: string;
private blacklist: string[];
public constructor(JWTSecretKey: string){
this.JWTSecretKey = JWTSecretKey;
this.blacklist = [];
}
generateJWT(payload: { [key: string]: any}, settings: jwt.SignOptions = {}, secretKey = this.JWTSecretKey): string {
return jwt.sign(payload, secretKey, settings);
}
verifyJWT(token: string, secretKey = this.JWTSecretKey) {
try{
if(this.blacklist.includes(token)) return { valid: false, message: "Token is blacklisted." };
const vertoken = jwt.verify(token, secretKey);
if(vertoken instanceof Object) {
return vertoken;
} else {
return undefined;
}
} catch (error) {
return { valid: false, message: "Token is invalid." };
}
}
decodeJWT(token: string) {
if(this.blacklist.includes(token)) return { valid: false, message: "Token is blacklisted." };
return jwt.decode(token);
}
getJWTExpirationDate(token: string) {
if(this.blacklist.includes(token)) return { valid: false, message: "Token is blacklisted." };
const decoded = this.decodeJWT(token) as { [key: string]: any };
return decoded.exp;
}
isJWTExpired(token: string) {
if(this.blacklist.includes(token)) return { valid: false, message: "Token is blacklisted." };
const expirationDate = this.getJWTExpirationDate(token);
return expirationDate < Date.now();
}
refreshJWT(token: string, settings: jwt.SignOptions = {}, secretKey = this.JWTSecretKey) {
if(this.blacklist.includes(token)) return { valid: false, message: "Token is blacklisted." };
const decoded = this.verifyJWT(token, secretKey);
if(decoded instanceof Object) {
if(decoded.valid == false) return { valid: false, message: "Token is invalid." };
let payLoadArray: { [key: string]: any } = {};
if (decoded instanceof Object) {
payLoadArray = decoded as { [key: string]: any };
} else {
return { valid: false, message: "Token is invalid." };
}
const newToken = this.generateJWT(payLoadArray, settings, secretKey);
return newToken;
}
return { valid: false, message: "Token is invalid." };
}
BlackListJWT(token: string) {
//check if token is already blacklisted
if (this.blacklist.includes(token)) {
return { valid: false, message: "Token is already blacklisted." };
}
//add token to blacklist
this.blacklist.push(token);
return { valid: true, message: "Token successfully blacklisted."};
}
ClearBlackList() {
//clear blacklist
this.blacklist = [];
return { valid: true, message: "Blacklist successfully cleared."};
}
GetBlackList() {
//return blacklist
return this.blacklist;
}
RemoveFromBlackList(token: string) {
//remove token from blacklist
if (this.blacklist.includes(token)) {
this.blacklist = this.blacklist.filter((item) => item !== token);
return { valid: true, message: "Token successfully removed from blacklist."};
}
return { valid: false, message: "Token is not blacklisted." };
}
IsBlackListed(token: string) {
//check if token is blacklisted
if (this.blacklist.includes(token)) {
return { valid: true, message: "Token is blacklisted." };
}
return { valid: false, message: "Token is not blacklisted." };
}
}
+43
View File
@@ -0,0 +1,43 @@
import PassCheck from "./passwordcheck";
describe('PassCheck', () => {
const passCheck = new PassCheck(10, {
minLength: 6,
maxLength: 32,
minLower: 2,
minUpper: 2,
minNum: 2,
minSpecial: 1,
specialChars: "!@#$%^&*()_+~`|}{[]:;?><,./-=",
});
const pass = "Test123!";
it('should hash a password', async () => {
const hash = await passCheck.hashPassword(pass);
expect(hash).toBeDefined();
});
it('should verify a password', async () => {
const hash = await passCheck.hashPassword(pass);
const result = await passCheck.verifyPassword(pass, hash);
expect(result).toBe(true);
});
it('should not verify a wrong password', async () => {
const hash = await passCheck.hashPassword(pass);
const result = await passCheck.verifyPassword("wrongpass", hash);
expect(result).toBe(false);
});
it('should not verify a wrong hash', async () => {
const result = await passCheck.verifyPassword(pass, "wronghash");
expect(result).toBe(false);
});
it('should not verify a wrong password and hash', async () => {
const result = await passCheck.verifyPassword("wrongpass",
"wronghash");
expect(result).toBe(false);
});
});
+23
View File
@@ -0,0 +1,23 @@
import PasswordGenerator from "./passgen";
describe('PasswordGenerator', () => {
const passgen = new PasswordGenerator({
minLength: 6,
maxLength: 32,
minLower: 2,
minUpper: 2,
minNum: 2,
minSpecial: 3,
specialChars: "!@#$%^&*()_+~`|}{[]:;?><,./-=",
});
it('should generate a password', async () => {
const pass = await passgen.Generate();
expect(pass).toBeDefined();
});
it('should generate a password with a specific length', async () => {
const pass = await passgen.Generate(12);
expect(pass.length).toEqual(12);
});
});
+59
View File
@@ -0,0 +1,59 @@
export default class PasswordGenerator{
private options: { minLength: number, maxLength: number, minLower: number, minUpper: number, minNum: number, minSpecial: number, specialChars: string };
constructor(options: { minLength: number, maxLength: number, minLower: number, minUpper: number, minNum: number, minSpecial: number, specialChars: string }){
const defaultOptions = {
minLength: 6,
maxLength: 32,
minLower: 2,
minUpper: 2,
minNum: 2,
minSpecial: 3,
specialChars: "!@#$%^&*()_+~`|}{[]:;?><,./-=",
};
this.options = { ...defaultOptions, ...options };
}
Generate(length: number = 0){
// Generate random password that complies with the options
const { minLength, maxLength, minLower, minUpper, minNum, minSpecial, specialChars } = this.options;
let pass = "";
let lowerRegex = 'abcdefghijklmnopqrstuvwxyz';
let upperRegex = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
let numRegex = '0123456789';
// Generate password
//generate random length
if(length === undefined || length < minLength || length > maxLength || length === 0){
length = Math.floor(Math.random() * (maxLength - minLength + 1) + minLength);
}
//generate random length for each character type
let lowerLength = Math.floor(Math.random() * (length - minLower - minUpper - minNum - minSpecial + 1) + minLower);
let upperLength = Math.floor(Math.random() * (length - lowerLength - minUpper - minNum - minSpecial + 1) + minUpper);
let numLength = Math.floor(Math.random() * (length - lowerLength - upperLength - minNum - minSpecial + 1) + minNum);
let specialLength = Math.floor(Math.random() * (length - lowerLength - upperLength - numLength - minSpecial + 1) + minSpecial);
//generate random characters
for(let i = 0; i < lowerLength; i++){
pass += lowerRegex[Math.floor(Math.random() * lowerRegex.length)];
}
for(let i = 0; i < upperLength; i++){
pass += upperRegex[Math.floor(Math.random() * upperRegex.length)];
}
for(let i = 0; i < numLength; i++){
pass += numRegex[Math.floor(Math.random() * numRegex.length)];
}
for(let i = 0; i < specialLength; i++){
pass += specialChars[Math.floor(Math.random() * specialChars.length)];
}
//shuffle password
pass = pass.split('').sort(function(){return 0.5-Math.random()}).join('');
return pass;
}
}
+26
View File
@@ -0,0 +1,26 @@
import PassPolicy from "./passpolicy";
describe("PassPolicy", () => {
const passPolicy = new PassPolicy({
minLength: 6,
maxLength: 32,
minLower: 2,
minUpper: 2,
minNum: 2,
minSpecial: 1,
specialChars: "!@#$%^&*()_+~`|}{[]:;?><,./-=",
});
it("should validate a password", () => {
const pass = "TesT123!";
const result = passPolicy.validate(pass);
expect(result.valid).toBe(true);
});
it("should check the difference between two passwords", () => {
const pass1 = "Test123!";
const pass2 = "Test123456!";
const result = passPolicy.CheckDifference(pass1, pass2 , 4);
expect(result.valid).toBe(true);
});
});
+81
View File
@@ -0,0 +1,81 @@
export default class PassPolicy {
private options: { minLength: number, maxLength: number, minLower: number, minUpper: number, minNum: number, minSpecial: number, specialChars: string };
constructor(options: { minLength: number, maxLength: number, minLower: number, minUpper: number, minNum: number, minSpecial: number, specialChars: string }){
const defaultOptions = {
minLength: 6,
maxLength: 32,
minLower: 2,
minUpper: 2,
minNum: 2,
minSpecial: 3,
specialChars: "!@#$%^&*()_+~`|}{[]:;?><,./-=",
};
this.options = { ...defaultOptions, ...options };
}
validate(password: string) {
const { minLength, maxLength, minLower, minUpper, minNum, minSpecial, specialChars } = this.options;
if (password.length < minLength || password.length > maxLength) {
return { valid: false, message: "Password length does not meet requirements." };
}
const lowerRegex = /[a-z]/g;
const upperRegex = /[A-Z]/g;
const numRegex = /[0-9]/g;
const specialRegex = new RegExp(`[${specialChars.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}]`, "g");
const lowerCount = (password.match(lowerRegex) || []).length;
const upperCount = (password.match(upperRegex) || []).length;
const numCount = (password.match(numRegex) || []).length;
const specialCount = (password.match(specialRegex) || []).length;
if (lowerCount < minLower) {
return { valid: false, message: "Password must contain at least " + minLower + " lowercase letter(s)." };
}
if (upperCount < minUpper) {
return { valid: false, message: "Password must contain at least " + minUpper + " uppercase letter(s)." };
}
if (numCount < minNum) {
return { valid: false, message: "Password must contain at least " + minNum + " digit(s)." };
}
if (specialCount < minSpecial) {
return { valid: false, message: "Password must contain at least " + minSpecial + " special character(s)." };
}
return { valid: true };
}
CheckDifference(newPassword: string, oldPassword: string, neededDifference: number = 3) {
//check if new password is different from old password
if (newPassword === oldPassword) {
return { valid: false, message: "New password must be different from old password." };
}
//check how many characters are different
let diffCount = 0;
//check if new password is longer than old password
if (newPassword.length > oldPassword.length) {
for (let i = 0; i < oldPassword.length; i++) {
if (newPassword[i] != oldPassword[i]) {
diffCount++;
}
}
diffCount += newPassword.length - oldPassword.length;
} else {
for (let i = 0; i < newPassword.length; i++) {
if (newPassword[i] != oldPassword[i]) {
diffCount++;
}
}
diffCount += oldPassword.length - newPassword.length;
}
//check if difference is enough
if (diffCount < neededDifference) {
return { valid: false, message: "New password must be different from old password by at least " + neededDifference + " characters." };
}
return { valid: true };
}
}
+21
View File
@@ -0,0 +1,21 @@
import bcrypt from 'bcrypt';
import PassPolicy from './passpolicy';
export default class PassCheck{
private BcryptSaltRounds: number;
private PassPolicy: PassPolicy;
constructor(BcryptSaltRounds: number, PassPolicyOptions: { minLength: number, maxLength: number, minLower: number, minUpper: number, minNum: number, minSpecial: number, specialChars: string }) {
this.BcryptSaltRounds = BcryptSaltRounds;
this.PassPolicy = new PassPolicy(PassPolicyOptions);
}
async verifyPassword(password: string, hash: string) {
return await bcrypt.compare(password, hash);
}
async hashPassword(password: string) {
const salt = await bcrypt.genSalt(this.BcryptSaltRounds);
const hash = await bcrypt.hash(password, salt);
return hash;
}
}
+199
View File
@@ -0,0 +1,199 @@
type Event = {
name: string;
cooldown: number;
maxAttempts: number;
lastAttempt?: number;
attempts?: number[];
};
type user = {
token: string;
events: { [name: string]: Event };
};
export default class RateLimit {
constructor(public users: { [token: string]: user} = {}, public events: { [name: string]: Event} = {}) {
this.users = users;
this.events = events;
}
public addEvent(event: Event) {
try{
this.events[event.name] = event;
//add event to all users
for (const token in this.users) {
this.users[token].events[event.name] = event;
}
return true;
} catch (error) {
return false;
}
}
public removeEvent(name: string) {
try{
delete this.events[name];
//remove event from all users
for (const token in this.users) {
delete this.users[token].events[name];
}
return true;
} catch (error) {
return false;
}
}
public addUser(token: string) {
try{
this.users[token] = { token, events: this.events };
return true;
} catch (error) {
return false;
}
}
public removeUser(token: string) {
try{
delete this.users[token];
return true;
} catch (error) {
return false;
}
}
public attempt(token: string, name: string) {
try{
if (!this.users[token] || !this.users[token].events[name]) {
return false;
}
const event = this.users[token].events[name];
if (event.attempts === undefined) {
event.attempts = [];
}
const now = Date.now();
if (event.lastAttempt && now - event.lastAttempt < event.cooldown) {
return false;
}
event.lastAttempt = now;
event.attempts.push(now);
if (event.attempts.length > event.maxAttempts) {
return false;
}
return true;
} catch (error) {
return false;
}
}
public getEvents() {
return this.events;
}
public getUsers() {
return this.users;
}
public getEvent(name: string) {
return this.events[name];
}
public getUser(token: string) {
return this.users[token];
}
public remainingAttempts(token: string, name: string) {
try{
if (!this.users[token] || !this.users[token].events[name]) {
return -1;
}
const event = this.users[token].events[name];
if (event.attempts === undefined) {
event.attempts = [];
}
return event.maxAttempts - event.attempts.length;
} catch (error) {
return -1;
}
}
public resetAttempts(token: string, name: string) {
try{
if (!this.users[token] || !this.users[token].events[name]) {
return false;
}
const event = this.users[token].events[name];
event.attempts = [];
return true;
} catch (error) {
return false;
}
}
public resetAllAttempts(token: string) {
try{
if (!this.users[token]) {
return false;
}
for (const name in this.users[token].events) {
this.resetAttempts(token, name);
}
return true;
} catch (error) {
return false;
}
}
public resetAllUsers() {
try{
for (const token in this.users) {
this.resetAllAttempts(token);
}
return true;
} catch (error) {
return false;
}
}
public resetEvent(name: string) {
try{
for (const token in this.users) {
this.resetAttempts(token, name);
}
return true;
} catch (error) {
return false;
}
}
public resetUser(token: string) {
try{
for (const name in this.users[token].events) {
this.resetAttempts(token, name);
}
return true;
} catch (error) {
return false;
}
}
}