use discord token format
This commit is contained in:
parent
818358cc68
commit
d01b924854
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
@ -74,7 +74,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.936.0",
|
"@aws-sdk/client-s3": "^3.936.0",
|
||||||
"@spacebarchat/medooze-webrtc": "^1.0.8",
|
|
||||||
"@toondepauw/node-zstd": "^1.2.0",
|
"@toondepauw/node-zstd": "^1.2.0",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
|
|||||||
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import jwt, { VerifyOptions } from "jsonwebtoken";
|
|
||||||
import { Config } from "./Config";
|
|
||||||
import { User } from "../entities";
|
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "fs/promises";
|
import { User } from "../entities";
|
||||||
import { existsSync } from "fs";
|
import { Config } from "./Config";
|
||||||
// TODO: dont use deprecated APIs lol
|
|
||||||
import {
|
import {
|
||||||
FindOptionsRelationByString,
|
FindOptionsRelationByString,
|
||||||
FindOptionsSelectByString,
|
FindOptionsSelectByString,
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
import * as console from "node:console";
|
|
||||||
|
|
||||||
export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
|
const DISCORD_EPOCH = 1293840000;
|
||||||
|
|
||||||
export type UserTokenData = {
|
export type UserTokenData = {
|
||||||
user: User;
|
user: User;
|
||||||
decoded: { id: string; iat: number };
|
decoded: {
|
||||||
|
id: string;
|
||||||
|
iat: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function logAuth(text: string) {
|
function b64url(input: Buffer | string) {
|
||||||
if(process.env.LOG_AUTH !== "true") return;
|
return Buffer.from(input)
|
||||||
console.log(`[AUTH] ${text}`);
|
.toString("base64")
|
||||||
|
.replace(/=/g, "")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
function rejectAndLog(rejectFunction: (reason?: any) => void, reason: any) {
|
function b64urlDecode(input: string) {
|
||||||
console.error(reason);
|
input = input.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
rejectFunction(reason);
|
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,
|
token: string,
|
||||||
opts?: {
|
opts?: {
|
||||||
select?: FindOptionsSelectByString<User>;
|
select?: FindOptionsSelectByString<User>;
|
||||||
relations?: FindOptionsRelationByString;
|
relations?: FindOptionsRelationByString;
|
||||||
},
|
},
|
||||||
): Promise<UserTokenData> =>
|
): Promise<UserTokenData> {
|
||||||
new Promise((resolve, reject) => {
|
token = token.replace("Bot ", "").replace("Bearer ", "");
|
||||||
token = token.replace("Bot ", ""); // there is no bot distinction in sb
|
|
||||||
token = token.replace("Bearer ", ""); // allow bearer tokens
|
|
||||||
|
|
||||||
const validateUser: jwt.VerifyCallback = async (err, out) => {
|
const parts = token.split(".");
|
||||||
const decoded = out as UserTokenData["decoded"];
|
if (parts.length !== 3) {
|
||||||
if (err || !decoded) {
|
throw new Error("Invalid token format");
|
||||||
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 publicKeyForHash = typeof publicKey === 'string'
|
const [p1, p2, sig] = parts;
|
||||||
? crypto.createPublicKey(publicKey)
|
const expected = b64url(sign(`${p1}.${p2}`));
|
||||||
: publicKey;
|
|
||||||
|
|
||||||
const fingerprint = crypto
|
if (
|
||||||
.createHash("sha256")
|
!crypto.timingSafeEqual(
|
||||||
.update(publicKeyForHash.export({ format: "pem", type: "spki" }))
|
Buffer.from(sig),
|
||||||
.digest("hex");
|
Buffer.from(expected),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid token signature");
|
||||||
|
}
|
||||||
|
|
||||||
return { privateKey, publicKey, fingerprint };
|
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 },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user