Custom oauth lib
The package used is deprecated. And I want to be able to debug oauth errors better.
This commit is contained in:
Generated
-6
@@ -7,7 +7,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@redis/client": "^5.1.0",
|
"@redis/client": "^5.1.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"discord-oauth2": "^2.12.1",
|
|
||||||
"discord.js": "^14.19.3",
|
"discord.js": "^14.19.3",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
@@ -2261,11 +2260,6 @@
|
|||||||
"scripts/actions/documentation"
|
"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": {
|
"node_modules/discord.js": {
|
||||||
"version": "14.19.3",
|
"version": "14.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.19.3.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@redis/client": "^5.1.0",
|
"@redis/client": "^5.1.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"discord-oauth2": "^2.12.1",
|
|
||||||
"discord.js": "^14.19.3",
|
"discord.js": "^14.19.3",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export * from './message-utils';
|
|||||||
export * from './ms-to-human';
|
export * from './ms-to-human';
|
||||||
export * from './name-sync';
|
export * from './name-sync';
|
||||||
export * from './note-utils';
|
export * from './note-utils';
|
||||||
|
export * from './oauth2';
|
||||||
export * from './record-utils';
|
export * from './record-utils';
|
||||||
export * from './refresh-commands';
|
export * from './refresh-commands';
|
||||||
export * from './search-regex';
|
export * from './search-regex';
|
||||||
|
|||||||
@@ -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<TokenResponse> {
|
||||||
|
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<DiscordUser> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-13
@@ -1,5 +1,4 @@
|
|||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
import DiscordOAuth2 from 'discord-oauth2';
|
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { Database } from '../shared/Database';
|
import { Database } from '../shared/Database';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
@@ -11,7 +10,7 @@ import { Client } from 'discord.js';
|
|||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import { fixPings, removeIssueLinks } from '../utils/github-user-utils';
|
import { fixPings, removeIssueLinks } from '../utils/github-user-utils';
|
||||||
import { logDebug } from '../utils/debug-utils';
|
import { logDebug } from '../utils/debug-utils';
|
||||||
import { AltData, comprehensiveAltLookupFromE621 } from '../utils';
|
import { AltData, comprehensiveAltLookupFromE621, DiscordOAuth2 } from '../utils';
|
||||||
|
|
||||||
declare module 'express-session' {
|
declare module 'express-session' {
|
||||||
interface SessionData {
|
interface SessionData {
|
||||||
@@ -25,12 +24,15 @@ const GITHUB_REPO_ID = 169334303;
|
|||||||
const DEV_BASE_URL = `http://localhost:${config.PORT}`;
|
const DEV_BASE_URL = `http://localhost:${config.PORT}`;
|
||||||
const PROD_BASE_URL = 'https://discord.e621.net';
|
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 PAGE_TEMPLATE = fs.readFileSync(path.join(__dirname, 'templates', 'page.html'), { encoding: 'utf-8' });
|
||||||
|
|
||||||
const oauth = new DiscordOAuth2({
|
const oauth = new DiscordOAuth2({
|
||||||
clientId: config.DISCORD_CLIENT_ID!,
|
clientId: config.DISCORD_CLIENT_ID!,
|
||||||
clientSecret: config.DISCORD_CLIENT_SECRET!,
|
clientSecret: config.DISCORD_CLIENT_SECRET!,
|
||||||
redirectUri: `${config.DEV_MODE ? DEV_BASE_URL : PROD_BASE_URL}/callback`,
|
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')
|
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);
|
const id = Number(userId);
|
||||||
|
|
||||||
tokenResponse = await oauth.tokenRequest({
|
tokenResponse = await oauth.getAccessToken(code, OAUTH_SCOPES);
|
||||||
code,
|
|
||||||
scope: 'identify guilds.join',
|
|
||||||
grantType: 'authorization_code'
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await oauth.getUser(tokenResponse.access_token);
|
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({
|
await oauth.addMember({
|
||||||
accessToken: tokenResponse.access_token,
|
accessToken: tokenResponse.access_token,
|
||||||
botToken: config.DISCORD_TOKEN!,
|
|
||||||
guildId: config.DISCORD_GUILD_ID!,
|
guildId: config.DISCORD_GUILD_ID!,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
nickname: username
|
nickname: username
|
||||||
@@ -103,6 +100,12 @@ async function handleInitial(req: Request, res: Response): Promise<any> {
|
|||||||
|
|
||||||
const oauthState = crypto.randomBytes(16).toString('hex');
|
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.username = username as string;
|
||||||
req.session.userId = user_id as string;
|
req.session.userId = user_id as string;
|
||||||
req.session.oauthState = oauthState;
|
req.session.oauthState = oauthState;
|
||||||
@@ -114,10 +117,7 @@ async function handleInitial(req: Request, res: Response): Promise<any> {
|
|||||||
return sendInteralServerError(res);
|
return sendInteralServerError(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect(oauth.generateAuthUrl({
|
res.redirect(oauthUrl);
|
||||||
state: oauthState,
|
|
||||||
scope: ['identify', 'guilds.join']
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ async function handleCallback(req: Request, res: Response): Promise<any> {
|
|||||||
const response = await joinGuild(code, userId, username);
|
const response = await joinGuild(code, userId, username);
|
||||||
if (response == JoinResponse.Error) {
|
if (response == JoinResponse.Error) {
|
||||||
console.error(`Error joining user: ${username} (${userId})`);
|
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) {
|
} else if (response == JoinResponse.Banned) {
|
||||||
return sendForbidden(res, 'User is banned.');
|
return sendForbidden(res, 'User is banned.');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user