mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-27 17:27:32 +08:00
added a state token to make sure we only link to the user that started the oauth2 flow
This commit is contained in:
parent
56e3943c04
commit
f72c3a6702
8 changed files with 82 additions and 31 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue