Implement JWT tokens

This commit is contained in:
Hri7566 2024-07-31 18:07:35 -04:00
parent 90a02d216a
commit b8174bb571
4 changed files with 141 additions and 105 deletions

View File

@ -1,94 +1,108 @@
import { config } from "../ws/usersConfig";
import jsonwebtoken from "jsonwebtoken";
import env from "./env";
import { readFileSync } from "fs";
import { Logger } from "./Logger";
import { readUser, updateUser } from "../data/user";
import { Gateway } from "../ws/Gateway";
import { config } from "../ws/usersConfig";
import env from "./env";
import { Logger } from "./Logger";
import jwt from "jsonwebtoken";
let privkey: string;
const logger = new Logger("Tokens");
let key: string;
if (config.tokenAuth == "jwt") {
privkey = readFileSync("./mppkey").toString();
key = readFileSync("./mppkey").toString();
}
const logger = new Logger("TokenGen");
export function generateToken(id: string): Promise<string | undefined> | undefined {
if (config.tokenAuth == "jwt") {
if (!privkey) throw new Error("Private key not found");
logger.info("Generating JWT token for user " + id + "...");
return new Promise((resolve, reject) => {
jsonwebtoken.sign({ id }, privkey, { algorithm: "RS256" }, (err, token) => {
if (err || !token) {
logger.warn("Token generation failed for user " + id);
reject(err);
}
logger.info("Token generation finished for user " + id);
resolve(token);
});
});
} else if (config.tokenAuth == "uuid") {
logger.info("Generating UUID token for user " + id + "...");
return new Promise(async (resolve, reject) => {
let token: string | undefined;
/**
* Get an existing token for a user
* @param userID ID of user
* @returns Token
**/
export async function getToken(userID: string) {
try {
const uuid = crypto.randomUUID();
token = `${id}.${uuid}`;
const user = await readUser(userID);
// Save token in user data
const user = await readUser(id);
if (!user) throw new Error("User not found");
if (!user) return;
if (typeof user.tokens !== "string") return;
if (!user.tokens) user.tokens = "[]";
const tokens = JSON.parse(user.tokens);
tokens.push(token);
user.tokens = JSON.stringify(tokens);
await updateUser(user.id, user);
const data = JSON.parse(user.tokens) as string[];
return data[0];
} catch (err) {
logger.warn("Token generation failed for user " + id);
reject(err);
logger.warn(`Unable to get token for user ${userID}:`, err);
}
if (!token) reject(new Error("Token generation failed for user " + id));
logger.info("Token generation finished for user " + id);
if (token) resolve(token);
});
} else return new Promise(() => undefined);
}
export async function verifyToken(token: string) {
if (config.tokenAuth !== "none") {
// Get tokens from user data
const user = await readUser(token.split(".")[0]);
if (!user) return false;
/**
* Create a new token for a user
* @param userID ID of user
* @param gateway Socket gateway context
* @returns Token
**/
export async function createToken(userID: string, gateway: Gateway) {
try {
const user = await readUser(userID);
if (!user.tokens) return false;
if (!user) return;
if (typeof user.tokens !== "string") user.tokens = "[]";
const tokens = JSON.parse(user.tokens);
if (!tokens) return false;
const data = JSON.parse(user.tokens) as string[];
let token = "";
// Check if the token is in the list
for (const tok of tokens) {
if (tok === token) return true;
if (config.tokenAuth == "uuid") {
token = crypto.randomUUID();
} else if (config.tokenAuth == "jwt") {
token = generateJWT(userID, gateway);
}
data.push(token);
user.tokens = JSON.stringify(data);
await updateUser(userID, user);
return token;
} catch (err) {
logger.warn(`Unable to create token for user ${userID}:`, err);
}
}
export function generateJWT(userID: string, gateway: Gateway) {
const payload = {
userID,
gateway
};
return jwt.sign(payload, key, {
algorithm: "RS256"
});
}
/**
* Validate a token
* @param userID ID of user
* @param token Token
* @returns True if token is valid, false otherwise
**/
export async function validateToken(userID: string, token: string) {
try {
const user = await readUser(userID);
if (!user) {
logger.warn(`Unable to validate token for user ${userID}: User not found, which is really weird`);
return false;
}
if (typeof user.tokens !== "string") {
user.tokens = "[]";
}
const data = JSON.parse(user.tokens) as string[];
if (data.indexOf(token) !== -1) {
return true;
}
return false;
}
export async function decryptJWT(token: string) {
if (config.tokenAuth != "jwt") return undefined;
if (!privkey) throw new Error("Cannot decrypt JWT without private key loaded");
return jsonwebtoken.decode(token);
} catch (err) {
logger.warn(`Unable to validate token for user ${userID}:`, err);
}
}

1
src/util/types.d.ts vendored
View File

@ -334,6 +334,7 @@ declare interface ClientEvents {
permissions: any;
token?: any;
accountInfo: any;
motd?: string;
};
ls: {

View File

@ -739,6 +739,25 @@ export class Socket extends EventEmitter {
logger.error(err);
}
}
/**
* Ban this socket's user for doing bad things
**/
public ban(duration: number, reason: string) {
// TODO cleaner ban system
// TODO save bans to database
const user = this.getUser();
if (!user) return;
this.sendNotification({
title: "Notice",
text: `You have been banned from the server for ${Math.floor(duration / 1000 / 60)} minutes. Reason: ${reason}`,
duration: 20000,
target: "#room",
class: "classic"
});
}
}
export const socketsBySocketID = new Map<string, Socket>();

View File

@ -1,6 +1,6 @@
import { Logger } from "../../../../util/Logger";
import { getMOTD } from "../../../../util/motd";
import { generateToken, verifyToken } from "../../../../util/token";
import { createToken, getToken, validateToken } from "../../../../util/token";
import { ClientEvents, ServerEventListener } from "../../../../util/types";
import { config } from "../../../usersConfig";
@ -15,8 +15,6 @@ export const hi: ServerEventListener<"hi"> = {
if (socket.gateway.hasProcessedHi) return;
let generatedToken: string | undefined;
// Browser challenge
if (config.browserChallenge == "basic") {
if (typeof msg.code !== "boolean") return;
@ -29,25 +27,33 @@ export const hi: ServerEventListener<"hi"> = {
}
// Is the browser challenge enabled and has the user completed it?
if (config.browserChallenge !== "none" && !socket.gateway.hasCompletedBrowserChallenge) return;
if (config.browserChallenge !== "none" && !socket.gateway.hasCompletedBrowserChallenge) return socket.ban(60000, "Browser challenge not completed");
// Is token auth enabled?
if (config.tokenAuth !== "none") {
logger.debug("token auth is enabled");
let token: string | undefined;
let generatedToken = false;
// Is the browser challenge enabled and has the user completed it?
if (msg.token) {
// Check if they have passed the browser challenge
// Send the token to the authenticator
// TODO
const verified = await verifyToken(msg.token);
if (typeof msg.token !== "string") {
// Get a saved token
token = await getToken(socket.getUserID());
if (typeof token !== "string") {
// Generate a new one
token = await createToken(socket.getUserID(), socket.gateway);
if (typeof token !== "string") {
logger.warn(`Unable to generate token for user ${socket.getUserID()}`);
} else {
// Generate a token
generatedToken = await generateToken(socket.getUserID());
if (!generatedToken) return;
generatedToken = true;
}
}
} else {
// Validate the token
const valid = await validateToken(socket.getUserID(), msg.token);
if (!valid) {
socket.ban(60000, "Invalid token");
return;
}
logger.debug("token:", generatedToken);
token = msg.token;
}
let part = socket.getParticipant();
@ -61,7 +67,7 @@ export const hi: ServerEventListener<"hi"> = {
};
}
const m = {
socket.sendArray([{
m: "hi",
accountInfo: undefined,
permissions: undefined,
@ -71,13 +77,9 @@ export const hi: ServerEventListener<"hi"> = {
color: part.color,
name: part.name
},
token: generatedToken,
motd: getMOTD()
};
//logger.debug("Hi message:", m);
socket.sendArray([m as ClientEvents["hi"]]);
motd: getMOTD(),
token
}]);
socket.gateway.hasProcessedHi = true;
}