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)
-