refactor checkToken

This commit is contained in:
Madeline 2023-07-28 09:26:18 +10:00
parent 8a3989c297
commit f1f68c3d31
No known key found for this signature in database
GPG Key ID: 80D25DA3BCB24281
8 changed files with 11001 additions and 8202 deletions

View File

@ -2390,12 +2390,16 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
"flags": {
"type": "integer"
},
"id": { "id": {
"type": "string" "type": "string"
} }
}, },
"required": [ "required": [
"color", "color",
"flags",
"guild", "guild",
"guild_id", "guild_id",
"hoist", "hoist",
@ -3632,47 +3636,45 @@
"type": "object", "type": "object",
"additionalProperties": false "additionalProperties": false
}, },
"id": {
"type": "string"
},
"roles": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Role"
}
},
"name": { "name": {
"type": "string" "type": "string"
}, },
"banner": {
"type": "string"
},
"unavailable": {
"type": "boolean"
},
"channels": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Channel"
}
},
"region": {
"type": "string"
},
"icon": { "icon": {
"type": "string" "type": "string"
}, },
"system_channel_id": { "parent": {
"type": "string" "type": "string"
}, },
"rules_channel_id": { "owner_id": {
"type": "string" "type": "string"
}, },
"afk_timeout": { "nsfw": {
"type": "integer" "type": "boolean"
}, },
"explicit_content_filter": { "invites": {
"type": "integer" "type": "array",
"items": {
"$ref": "#/components/schemas/Invite"
}
},
"voice_states": {
"type": "array",
"items": {
"$ref": "#/components/schemas/VoiceState"
}
},
"webhooks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Webhook"
}
},
"id": {
"type": "string"
},
"_do_validate": {
"type": "object",
"additionalProperties": false
}, },
"assign": { "assign": {
"type": "object", "type": "object",
@ -3707,6 +3709,39 @@
"type": "object", "type": "object",
"additionalProperties": false "additionalProperties": false
}, },
"roles": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Role"
}
},
"banner": {
"type": "string"
},
"unavailable": {
"type": "boolean"
},
"channels": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Channel"
}
},
"region": {
"type": "string"
},
"system_channel_id": {
"type": "string"
},
"rules_channel_id": {
"type": "string"
},
"afk_timeout": {
"type": "integer"
},
"explicit_content_filter": {
"type": "integer"
},
"afk_channel_id": { "afk_channel_id": {
"type": "string" "type": "string"
}, },
@ -3773,30 +3808,9 @@
"$ref": "#/components/schemas/Sticker" "$ref": "#/components/schemas/Sticker"
} }
}, },
"invites": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Invite"
}
},
"voice_states": {
"type": "array",
"items": {
"$ref": "#/components/schemas/VoiceState"
}
},
"webhooks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Webhook"
}
},
"mfa_level": { "mfa_level": {
"type": "integer" "type": "integer"
}, },
"owner_id": {
"type": "string"
},
"preferred_locale": { "preferred_locale": {
"type": "string" "type": "string"
}, },
@ -3830,21 +3844,11 @@
"nsfw_level": { "nsfw_level": {
"type": "integer" "type": "integer"
}, },
"nsfw": {
"type": "boolean"
},
"parent": {
"type": "string"
},
"permissions": { "permissions": {
"type": "integer" "type": "integer"
}, },
"premium_progress_bar_enabled": { "premium_progress_bar_enabled": {
"type": "boolean" "type": "boolean"
},
"_do_validate": {
"type": "object",
"additionalProperties": false
} }
}, },
"required": [ "required": [
@ -4173,7 +4177,9 @@
"channel": { "channel": {
"$ref": "#/components/schemas/RateLimitOptions" "$ref": "#/components/schemas/RateLimitOptions"
}, },
"auth": {} "auth": {
"$ref": "#/components/schemas/AuthRateLimit"
}
}, },
"required": [ "required": [
"auth", "auth",
@ -4182,6 +4188,21 @@
"webhook" "webhook"
] ]
}, },
"AuthRateLimit": {
"type": "object",
"properties": {
"login": {
"$ref": "#/components/schemas/RateLimitOptions"
},
"register": {
"$ref": "#/components/schemas/RateLimitOptions"
}
},
"required": [
"login",
"register"
]
},
"GlobalRateLimits": { "GlobalRateLimits": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4664,13 +4685,13 @@
"discovery_splash": { "discovery_splash": {
"type": "string" "type": "string"
}, },
"region": {
"type": "string"
},
"icon": { "icon": {
"type": "string", "type": "string",
"nullable": true "nullable": true
}, },
"region": {
"type": "string"
},
"guild_template_code": { "guild_template_code": {
"type": "string" "type": "string"
}, },
@ -5406,6 +5427,12 @@
}, },
"promotional_email_opt_in": { "promotional_email_opt_in": {
"type": "boolean" "type": "boolean"
},
"unique_username_registration": {
"type": "boolean"
},
"global_name": {
"type": "string"
} }
}, },
"required": [ "required": [
@ -5684,13 +5711,6 @@
"version": { "version": {
"type": "integer" "type": "integer"
}, },
"guild_id": {
"type": "string",
"nullable": true
},
"flags": {
"type": "integer"
},
"message_notifications": { "message_notifications": {
"type": "integer" "type": "integer"
}, },
@ -5716,6 +5736,13 @@
"suppress_roles": { "suppress_roles": {
"type": "boolean" "type": "boolean"
}, },
"guild_id": {
"type": "string",
"nullable": true
},
"flags": {
"type": "integer"
},
"mute_scheduled_events": { "mute_scheduled_events": {
"type": "boolean" "type": "boolean"
}, },
@ -6934,6 +6961,9 @@
"APIPrivateUser": { "APIPrivateUser": {
"type": "object", "type": "object",
"properties": { "properties": {
"flags": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@ -6980,9 +7010,6 @@
"pronouns": { "pronouns": {
"type": "string" "type": "string"
}, },
"flags": {
"type": "string"
},
"mfa_enabled": { "mfa_enabled": {
"type": "boolean" "type": "boolean"
}, },
@ -7051,6 +7078,9 @@
"newToken": { "newToken": {
"type": "string" "type": "string"
}, },
"flags": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@ -7097,9 +7127,6 @@
"pronouns": { "pronouns": {
"type": "string" "type": "string"
}, },
"flags": {
"type": "string"
},
"mfa_enabled": { "mfa_enabled": {
"type": "boolean" "type": "boolean"
}, },
@ -7264,10 +7291,10 @@
"APIPublicMember": { "APIPublicMember": {
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "guild_id": {
"type": "string" "type": "string"
}, },
"guild_id": { "id": {
"type": "string" "type": "string"
}, },
"nick": { "nick": {
@ -7696,10 +7723,10 @@
"additionalProperties": false, "additionalProperties": false,
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "guild_id": {
"type": "string" "type": "string"
}, },
"guild_id": { "id": {
"type": "string" "type": "string"
}, },
"nick": { "nick": {

File diff suppressed because it is too large Load Diff

View File

@ -92,12 +92,7 @@ export async function Authentication(
Sentry.setUser({ id: req.user_id }); Sentry.setUser({ id: req.user_id });
try { try {
const { jwtSecret } = Config.get().security; const { decoded, user } = await checkToken(req.headers.authorization);
const { decoded, user } = await checkToken(
req.headers.authorization,
jwtSecret,
);
req.token = decoded; req.token = decoded;
req.user_id = decoded.id; req.user_id = decoded.id;

View File

@ -48,11 +48,9 @@ router.post(
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { password, token } = req.body as PasswordResetSchema; const { password, token } = req.body as PasswordResetSchema;
const { jwtSecret } = Config.get().security;
let user; let user;
try { try {
const userTokenData = await checkToken(token, jwtSecret, true); const userTokenData = await checkToken(token);
user = userTokenData.user; user = userTokenData.user;
} catch { } catch {
throw FieldErrors({ throw FieldErrors({

View File

@ -78,11 +78,10 @@ router.post(
} }
} }
const { jwtSecret } = Config.get().security;
let user; let user;
try { try {
const userTokenData = await checkToken(token, jwtSecret, true); const userTokenData = await checkToken(token);
user = userTokenData.user; user = userTokenData.user;
} catch { } catch {
throw FieldErrors({ throw FieldErrors({

View File

@ -30,7 +30,6 @@ import {
Intents, Intents,
Member, Member,
ReadyEventData, ReadyEventData,
User,
Session, Session,
EVENTEnum, EVENTEnum,
Config, Config,
@ -60,17 +59,6 @@ import { check } from "./instanceOf";
// TODO: user sharding // TODO: user sharding
// TODO: check privileged intents, if defined in the config // TODO: check privileged intents, if defined in the config
const getUserFromToken = async (token: string): Promise<string | null> => {
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) { export async function onIdentify(this: WebSocket, data: Payload) {
if (this.user_id) { if (this.user_id) {
// we've already identified // we've already identified
@ -85,12 +73,12 @@ export async function onIdentify(this: WebSocket, data: Payload) {
this.capabilities = new Capabilities(identify.capabilities || 0); this.capabilities = new Capabilities(identify.capabilities || 0);
// Check auth const { user } = await checkToken(identify.token, {
// TODO: the checkToken call will fetch user, and then we have to refetch with different select relations: ["relationships", "relationships.to", "settings"],
// checkToken should be able to select what we want select: [...PrivateUserProjection, "relationships"],
const user_id = await getUserFromToken(identify.token); });
if (!user_id) return this.close(CLOSECODES.Authentication_failed); if (!user) return this.close(CLOSECODES.Authentication_failed);
this.user_id = user_id; this.user_id = user.id;
// Check intents // Check intents
if (!identify.intents) identify.intents = 30064771071n; // TODO: what is this number? 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? // TODO: why do we even care about this right now?
console.log( console.log(
`[Gateway] Invalid sharding from ${user_id}: ${identify.shard}`, `[Gateway] Invalid sharding from ${user.id}: ${identify.shard}`,
); );
return this.close(CLOSECODES.Invalid_shard); return this.close(CLOSECODES.Invalid_shard);
} }
@ -132,22 +120,14 @@ export async function onIdentify(this: WebSocket, data: Payload) {
}); });
// Get from database: // Get from database:
// * the current user,
// * the users read states // * the users read states
// * guild members for this user // * guild members for this user
// * recipients ( dm channels ) // * recipients ( dm channels )
// * the bot application, if it exists // * the bot application, if it exists
const [, user, application, read_states, members, recipients] = const [, application, read_states, members, recipients] = await Promise.all(
await Promise.all([ [
session.save(), 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({ Application.findOne({
where: { id: this.user_id }, where: { id: this.user_id },
select: ["id", "flags"], 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` // We forgot to migrate user settings from the JSON column of `users`
// to the `user_settings` table theyre in now, // to the `user_settings` table theyre in now,
@ -407,6 +388,17 @@ export async function onIdentify(this: WebSocket, data: Payload) {
merged_members: merged_members, merged_members: merged_members,
sessions: allSessions, 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: { consents: {
personalization: { personalization: {
consented: false, // TODO consented: false, // TODO
@ -421,18 +413,8 @@ export async function onIdentify(this: WebSocket, data: Payload) {
friend_suggestion_count: 0, friend_suggestion_count: 0,
analytics_token: "", analytics_token: "",
tutorial: null, tutorial: null,
resume_gateway_url:
Config.get().gateway.endpointClient ||
Config.get().gateway.endpointPublic ||
"ws://127.0.0.1:3001",
session_type: "normal", // TODO session_type: "normal", // TODO
auth_session_id_hash: "", // TODO auth_session_id_hash: "", // TODO
// lol hack whatever
required_action:
Config.get().login.requireVerification && !user.verified
? "REQUIRE_VERIFIED_EMAIL"
: undefined,
}; };
// Send READY // Send READY

View File

@ -42,4 +42,8 @@ export interface RegisterSchema {
captcha_key?: string; captcha_key?: string;
promotional_email_opt_in?: boolean; promotional_email_opt_in?: boolean;
// part of pomelo
unique_username_registration?: boolean;
global_name?: string;
} }

View File

@ -19,94 +19,66 @@
import jwt, { VerifyOptions } from "jsonwebtoken"; import jwt, { VerifyOptions } from "jsonwebtoken";
import { Config } from "./Config"; import { Config } from "./Config";
import { User } from "../entities"; import { User } from "../entities";
// TODO: dont use deprecated APIs lol
import {
FindOptionsRelationByString,
FindOptionsSelectByString,
} from "typeorm";
export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] }; export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
export type UserTokenData = { export type UserTokenData = {
user: User; user: User;
decoded: { id: string; iat: number }; decoded: { id: string; iat: number; email?: string };
}; };
async function checkEmailToken( export const checkToken = (
decoded: jwt.JwtPayload,
): Promise<UserTokenData> {
// 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(
token: string, token: string,
jwtSecret: string, opts?: {
isEmailVerification = false, select?: FindOptionsSelectByString<User>;
): Promise<UserTokenData> { relations?: FindOptionsRelationByString;
return new Promise((res, rej) => { },
token = token.replace("Bot ", ""); ): Promise<UserTokenData> =>
token = token.replace("Bearer ", ""); new Promise((resolve, reject) => {
/** jwt.verify(
in spacebar, even with instances that have bot distinction; we won't enforce "Bot" prefix, token,
as we don't really have separate pathways for bots 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) => { const user = await User.findOne({
if (err || !decoded) return rej("Invalid Token"); where: decoded.email
if ( ? { email: decoded.email }
typeof decoded == "string" || : { id: decoded.id },
!("id" in decoded) || select: [
!decoded.iat ...(opts?.select || []),
) "bot",
return rej("Invalid Token"); // will never happen, just for typings. "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({ // we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
where: { id: decoded.id }, if (
select: ["data", "bot", "disabled", "deleted", "rights"], 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 return resolve({ decoded, user });
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);
});
}); });
}
export async function generateToken(id: string, email?: string) { export async function generateToken(id: string, email?: string) {
const iat = Math.floor(Date.now() / 1000); const iat = Math.floor(Date.now() / 1000);