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 { 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
|
||||||
|
* @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<string | undefined> | undefined {
|
if (!user) return;
|
||||||
if (config.tokenAuth == "jwt") {
|
if (typeof user.tokens !== "string") return;
|
||||||
if (!privkey) throw new Error("Private key not found");
|
|
||||||
|
|
||||||
logger.info("Generating JWT token for user " + id + "...");
|
const data = JSON.parse(user.tokens) as string[];
|
||||||
|
return data[0];
|
||||||
return new Promise((resolve, reject) => {
|
} catch (err) {
|
||||||
jsonwebtoken.sign({ id }, privkey, { algorithm: "RS256" }, (err, token) => {
|
logger.warn(`Unable to get token for user ${userID}:`, err);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -334,6 +334,7 @@ declare interface ClientEvents {
|
||||||
permissions: any;
|
permissions: any;
|
||||||
token?: any;
|
token?: any;
|
||||||
accountInfo: any;
|
accountInfo: any;
|
||||||
|
motd?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
ls: {
|
ls: {
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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);
|
||||||
} else {
|
|
||||||
// Generate a token
|
if (typeof token !== "string") {
|
||||||
generatedToken = await generateToken(socket.getUserID());
|
logger.warn(`Unable to generate token for user ${socket.getUserID()}`);
|
||||||
if (!generatedToken) return;
|
} 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();
|
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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue