From b8174bb5715e8bf55bc51fc3e4972b5d7bcee0fe Mon Sep 17 00:00:00 2001 From: Hri7566 Date: Wed, 31 Jul 2024 18:07:35 -0400 Subject: [PATCH] Implement JWT tokens --- src/util/token.ts | 172 ++++++++++++++++-------------- src/util/types.d.ts | 1 + src/ws/Socket.ts | 19 ++++ src/ws/events/user/handlers/hi.ts | 54 +++++----- 4 files changed, 141 insertions(+), 105 deletions(-) diff --git a/src/util/token.ts b/src/util/token.ts index 7f1853d..54d22af 100644 --- a/src/util/token.ts +++ b/src/util/token.ts @@ -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"); +/** + * Get an existing token for a user + * @param userID ID of user + * @returns Token + **/ +export async function getToken(userID: string) { + try { + const user = await readUser(userID); -export function generateToken(id: string): Promise | undefined { - if (config.tokenAuth == "jwt") { - if (!privkey) throw new Error("Private key not found"); + if (!user) return; + if (typeof user.tokens !== "string") return; - 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; - - try { - const uuid = crypto.randomUUID(); - token = `${id}.${uuid}`; - - // Save token in user data - const user = await readUser(id); - if (!user) throw new Error("User not found"); - - if (!user.tokens) user.tokens = "[]"; - - const tokens = JSON.parse(user.tokens); - tokens.push(token); - - user.tokens = JSON.stringify(tokens); - await updateUser(user.id, user); - } catch (err) { - logger.warn("Token generation failed for user " + id); - reject(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; - - if (!user.tokens) return false; - - const tokens = JSON.parse(user.tokens); - if (!tokens) return false; - - // Check if the token is in the list - for (const tok of tokens) { - if (tok === token) return true; - } + const data = JSON.parse(user.tokens) as string[]; + return data[0]; + } catch (err) { + logger.warn(`Unable to get token for user ${userID}:`, err); } - - return false; } -export async function decryptJWT(token: string) { - if (config.tokenAuth != "jwt") return undefined; +/** + * 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 (!privkey) throw new Error("Cannot decrypt JWT without private key loaded"); + if (!user) return; + if (typeof user.tokens !== "string") user.tokens = "[]"; - return jsonwebtoken.decode(token); + const data = JSON.parse(user.tokens) as string[]; + let token = ""; + + 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; + } catch (err) { + logger.warn(`Unable to validate token for user ${userID}:`, err); + } } diff --git a/src/util/types.d.ts b/src/util/types.d.ts index 833d8e6..4807f11 100755 --- a/src/util/types.d.ts +++ b/src/util/types.d.ts @@ -334,6 +334,7 @@ declare interface ClientEvents { permissions: any; token?: any; accountInfo: any; + motd?: string; }; ls: { diff --git a/src/ws/Socket.ts b/src/ws/Socket.ts index a18f9b1..b343a4b 100755 --- a/src/ws/Socket.ts +++ b/src/ws/Socket.ts @@ -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(); diff --git a/src/ws/events/user/handlers/hi.ts b/src/ws/events/user/handlers/hi.ts index af125e3..91067b1 100755 --- a/src/ws/events/user/handlers/hi.ts +++ b/src/ws/events/user/handlers/hi.ts @@ -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); - } else { - // Generate a token - generatedToken = await generateToken(socket.getUserID()); - if (!generatedToken) return; + 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 { + 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; }