From 9d18b971446576109ae992163ad143be7ddf44a3 Mon Sep 17 00:00:00 2001 From: Tarrgon <61888458+Tarrgon@users.noreply.github.com> Date: Sat, 13 Dec 2025 17:58:10 -0500 Subject: [PATCH] Custom oauth lib The package used is deprecated. And I want to be able to debug oauth errors better. --- package-lock.json | 6 -- package.json | 1 - src/utils/index.ts | 1 + src/utils/oauth2.ts | 124 +++++++++++++++++++++++++++++++++++++++++ src/webserver/index.ts | 26 ++++----- 5 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 src/utils/oauth2.ts diff --git a/package-lock.json b/package-lock.json index 76959e2..000fb65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "dependencies": { "@redis/client": "^5.1.0", "body-parser": "^2.2.0", - "discord-oauth2": "^2.12.1", "discord.js": "^14.19.3", "dotenv": "^16.5.0", "express": "^5.1.0", @@ -2261,11 +2260,6 @@ "scripts/actions/documentation" ] }, - "node_modules/discord-oauth2": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/discord-oauth2/-/discord-oauth2-2.12.1.tgz", - "integrity": "sha512-/Um39bRxVjcGHUu1YaTLangZvZveXjsX4BNsa1Iyd6OQG0jL972IBQGKD0mYqQswxC3bT+hqWSouabfI2RdaZA==" - }, "node_modules/discord.js": { "version": "14.19.3", "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.19.3.tgz", diff --git a/package.json b/package.json index a9a50f2..12c7cc2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "dependencies": { "@redis/client": "^5.1.0", "body-parser": "^2.2.0", - "discord-oauth2": "^2.12.1", "discord.js": "^14.19.3", "dotenv": "^16.5.0", "express": "^5.1.0", diff --git a/src/utils/index.ts b/src/utils/index.ts index 91601b5..2c97b9c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -16,6 +16,7 @@ export * from './message-utils'; export * from './ms-to-human'; export * from './name-sync'; export * from './note-utils'; +export * from './oauth2'; export * from './record-utils'; export * from './refresh-commands'; export * from './search-regex'; diff --git a/src/utils/oauth2.ts b/src/utils/oauth2.ts new file mode 100644 index 0000000..3368826 --- /dev/null +++ b/src/utils/oauth2.ts @@ -0,0 +1,124 @@ +type ClientOptions = { + clientId: string + clientSecret: string + clientToken: string + redirectUri: string + credentials: string +}; + +type GenerateUrlParameters = { + state: string + scope: string[] + type: 'code' | 'token' +}; + +type TokenResponse = { + access_token: string + token_type: string + expires_in: number + refresh_token: string + scope: string +} + +type DiscordUser = { + id: string + username: string + // bunch of other stuff we don't use +} + +type AddMemberOptions = { + accessToken: string + botToken?: string + guildId: string + userId: string + nickname?: string +} + +const OAUTH_BASE_URL = 'https://discord.com/oauth2'; +const OAUTH_API_BASE_URL = 'https://discord.com/api/oauth2'; +const API_BASE_URL = 'https://discord.com/api'; + +export class DiscordOAuth2 { + constructor(private options: ClientOptions) { } + + generateOauth2Url(options: GenerateUrlParameters) { + const url = new URL(`${OAUTH_BASE_URL}/authorize`); + + const params = new URLSearchParams({ + client_id: this.options.clientId, + response_type: options.type, + redirect_uri: this.options.redirectUri, + scope: options.scope.join('+'), + state: options.state + }); + + url.search = params.toString(); + + return url.toString(); + } + + async getAccessToken(code: string, scope: string[]): Promise { + const res = await fetch(`${OAUTH_API_BASE_URL}/token`, { + method: 'POST', + body: new URLSearchParams({ + client_id: this.options.clientId, + client_secret: this.options.clientSecret, + code, + grant_type: 'authorization_code', + redirect_uri: this.options.redirectUri, + scope: scope.join(' ') + }).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json' + } + }); + + const data = await res.json(); + return data as TokenResponse; + } + + async getUser(accessToken: string): Promise { + const res = await fetch(`${API_BASE_URL}/users/@me`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json' + } + }); + + return await res.json() as DiscordUser; + } + + async addMember(options: AddMemberOptions) { + const res = await fetch(`${API_BASE_URL}/guilds/${options.guildId}/members/${options.userId}`, { + method: 'PUT', + body: JSON.stringify({ + nick: options.nickname, + access_token: options.accessToken + }), + headers: { + 'Content-Type': 'application/json', + Authorization: `Bot ${this.options.clientToken}`, + Accept: 'application/json' + } + }); + + return await res.json(); + } + + async revokeToken(token: string) { + const res = await fetch(`${OAUTH_API_BASE_URL}/token/revoke`, { + method: 'POST', + body: new URLSearchParams({ + token + }).toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${this.options.credentials}`, + Accept: 'application/json' + } + }); + + return await res.json(); + } +} \ No newline at end of file diff --git a/src/webserver/index.ts b/src/webserver/index.ts index 0bb3a11..2fa7b05 100644 --- a/src/webserver/index.ts +++ b/src/webserver/index.ts @@ -1,5 +1,4 @@ import express, { Request, Response } from 'express'; -import DiscordOAuth2 from 'discord-oauth2'; import { config } from '../config'; import { Database } from '../shared/Database'; import crypto from 'crypto'; @@ -11,7 +10,7 @@ import { Client } from 'discord.js'; import bodyParser from 'body-parser'; import { fixPings, removeIssueLinks } from '../utils/github-user-utils'; import { logDebug } from '../utils/debug-utils'; -import { AltData, comprehensiveAltLookupFromE621 } from '../utils'; +import { AltData, comprehensiveAltLookupFromE621, DiscordOAuth2 } from '../utils'; declare module 'express-session' { interface SessionData { @@ -25,12 +24,15 @@ const GITHUB_REPO_ID = 169334303; const DEV_BASE_URL = `http://localhost:${config.PORT}`; const PROD_BASE_URL = 'https://discord.e621.net'; +const OAUTH_SCOPES = ['identify', 'guilds.join']; + const PAGE_TEMPLATE = fs.readFileSync(path.join(__dirname, 'templates', 'page.html'), { encoding: 'utf-8' }); const oauth = new DiscordOAuth2({ clientId: config.DISCORD_CLIENT_ID!, clientSecret: config.DISCORD_CLIENT_SECRET!, redirectUri: `${config.DEV_MODE ? DEV_BASE_URL : PROD_BASE_URL}/callback`, + clientToken: config.DISCORD_TOKEN!, credentials: Buffer.from(`${config.DISCORD_CLIENT_ID!}:${config.DISCORD_CLIENT_SECRET!}`).toString('base64') }); @@ -48,11 +50,7 @@ async function joinGuild(code: string, userId: string, username: string): Promis const id = Number(userId); - tokenResponse = await oauth.tokenRequest({ - code, - scope: 'identify guilds.join', - grantType: 'authorization_code' - }); + tokenResponse = await oauth.getAccessToken(code, OAUTH_SCOPES); const user = await oauth.getUser(tokenResponse.access_token); @@ -64,7 +62,6 @@ async function joinGuild(code: string, userId: string, username: string): Promis await oauth.addMember({ accessToken: tokenResponse.access_token, - botToken: config.DISCORD_TOKEN!, guildId: config.DISCORD_GUILD_ID!, userId: user.id, nickname: username @@ -103,6 +100,12 @@ async function handleInitial(req: Request, res: Response): Promise { const oauthState = crypto.randomBytes(16).toString('hex'); + const oauthUrl = await oauth.generateOauth2Url({ + state: oauthState, + scope: OAUTH_SCOPES, + type: 'code' + }); + req.session.username = username as string; req.session.userId = user_id as string; req.session.oauthState = oauthState; @@ -114,10 +117,7 @@ async function handleInitial(req: Request, res: Response): Promise { return sendInteralServerError(res); } - res.redirect(oauth.generateAuthUrl({ - state: oauthState, - scope: ['identify', 'guilds.join'] - })); + res.redirect(oauthUrl); }); } @@ -146,7 +146,7 @@ async function handleCallback(req: Request, res: Response): Promise { const response = await joinGuild(code, userId, username); if (response == JoinResponse.Error) { console.error(`Error joining user: ${username} (${userId})`); - return sendInteralServerError(res, 'Unable to join user to guild.'); + return sendInteralServerError(res, 'Unable to join user to guild. Retry later. If issue persists, please contact staff.'); } else if (response == JoinResponse.Banned) { return sendForbidden(res, 'User is banned.'); }