From f036998fadc031bc0a49cad7bab48b3cdea4985b Mon Sep 17 00:00:00 2001 From: Yusuf Bera Ertan Date: Sat, 24 Aug 2024 17:28:19 +0300 Subject: [PATCH] fix: actually validate oauth sob --- src/lib/guestbookAuth.ts | 83 ++++++++++++++++++++++++++++ src/routes/guestbook/+page.server.ts | 48 +++++++++++----- src/routes/guestbook/+page.svelte | 14 +---- 3 files changed, 117 insertions(+), 28 deletions(-) diff --git a/src/lib/guestbookAuth.ts b/src/lib/guestbookAuth.ts index 3bf9d67..191ed4c 100644 --- a/src/lib/guestbookAuth.ts +++ b/src/lib/guestbookAuth.ts @@ -6,20 +6,89 @@ import base64url from "base64url"; export const callbackUrl = `${PUBLIC_BASE_URL}/guestbook/` +interface TokenResponse { + accessToken: string, + tokenType: string, + scope: string, +} + export const discord = { + name: 'discord', getAuthUrl: (state: string, scopes: string[] = []) => { const client_id = env.DISCORD_CLIENT_ID const redir_uri = encodeURIComponent(callbackUrl) const scope = scopes.join("+") return `https://discord.com/oauth2/authorize?client_id=${client_id}&response_type=code&redirect_uri=${redir_uri}&scope=${scope}&state=${state}` + }, + getToken: async (code: string): Promise => { + const api = `https://discord.com/api/oauth2/token` + const body = new URLSearchParams({ + client_id: env.DISCORD_CLIENT_ID, + client_secret: env.DISCORD_CLIENT_SECRET, + grant_type: 'authorization_code', + redirect_uri: callbackUrl, + code, + }) + const resp = await fetch(api, { method: 'POST', body }) + if (resp.status !== 200) { + throw new Error("woopsies, couldnt get oauth token") + } + const tokenResp: any = await resp.json() + return { + accessToken: tokenResp.access_token, + tokenType: tokenResp.token_type, + scope: tokenResp.scope, + } + }, + identifyToken: async (tokenResp: TokenResponse): Promise => { + const api = `https://discord.com/api/users/@me` + const resp = await fetch(api, {headers: { + 'Authorization': `${tokenResp.tokenType} ${tokenResp.accessToken}` + }}) + if (resp.status !== 200) { + throw new Error("woopsies, couldnt validate access token") + } + const body = await resp.json() + return body.username } } export const github = { + name: 'github', getAuthUrl: (state: string, scopes: string[] = []) => { const client_id = env.GITHUB_CLIENT_ID const redir_uri = encodeURIComponent(callbackUrl) const scope = encodeURIComponent(scopes.join(" ")) return `https://github.com/login/oauth/authorize?client_id=${client_id}&redirect_uri=${redir_uri}&scope=${scope}&state=${state}` + }, + getToken: async (code: string): Promise => { + const api = `https://discord.com/api/oauth2/token` + const body = new URLSearchParams({ + client_id: env.GITHUB_CLIENT_ID, + client_secret: env.GITHUB_CLIENT_SECRET, + redirect_uri: callbackUrl, + code, + }) + const resp = await fetch(api, { method: 'POST', body, headers: { 'Accept': 'application/json' } }) + if (resp.status !== 200) { + throw new Error("woopsies, couldnt get oauth token") + } + const tokenResp: any = await resp.json() + return { + accessToken: tokenResp.access_token, + tokenType: tokenResp.token_type, + scope: tokenResp.scope, + } + }, + identifyToken: async (tokenResp: TokenResponse): Promise => { + const api = `https://api.github.com/user` + const resp = await fetch(api, {headers: { + 'Authorization': `${tokenResp.tokenType} ${tokenResp.accessToken}` + }}) + if (resp.status !== 200) { + throw new Error("woopsies, couldnt validate access token") + } + const body = await resp.json() + return body.login } } @@ -57,9 +126,23 @@ export const extractCode = (url: URL, cookies: Cookies) => { return code } +export const getAuthClient = (name: string) => { + switch (name) { + case "discord": + return discord + + case "github": + return github + + default: + return null + } +} + export default { callbackUrl, discord, github, createAuthUrl, extractCode, + getAuthClient, } \ No newline at end of file diff --git a/src/routes/guestbook/+page.server.ts b/src/routes/guestbook/+page.server.ts index 9416e54..3c06ae8 100644 --- a/src/routes/guestbook/+page.server.ts +++ b/src/routes/guestbook/+page.server.ts @@ -24,20 +24,16 @@ const scopeCookies = (cookies: Cookies) => { const postAction = (client: any, scopes: string[]) => { return async ({ request, cookies }: { request: Request, cookies: Cookies }) => { - const form = await request.formData() - const author = form.get("author")?.toString().substring(0, 32).replace(/([^_a-z0-9]+)/gi, '') - const content = form.get("content")?.toString().substring(0, 512) const scopedCookies = scopeCookies(cookies) - if (author === undefined || content === undefined) { - scopedCookies.set("sendError", "one of author or content fields are missing") - redirect(303, auth.callbackUrl) - } - if (['dusk', 'yusdacra'].includes(author.trim())) { - scopedCookies.set("sendError", "author cannot be dusk or yusdacra (those are my names choose something else smh)") + scopedCookies.set("postAuth", client.name) + const form = await request.formData() + const content = form.get("content")?.toString().substring(0, 512) + if (content === undefined) { + scopedCookies.set("sendError", "content field is missing") redirect(303, auth.callbackUrl) } // save form content in a cookie - const params = new URLSearchParams({ author, content }) + const params = new URLSearchParams({ content }) scopedCookies.set("postData", params.toString()) // get auth url to redirect user to const authUrl = auth.createAuthUrl((state) => client.getAuthUrl(state, scopes), cookies) @@ -62,9 +58,11 @@ export async function load({ url, fetch, cookies }) { getRatelimited: false, } const rawPostData = scopedCookies.get("postData") || null - if (rawPostData !== null) { + const postAuth = scopedCookies.get("postAuth") || null + if (rawPostData !== null && postAuth !== null) { // delete the postData cookie after we got it cause we dont need it anymore scopedCookies.delete("postData") + scopedCookies.delete("postAuth") // check if we are landing from an auth from a post action let code: string | null = null // try to get the code, fails if invalid oauth request @@ -73,12 +71,32 @@ export async function load({ url, fetch, cookies }) { } catch (err: any) { data.sendError = err.toString() } - // if we do have a code, then actually make the put request to guestbook server - if (code !== null) { + // if we do have a code, then make the access token request + const authClient = auth.getAuthClient(postAuth) + if (authClient !== null && code !== null) { + // get and validate access token, also get username + let author: string + try { + const tokenResp = await authClient.getToken(code) + author = await authClient.identifyToken(tokenResp) + } catch(err: any) { + scopedCookies.set("sendError", `oauth failed: ${err.toString()}`) + redirect(303, auth.callbackUrl) + } let respRaw: Response try { const postData = new URLSearchParams(rawPostData) - respRaw = await fetch(`${GUESTBOOK_BASE_URL}`, { method: 'POST', body: postData }) + // set author to the identified value we got + postData.set('author', author) + // return error if content was not set or if empty + const content = postData.get('content') + if (content === null || content.trim().length === 0) { + scopedCookies.set("sendError", `content field was empty`) + redirect(303, auth.callbackUrl) + } + // set content, make sure to trim it + postData.set('content', content.substring(0, 512).trim()) + respRaw = await fetch(GUESTBOOK_BASE_URL, { method: 'POST', body: postData }) } catch (err: any) { scopedCookies.set("sendError", `${err.toString()} (is guestbook server running?)`) redirect(303, auth.callbackUrl) @@ -97,7 +115,7 @@ export async function load({ url, fetch, cookies }) { data.page = Math.max(data.page, 1) let respRaw: Response try { - respRaw = await fetch(GUESTBOOK_BASE_URL + "/" + data.page) + respRaw = await fetch(`${GUESTBOOK_BASE_URL}/${data.page}`) } catch (err: any) { data.getError = `${err.toString()} (is guestbook server running?)` return data diff --git a/src/routes/guestbook/+page.svelte b/src/routes/guestbook/+page.svelte index 1ca67cb..3ae17c6 100644 --- a/src/routes/guestbook/+page.svelte +++ b/src/routes/guestbook/+page.svelte @@ -16,10 +16,6 @@ just fill the post in and click on your preferred auth method to post

rules: be a good human bean pretty please

-

- (note: the author name must only include alphanumerical characters or underscore, and must - be less than 32 characters) -

@@ -34,15 +30,7 @@ required />

- --- posted by + --- posted by ...