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 { readFileSync } from "fs";
import { Logger } from "./Logger";
import { readUser, updateUser } from "../data/user"; 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") { if (config.tokenAuth == "jwt") {
privkey = readFileSync("./mppkey").toString(); key = readFileSync("./mppkey").toString();
} }
const logger = new Logger("TokenGen"); /**
* Get an existing token for a user
export function generateToken(id: string): Promise<string | undefined> | undefined { * @param userID ID of user
if (config.tokenAuth == "jwt") { * @returns Token
if (!privkey) throw new Error("Private key not found"); **/
export async function getToken(userID: string) {
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 { try {
const uuid = crypto.randomUUID(); const user = await readUser(userID);
token = `${id}.${uuid}`;
// Save token in user data if (!user) return;
const user = await readUser(id); if (typeof user.tokens !== "string") return;
if (!user) throw new Error("User not found");
if (!user.tokens) user.tokens = "[]"; const data = JSON.parse(user.tokens) as string[];
return data[0];
const tokens = JSON.parse(user.tokens);
tokens.push(token);
user.tokens = JSON.stringify(tokens);
await updateUser(user.id, user);
} catch (err) { } catch (err) {
logger.warn("Token generation failed for user " + id); logger.warn(`Unable to get token for user ${userID}:`, err);
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") { * Create a new token for a user
// Get tokens from user data * @param userID ID of user
const user = await readUser(token.split(".")[0]); * @param gateway Socket gateway context
if (!user) return false; * @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); const data = JSON.parse(user.tokens) as string[];
if (!tokens) return false; let token = "";
// Check if the token is in the list if (config.tokenAuth == "uuid") {
for (const tok of tokens) { token = crypto.randomUUID();
if (tok === token) return true; } 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; return false;
} } catch (err) {
logger.warn(`Unable to validate token for user ${userID}:`, err);
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);
} }

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

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

View File

@ -739,6 +739,25 @@ export class Socket extends EventEmitter {
logger.error(err); 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>(); export const socketsBySocketID = new Map<string, Socket>();

View File

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