Implement JWT tokens
This commit is contained in:
parent
90a02d216a
commit
b8174bb571
|
@ -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));
|
||||
/**
|
||||
* 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);
|
||||
|
||||
logger.info("Token generation finished for user " + id);
|
||||
if (!user) return;
|
||||
if (typeof user.tokens !== "string") user.tokens = "[]";
|
||||
|
||||
if (token) resolve(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"
|
||||
});
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -334,6 +334,7 @@ declare interface ClientEvents {
|
|||
permissions: any;
|
||||
token?: any;
|
||||
accountInfo: any;
|
||||
motd?: string;
|
||||
};
|
||||
|
||||
ls: {
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue