import GetPermissions from "../../permissions.js"; import { jsonError } from "../../common.js"; import tokenPrefixes from "../../../data/token_prefixes.json"; async function generateTokenHash(token: string): Promise { const hash = await crypto.subtle.digest( "SHA-512", new TextEncoder().encode(token), ); return btoa(String.fromCharCode(...new Uint8Array(hash))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); } export async function onRequestDelete(context: RequestContext) { const cookies = context.request.headers.get("cookie")?.split("; "); if (!cookies) return jsonError("Not logged in", 401); for (const cookie of cookies) { const [name, value] = cookie.split("="); if (name !== "_s") continue; await context.env.DATA.delete(`auth_${await generateTokenHash(value)}`); return new Response(null, { headers: { "clear-site-data": '"cookies"', }, status: 204, }); } } export async function onRequestGet(context: RequestContext) { const { host, protocol, searchParams } = new URL(context.request.url); const code = searchParams.get("code"); const state = searchParams.get("state"); if (!code) return jsonError("Missing code", 400); if (!state) return jsonError("Missing state", 400); const stateRedirect = await context.env.DATA.get(`state_${state}`); if (!stateRedirect) return jsonError("Invalid state", 400); const tokenReq = await fetch("https://discord.com/api/oauth2/token", { body: new URLSearchParams({ code, grant_type: "authorization_code", redirect_uri: `${protocol}//${host}/api/auth/session`, }).toString(), headers: { authorization: `Basic ${btoa( context.env.DISCORD_ID + ":" + context.env.DISCORD_SECRET, )}`, "content-type": "application/x-www-form-urlencoded", }, method: "POST", }); if (!tokenReq.ok) { console.log(await tokenReq.text()); return jsonError("Failed to redeem code", 500); } const tokenData: { access_token: string; expires_in: number; refresh_token: string; scope: string; token_type: string; } = await tokenReq.json(); if (tokenData.scope.search("guilds.members.read") === -1) return jsonError("Do not touch the scopes!", 400); const oauthData = { ...tokenData, refresh_at: Date.now() + tokenData.expires_in * 1000 - 86400000, }; const userReq = await fetch("https://discord.com/api/v10/users/@me", { headers: { authorization: `Bearer ${tokenData.access_token}`, }, }); if (!userReq.ok) { console.log(await userReq.text()); return jsonError("Failed to retrieve user", 500); } const userData: { [k: string]: any } = await userReq.json(); const serverMemberReq = await fetch( "https://discord.com/api/v10/users/@me/guilds/242263977986359297/member", { headers: { authorization: `Bearer ${tokenData.access_token}`, }, }, ); const memberData: { [k: string]: any } = await serverMemberReq.json(); if (serverMemberReq.ok) { userData.permissions = await GetPermissions( userData.id, context, memberData.roles, ); userData.roles = memberData.roles; } else { userData.permissions = await GetPermissions(userData.id, context); } const selectedTokenStart = tokenPrefixes[Math.round(Math.random() * (tokenPrefixes.length - 1))] + "_"; const authToken = selectedTokenStart + `${crypto.randomUUID()}${crypto.randomUUID()}${crypto.randomUUID()}${crypto.randomUUID()}`.replaceAll( "-", "", ); const tokenHash = await generateTokenHash(authToken); await context.env.DATA.put(`auth_${tokenHash}`, JSON.stringify(userData), { expirationTtl: 2419200, }); await context.env.DATA.put( `oauthcredentials_${userData.id}`, JSON.stringify(oauthData), { expirationTtl: 1209600, }, ); return new Response(null, { headers: { location: stateRedirect, "set-cookie": `_s=${authToken}; HttpOnly; Max-Age=${tokenData.expires_in}; Path=/; SameSite=Lax; Secure`, }, status: 302, }); }