diff --git a/package-lock.json b/package-lock.json
index 909f510c..31ed56df 100644
Binary files a/package-lock.json and b/package-lock.json differ
diff --git a/package.json b/package.json
index 1a20cf0c..79a2416c 100644
--- a/package.json
+++ b/package.json
@@ -74,7 +74,6 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.936.0",
- "@spacebarchat/medooze-webrtc": "^1.0.8",
"@toondepauw/node-zstd": "^1.2.0",
"@types/web-push": "^3.6.4",
"ajv": "^8.17.1",
diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts
index 3c3f859f..aa239669 100644
--- a/src/util/util/Token.ts
+++ b/src/util/util/Token.ts
@@ -1,196 +1,111 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-import jwt, { VerifyOptions } from "jsonwebtoken";
-import { Config } from "./Config";
-import { User } from "../entities";
import crypto from "node:crypto";
-import fs from "fs/promises";
-import { existsSync } from "fs";
-// TODO: dont use deprecated APIs lol
+import { User } from "../entities";
+import { Config } from "./Config";
import {
FindOptionsRelationByString,
FindOptionsSelectByString,
} from "typeorm";
-import * as console from "node:console";
-export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
+const DISCORD_EPOCH = 1293840000;
export type UserTokenData = {
user: User;
- decoded: { id: string; iat: number };
+ decoded: {
+ id: string;
+ iat: number;
+ };
};
-function logAuth(text: string) {
- if(process.env.LOG_AUTH !== "true") return;
- console.log(`[AUTH] ${text}`);
+function b64url(input: Buffer | string) {
+ return Buffer.from(input)
+ .toString("base64")
+ .replace(/=/g, "")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_");
}
-function rejectAndLog(rejectFunction: (reason?: any) => void, reason: any) {
- console.error(reason);
- rejectFunction(reason);
+function b64urlDecode(input: string) {
+ input = input.replace(/-/g, "+").replace(/_/g, "/");
+ return Buffer.from(input, "base64").toString();
}
-export const checkToken = (
+function sign(data: string) {
+ return crypto
+ .createHmac("sha256", Config.get().security.jwtSecret)
+ .update(data)
+ .digest();
+}
+
+export function generateToken(userId: string): string {
+ const unixNow = Math.floor(Date.now() / 1000);
+ const iat = unixNow - DISCORD_EPOCH;
+
+ const p1 = b64url(userId);
+ const p2 = b64url(iat.toString());
+
+ const sig = b64url(sign(`${p1}.${p2}`));
+
+ return `${p1}.${p2}.${sig}`;
+}
+
+export async function checkToken(
token: string,
opts?: {
select?: FindOptionsSelectByString;
relations?: FindOptionsRelationByString;
},
-): Promise =>
- new Promise((resolve, reject) => {
- token = token.replace("Bot ", ""); // there is no bot distinction in sb
- token = token.replace("Bearer ", ""); // allow bearer tokens
+): Promise {
+ token = token.replace("Bot ", "").replace("Bearer ", "");
- const validateUser: jwt.VerifyCallback = async (err, out) => {
- const decoded = out as UserTokenData["decoded"];
- if (err || !decoded) {
- logAuth("validateUser rejected: " + err);
- return rejectAndLog(reject, "Invalid Token meow " + err);
- }
-
- const user = await User.findOne({
- where: { id: decoded.id },
- select: [
- ...(opts?.select || []),
- "id",
- "bot",
- "disabled",
- "deleted",
- "rights",
- "data",
- ],
- relations: opts?.relations,
- });
-
- if (!user) {
- logAuth("validateUser rejected: User not found");
- return rejectAndLog(reject, "User not found");
- }
-
- // we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
- if (
- decoded.iat * 1000 <
- new Date(user.data.valid_tokens_since).setSeconds(0, 0)
- ) {
- logAuth("validateUser rejected: Token not yet valid");
- return rejectAndLog(reject, "Invalid Token");
- }
-
- if (user.disabled) {
- logAuth("validateUser rejected: User disabled");
- return rejectAndLog(reject, "User disabled");
- }
- if (user.deleted) {
- logAuth("validateUser rejected: User deleted");
- return rejectAndLog(reject, "User not found");
- }
-
- logAuth("validateUser success: " + JSON.stringify({ decoded, user }));
- return resolve({ decoded, user });
- };
-
- const dec = jwt.decode(token, { complete: true });
- if (!dec) return reject("Could not parse token");
- logAuth("Decoded token: " + JSON.stringify(dec));
-
- if (dec.header.alg == "HS256") {
- jwt.verify(
- token,
- Config.get().security.jwtSecret,
- JWTOptions,
- validateUser,
- );
- } else if (dec.header.alg == "ES512") {
- loadOrGenerateKeypair().then((keyPair) => {
- jwt.verify(
- token,
- keyPair.publicKey,
- { algorithms: ["ES512"] },
- validateUser,
- );
- });
- } else return reject("Invalid token algorithm");
- });
-
-export async function generateToken(id: string) {
- const iat = Math.floor(Date.now() / 1000);
- const keyPair = await loadOrGenerateKeypair();
-
- return new Promise((res, rej) => {
- jwt.sign(
- { id, iat, kid: keyPair.fingerprint },
- keyPair.privateKey,
- {
- algorithm: "ES512",
- },
- (err, token) => {
- if (err) return rej(err);
- return res(token);
- },
- );
- });
-}
-
-// Get ECDSA keypair from file or generate it
-export async function loadOrGenerateKeypair() {
- let privateKey: string | crypto.KeyObject;
- let publicKey: string | crypto.KeyObject;
-
- if (existsSync("jwt.key") && existsSync("jwt.key.pub")) {
- const [loadedPrivateKey, loadedPublicKey] = await Promise.all([
- fs.readFile("jwt.key", "utf-8"),
- fs.readFile("jwt.key.pub", "utf-8"),
- ]);
-
- privateKey = loadedPrivateKey;
- publicKey = loadedPublicKey;
- } else {
- console.log("[JWT] Generating new keypair");
- const res = crypto.generateKeyPairSync("ec", {
- namedCurve: "secp521r1",
- publicKeyEncoding: {
- type: "spki",
- format: "pem"
- },
- privateKeyEncoding: {
- type: "pkcs8",
- format: "pem"
- }
- });
-
- await Promise.all([
- fs.writeFile("jwt.key", res.privateKey),
- fs.writeFile("jwt.key.pub", res.publicKey),
- ]);
-
- privateKey = res.privateKey;
- publicKey = res.publicKey;
+ const parts = token.split(".");
+ if (parts.length !== 3) {
+ throw new Error("Invalid token format");
}
- const publicKeyForHash = typeof publicKey === 'string'
- ? crypto.createPublicKey(publicKey)
- : publicKey;
-
- const fingerprint = crypto
- .createHash("sha256")
- .update(publicKeyForHash.export({ format: "pem", type: "spki" }))
- .digest("hex");
+ const [p1, p2, sig] = parts;
+ const expected = b64url(sign(`${p1}.${p2}`));
- return { privateKey, publicKey, fingerprint };
-}
+ if (
+ !crypto.timingSafeEqual(
+ Buffer.from(sig),
+ Buffer.from(expected),
+ )
+ ) {
+ throw new Error("Invalid token signature");
+ }
+
+ const id = b64urlDecode(p1);
+ const iat = Number(b64urlDecode(p2));
+
+ if (!Number.isFinite(iat)) {
+ throw new Error("Invalid timestamp");
+ }
+
+ const user = await User.findOne({
+ where: { id },
+ select: [
+ ...(opts?.select || []),
+ "id",
+ "bot",
+ "disabled",
+ "deleted",
+ "rights",
+ "data",
+ ],
+ relations: opts?.relations,
+ });
+
+ if (!user) throw new Error("User not found");
+ if (user.disabled) throw new Error("User disabled");
+ if (user.deleted) throw new Error("User deleted");
+
+ const unixTime = (iat + DISCORD_EPOCH) * 1000;
+ if (unixTime < new Date(user.data.valid_tokens_since).setSeconds(0, 0)) {
+ throw new Error("Token revoked");
+ }
+
+ return {
+ user,
+ decoded: { id, iat },
+ };
+}
\ No newline at end of file