added a state token to make sure we only link to the user that started the oauth2 flow

This commit is contained in:
Miodec 2022-11-09 15:44:31 +01:00
parent 56e3943c04
commit f72c3a6702
8 changed files with 82 additions and 31 deletions

View file

@ -3,7 +3,7 @@ import * as UserDAL from "../../dal/user";
import MonkeyError from "../../utils/error";
import Logger from "../../utils/logger";
import { MonkeyResponse } from "../../utils/monkey-response";
import { getDiscordUser } from "../../utils/discord";
import * as DiscordUtils from "../../utils/discord";
import { buildAgentLog, sanitizeString } from "../../utils/misc";
import * as George from "../../tasks/george";
import admin from "firebase-admin";
@ -14,6 +14,8 @@ import { deleteConfig } from "../../dal/config";
import { verify } from "../../utils/captcha";
import * as LeaderboardsDAL from "../../dal/leaderboards";
import { purgeUserFromDailyLeaderboards } from "../../utils/daily-leaderboards";
import { randomBytes } from "crypto";
import * as RedisClient from "../../init/redis";
async function verifyCaptcha(captcha: string): Promise<void> {
if (!(await verify(captcha))) {
@ -210,21 +212,52 @@ export async function getUser(
return new MonkeyResponse("User data retrieved", userData);
}
export async function getOauthLink(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const connection = RedisClient.getConnection();
if (!connection) {
throw new MonkeyError(500, "Redis connection not found");
}
const { uid } = req.ctx.decodedToken;
const token = randomBytes(10).toString("hex");
//add the token uid pair to reids
await connection.setex(`discordoauth:${uid}`, 60, token);
//build the url
const url = DiscordUtils.getOauthLink();
//return
return new MonkeyResponse("Discord oauth link generated", {
url: `${url}&state=${token}`,
});
}
export async function linkDiscord(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const connection = RedisClient.getConnection();
if (!connection) {
throw new MonkeyError(500, "Redis connection not found");
}
const { uid } = req.ctx.decodedToken;
const { tokenType, accessToken } = req.body;
const { tokenType, accessToken, state } = req.body;
const redisToken = await connection.getdel(`discordoauth:${uid}`);
if (!redisToken || redisToken !== state) {
throw new MonkeyError(403, "Invalid user token");
}
const userInfo = await UserDAL.getUser(uid, "link discord");
if (userInfo.banned) {
throw new MonkeyError(403, "Banned accounts cannot link with Discord");
}
const { id: discordId, avatar: discordAvatar } = await getDiscordUser(
tokenType,
accessToken
);
const { id: discordId, avatar: discordAvatar } =
await DiscordUtils.getDiscordUser(tokenType, accessToken);
if (userInfo.discordId) {
await UserDAL.linkDiscord(uid, userInfo.discordId, discordAvatar);

View file

@ -343,6 +343,14 @@ const requireDiscordIntegrationEnabled = validateConfiguration({
invalidMessage: "Discord integration is not available at this time",
});
router.get(
"/discord/oauth",
requireDiscordIntegrationEnabled,
authenticateRequest(),
RateLimit.userDiscordLink,
asyncHandler(UserController.getOauthLink)
);
router.post(
"/discord/link",
requireDiscordIntegrationEnabled,
@ -352,6 +360,7 @@ router.post(
body: {
tokenType: joi.string().required(),
accessToken: joi.string().required(),
state: joi.string().length(20).required(),
},
}),
asyncHandler(UserController.linkDiscord)

View file

@ -32,3 +32,11 @@ export async function getDiscordUser(
return (await response.json()) as DiscordUser;
}
export function getOauthLink(): string {
return `${BASE_URL}/oauth2/authorize?client_id=798272335035498557&redirect_uri=${
process.env.MODE === "dev"
? `http%3A%2F%2Flocalhost%3A3000%2Fverify`
: `https%3A%2F%2Fmonkeytype.com%2Fverify`
}&response_type=token&scope=identify`;
}

View file

@ -153,9 +153,17 @@ export default class Users {
return await this.httpClient.post(`${BASE_PATH}/customThemes`, { payload });
}
async linkDiscord(tokenType: string, accessToken: string): Ape.EndpointData {
async getOauthLink(): Ape.EndpointData {
return await this.httpClient.get(`${BASE_PATH}/discord/oauth`);
}
async linkDiscord(
tokenType: string,
accessToken: string,
state: string
): Ape.EndpointData {
return await this.httpClient.post(`${BASE_PATH}/discord/link`, {
payload: { tokenType, accessToken },
payload: { tokenType, accessToken, state },
});
}

View file

@ -15,6 +15,7 @@ import * as ApeKeysPopup from "../popups/ape-keys-popup";
import * as CookiePopup from "../popups/cookie-popup";
import Page from "./page";
import { Auth } from "../firebase";
import Ape from "../ape";
interface SettingsGroups {
[key: string]: SettingsGroup;
@ -1104,6 +1105,15 @@ $(".pageSettings .section.autoSwitchThemeInputs").on(
}
);
$(".pageSettings .section.discordIntegration .getLinkAndGoToOauth").on(
"click",
() => {
Ape.users.getOauthLink().then((res) => {
window.open(res.data.url, "_self");
});
}
);
let configEventDisabled = false;
export function setEventDisabled(value: boolean): void {
configEventDisabled = value;

View file

@ -47,16 +47,6 @@ if (window.location.hostname === "localhost") {
$("body").append(
`<div class="devIndicator tl">local</div><div class="devIndicator br">local</div>`
);
$(".pageSettings .discordIntegration .buttons a").attr(
"href",
"https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fverify&response_type=token&scope=identify"
);
$(
".pageSettings .discordIntegration .info #discordButtonGroup #updateDiscordAvatarButton"
).attr(
"href",
"https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fverify&response_type=token&scope=identify"
);
}
//stop space scrolling

View file

@ -19,10 +19,10 @@ export async function linkDiscord(hashOverride: string): Promise<void> {
history.replaceState(null, "", "/");
const accessToken = fragment.get("access_token") as string;
const tokenType = fragment.get("token_type") as string;
const state = fragment.get("state") as string;
Loader.show();
const response = await Ape.users.linkDiscord(tokenType, accessToken);
const response = await Ape.users.linkDiscord(tokenType, accessToken, state);
Loader.hide();
if (response.status !== 200) {

View file

@ -53,13 +53,7 @@
give you a role.
</div>
<div class="buttons">
<a
class="button"
href="https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=https%3A%2F%2Fmonkeytype.com%2Fverify&response_type=token&scope=identify"
style="text-decoration: none"
>
Link with Discord
</a>
<div class="button getLinkAndGoToOauth">Link with Discord</div>
</div>
<div class="info hidden">
<div>
@ -67,14 +61,13 @@
Your accounts are linked!
</div>
<div id="discordButtonGroup">
<a
<div
id="updateDiscordAvatarButton"
href="https://discord.com/api/oauth2/authorize?client_id=798272335035498557&redirect_uri=https%3A%2F%2Fmonkeytype.com%2Fverify&response_type=token&scope=identify"
class="textButton"
class="textButton getLinkAndGoToOauth"
>
<i class="fas fa-sync-alt" aria-hidden="true"></i>
Update avatar
</a>
</div>
<div id="unlinkDiscordButton" class="textButton">
<i class="fas fa-unlink" aria-hidden="true"></i>
Unlink