From f1f68c3d314c1b8bf42641a165ef6c9a8ebd348e Mon Sep 17 00:00:00 2001 From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com> Date: Fri, 28 Jul 2023 09:26:18 +1000 Subject: [PATCH] refactor checkToken --- assets/openapi.json | Bin 571228 -> 572156 bytes assets/schemas.json | Bin 18199664 -> 18293788 bytes src/api/middlewares/Authentication.ts | 7 +- src/api/routes/auth/reset.ts | 4 +- src/api/routes/auth/verify/index.ts | 3 +- src/gateway/opcodes/Identify.ts | 62 +++++-------- src/util/schemas/RegisterSchema.ts | 4 + src/util/util/Token.ts | 120 ++++++++++---------------- 8 files changed, 75 insertions(+), 125 deletions(-) diff --git a/assets/openapi.json b/assets/openapi.json index 9d4f0a1ecd58b826406fa36aa3ea4c3052f20449..cf262c2cdd77bcb3c247df8b22585a2e5046f4ae 100644 GIT binary patch delta 759 zcmb7>T}YE*6vuha^S-Z}jp>L=#gDbt$L6w}n5apwdZ84FH^mZdZn{lT)A>Px7p^WU zglzd}M~Y|>7*@#A>LDz;DkzQVMHflrFtaZDc%g(!skTW7-F0!E!~e(O9RC03^=HG% zlEHVEgo{xbhfJ1uDi_1WWF)F2rmfh7${3MQf0*R)rzPah6|A(8hfI6b$ZFT`#lUys zWcooX1`d#&Z1<3A{K(nsfI&B{#zVi!Y1USn!nZ7vJPG}mp#x=&REYkdn1$zo=5TqQ zyd=2qJ#FG|7Dy>U`waE4=iRf+HxP$?PVl09i?~p(ic{k{H#o4u0aGm4(}JOH5Sh=H z6}yc35-M8LML)8lVUhETG1BW+=cB z18isdu`>K(iv;)e!bY4fpaRaBBdXJ)!^$$6flU_B@*XpMD#TG6$wuWTl`&WiN!a2B zJCjo-Ugd$?4e0wolQ%SayIl6J4p;jzM~lne?(#Htbhe|epJrm<63LD8G`Bh(&2~IF z1AYA9G^7~&onF@SLcH;8320=S8r#0X0$27E4Z*-HY~{;8U{}QZIM`SJC~PG;7|@Wq zNJM+M9Hg%VmO7Ci3zaLwbcgCch7IEV2|Azf_ppUkX7{3g2x|DRN%|x|#$`$HvP8_l z#CS+zzOo_=Ow!G3s=`XPMU^%Bs9Erl>0-U0M*RuMK>rjqMzJnxKW&Yd=aATjf4N>G Vh-X47AyUyUMJkSmRH}2Me*jyR9)}Jk<7IK9 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 { const { password, token } = req.body as PasswordResetSchema; - const { jwtSecret } = Config.get().security; - let user; try { - const userTokenData = await checkToken(token, jwtSecret, true); + const userTokenData = await checkToken(token); user = userTokenData.user; } catch { throw FieldErrors({ diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts index a98c17fa..49f74277 100644 --- a/src/api/routes/auth/verify/index.ts +++ b/src/api/routes/auth/verify/index.ts @@ -78,11 +78,10 @@ router.post( } } - const { jwtSecret } = Config.get().security; let user; try { - const userTokenData = await checkToken(token, jwtSecret, true); + const userTokenData = await checkToken(token); user = userTokenData.user; } catch { throw FieldErrors({ diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index 5816c308..837ae351 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -30,7 +30,6 @@ import { Intents, Member, ReadyEventData, - User, Session, EVENTEnum, Config, @@ -60,17 +59,6 @@ import { check } from "./instanceOf"; // TODO: user sharding // TODO: check privileged intents, if defined in the config -const getUserFromToken = async (token: string): Promise => { - try { - const { jwtSecret } = Config.get().security; - const { decoded } = await checkToken(token, jwtSecret); - return decoded.id; - } catch (e) { - console.error(`[Gateway] Invalid token`, e); - return null; - } -}; - export async function onIdentify(this: WebSocket, data: Payload) { if (this.user_id) { // we've already identified @@ -85,12 +73,12 @@ export async function onIdentify(this: WebSocket, data: Payload) { this.capabilities = new Capabilities(identify.capabilities || 0); - // Check auth - // TODO: the checkToken call will fetch user, and then we have to refetch with different select - // checkToken should be able to select what we want - const user_id = await getUserFromToken(identify.token); - if (!user_id) return this.close(CLOSECODES.Authentication_failed); - this.user_id = user_id; + const { user } = await checkToken(identify.token, { + relations: ["relationships", "relationships.to", "settings"], + select: [...PrivateUserProjection, "relationships"], + }); + if (!user) return this.close(CLOSECODES.Authentication_failed); + this.user_id = user.id; // Check intents if (!identify.intents) identify.intents = 30064771071n; // TODO: what is this number? @@ -112,7 +100,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { ) { // TODO: why do we even care about this right now? console.log( - `[Gateway] Invalid sharding from ${user_id}: ${identify.shard}`, + `[Gateway] Invalid sharding from ${user.id}: ${identify.shard}`, ); return this.close(CLOSECODES.Invalid_shard); } @@ -132,22 +120,14 @@ export async function onIdentify(this: WebSocket, data: Payload) { }); // Get from database: - // * the current user, // * the users read states // * guild members for this user // * recipients ( dm channels ) // * the bot application, if it exists - const [, user, application, read_states, members, recipients] = - await Promise.all([ + const [, application, read_states, members, recipients] = await Promise.all( + [ session.save(), - // TODO: Refactor checkToken to allow us to skip this additional query - User.findOneOrFail({ - where: { id: this.user_id }, - relations: ["relationships", "relationships.to", "settings"], - select: [...PrivateUserProjection, "relationships"], - }), - Application.findOne({ where: { id: this.user_id }, select: ["id", "flags"], @@ -224,7 +204,8 @@ export async function onIdentify(this: WebSocket, data: Payload) { }, }, }), - ]); + ], + ); // We forgot to migrate user settings from the JSON column of `users` // to the `user_settings` table theyre in now, @@ -407,6 +388,17 @@ export async function onIdentify(this: WebSocket, data: Payload) { merged_members: merged_members, sessions: allSessions, + resume_gateway_url: + Config.get().gateway.endpointClient || + Config.get().gateway.endpointPublic || + "ws://127.0.0.1:3001", + + // lol hack whatever + required_action: + Config.get().login.requireVerification && !user.verified + ? "REQUIRE_VERIFIED_EMAIL" + : undefined, + consents: { personalization: { consented: false, // TODO @@ -421,18 +413,8 @@ export async function onIdentify(this: WebSocket, data: Payload) { friend_suggestion_count: 0, analytics_token: "", tutorial: null, - resume_gateway_url: - Config.get().gateway.endpointClient || - Config.get().gateway.endpointPublic || - "ws://127.0.0.1:3001", session_type: "normal", // TODO auth_session_id_hash: "", // TODO - - // lol hack whatever - required_action: - Config.get().login.requireVerification && !user.verified - ? "REQUIRE_VERIFIED_EMAIL" - : undefined, }; // Send READY diff --git a/src/util/schemas/RegisterSchema.ts b/src/util/schemas/RegisterSchema.ts index f6c99b18..7b7de9c7 100644 --- a/src/util/schemas/RegisterSchema.ts +++ b/src/util/schemas/RegisterSchema.ts @@ -42,4 +42,8 @@ export interface RegisterSchema { captcha_key?: string; promotional_email_opt_in?: boolean; + + // part of pomelo + unique_username_registration?: boolean; + global_name?: string; } diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts index 90310176..eec72522 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts @@ -19,94 +19,66 @@ import jwt, { VerifyOptions } from "jsonwebtoken"; import { Config } from "./Config"; import { User } from "../entities"; +// TODO: dont use deprecated APIs lol +import { + FindOptionsRelationByString, + FindOptionsSelectByString, +} from "typeorm"; export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] }; export type UserTokenData = { user: User; - decoded: { id: string; iat: number }; + decoded: { id: string; iat: number; email?: string }; }; -async function checkEmailToken( - decoded: jwt.JwtPayload, -): Promise { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (res, rej) => { - if (!decoded.iat) return rej("Invalid Token"); // will never happen, just for typings. - - const user = await User.findOne({ - where: { - email: decoded.email, - }, - select: [ - "email", - "id", - "verified", - "deleted", - "disabled", - "username", - "data", - ], - }); - - if (!user) return rej("Invalid Token"); - - if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000) - return rej("Invalid Token"); - - // Using as here because we assert `id` and `iat` are in decoded. - // TS just doesn't want to assume its there, though. - return res({ decoded, user } as UserTokenData); - }); -} - -export function checkToken( +export const checkToken = ( token: string, - jwtSecret: string, - isEmailVerification = false, -): Promise { - return new Promise((res, rej) => { - token = token.replace("Bot ", ""); - token = token.replace("Bearer ", ""); - /** - in spacebar, even with instances that have bot distinction; we won't enforce "Bot" prefix, - as we don't really have separate pathways for bots - **/ + opts?: { + select?: FindOptionsSelectByString; + relations?: FindOptionsRelationByString; + }, +): Promise => + new Promise((resolve, reject) => { + jwt.verify( + token, + Config.get().security.jwtSecret, + JWTOptions, + async (err, out) => { + const decoded = out as UserTokenData["decoded"]; + if (err || !decoded) return reject("Invalid Token"); - jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded) => { - if (err || !decoded) return rej("Invalid Token"); - if ( - typeof decoded == "string" || - !("id" in decoded) || - !decoded.iat - ) - return rej("Invalid Token"); // will never happen, just for typings. + const user = await User.findOne({ + where: decoded.email + ? { email: decoded.email } + : { id: decoded.id }, + select: [ + ...(opts?.select || []), + "bot", + "disabled", + "deleted", + "rights", + "data", + ], + relations: opts?.relations, + }); - if (isEmailVerification) return res(checkEmailToken(decoded)); + if (!user) return reject("User not found"); - const user = await User.findOne({ - where: { id: decoded.id }, - select: ["data", "bot", "disabled", "deleted", "rights"], - }); + // 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) + ) + return reject("Invalid Token"); - if (!user) return rej("Invalid Token"); + if (user.disabled) return reject("User disabled"); + if (user.deleted) return 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) - ) - return rej("Invalid Token"); - - if (user.disabled) return rej("User disabled"); - if (user.deleted) return rej("User not found"); - - // Using as here because we assert `id` and `iat` are in decoded. - // TS just doesn't want to assume its there, though. - return res({ decoded, user } as UserTokenData); - }); + return resolve({ decoded, user }); + }, + ); }); -} export async function generateToken(id: string, email?: string) { const iat = Math.floor(Date.now() / 1000);