fix: actually validate oauth sob

This commit is contained in:
dusk 2024-08-24 17:28:19 +03:00
parent dfeda15cd6
commit f036998fad
Signed by: dusk
SSH Key Fingerprint: SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw
3 changed files with 117 additions and 28 deletions

View File

@ -6,20 +6,89 @@ import base64url from "base64url";
export const callbackUrl = `${PUBLIC_BASE_URL}/guestbook/` export const callbackUrl = `${PUBLIC_BASE_URL}/guestbook/`
interface TokenResponse {
accessToken: string,
tokenType: string,
scope: string,
}
export const discord = { export const discord = {
name: 'discord',
getAuthUrl: (state: string, scopes: string[] = []) => { getAuthUrl: (state: string, scopes: string[] = []) => {
const client_id = env.DISCORD_CLIENT_ID const client_id = env.DISCORD_CLIENT_ID
const redir_uri = encodeURIComponent(callbackUrl) const redir_uri = encodeURIComponent(callbackUrl)
const scope = scopes.join("+") 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}` 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<TokenResponse> => {
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<string> => {
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 = { export const github = {
name: 'github',
getAuthUrl: (state: string, scopes: string[] = []) => { getAuthUrl: (state: string, scopes: string[] = []) => {
const client_id = env.GITHUB_CLIENT_ID const client_id = env.GITHUB_CLIENT_ID
const redir_uri = encodeURIComponent(callbackUrl) const redir_uri = encodeURIComponent(callbackUrl)
const scope = encodeURIComponent(scopes.join(" ")) const scope = encodeURIComponent(scopes.join(" "))
return `https://github.com/login/oauth/authorize?client_id=${client_id}&redirect_uri=${redir_uri}&scope=${scope}&state=${state}` return `https://github.com/login/oauth/authorize?client_id=${client_id}&redirect_uri=${redir_uri}&scope=${scope}&state=${state}`
},
getToken: async (code: string): Promise<TokenResponse> => {
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<string> => {
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 return code
} }
export const getAuthClient = (name: string) => {
switch (name) {
case "discord":
return discord
case "github":
return github
default:
return null
}
}
export default { export default {
callbackUrl, callbackUrl,
discord, github, discord, github,
createAuthUrl, createAuthUrl,
extractCode, extractCode,
getAuthClient,
} }

View File

@ -24,20 +24,16 @@ const scopeCookies = (cookies: Cookies) => {
const postAction = (client: any, scopes: string[]) => { const postAction = (client: any, scopes: string[]) => {
return async ({ request, cookies }: { request: Request, cookies: Cookies }) => { 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) const scopedCookies = scopeCookies(cookies)
if (author === undefined || content === undefined) { scopedCookies.set("postAuth", client.name)
scopedCookies.set("sendError", "one of author or content fields are missing") const form = await request.formData()
redirect(303, auth.callbackUrl) const content = form.get("content")?.toString().substring(0, 512)
} if (content === undefined) {
if (['dusk', 'yusdacra'].includes(author.trim())) { scopedCookies.set("sendError", "content field is missing")
scopedCookies.set("sendError", "author cannot be dusk or yusdacra (those are my names choose something else smh)")
redirect(303, auth.callbackUrl) redirect(303, auth.callbackUrl)
} }
// save form content in a cookie // save form content in a cookie
const params = new URLSearchParams({ author, content }) const params = new URLSearchParams({ content })
scopedCookies.set("postData", params.toString()) scopedCookies.set("postData", params.toString())
// get auth url to redirect user to // get auth url to redirect user to
const authUrl = auth.createAuthUrl((state) => client.getAuthUrl(state, scopes), cookies) const authUrl = auth.createAuthUrl((state) => client.getAuthUrl(state, scopes), cookies)
@ -62,9 +58,11 @@ export async function load({ url, fetch, cookies }) {
getRatelimited: false, getRatelimited: false,
} }
const rawPostData = scopedCookies.get("postData") || null 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 // delete the postData cookie after we got it cause we dont need it anymore
scopedCookies.delete("postData") scopedCookies.delete("postData")
scopedCookies.delete("postAuth")
// check if we are landing from an auth from a post action // check if we are landing from an auth from a post action
let code: string | null = null let code: string | null = null
// try to get the code, fails if invalid oauth request // try to get the code, fails if invalid oauth request
@ -73,12 +71,32 @@ export async function load({ url, fetch, cookies }) {
} catch (err: any) { } catch (err: any) {
data.sendError = err.toString() data.sendError = err.toString()
} }
// if we do have a code, then actually make the put request to guestbook server // if we do have a code, then make the access token request
if (code !== null) { 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 let respRaw: Response
try { try {
const postData = new URLSearchParams(rawPostData) 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) { } catch (err: any) {
scopedCookies.set("sendError", `${err.toString()} (is guestbook server running?)`) scopedCookies.set("sendError", `${err.toString()} (is guestbook server running?)`)
redirect(303, auth.callbackUrl) redirect(303, auth.callbackUrl)
@ -97,7 +115,7 @@ export async function load({ url, fetch, cookies }) {
data.page = Math.max(data.page, 1) data.page = Math.max(data.page, 1)
let respRaw: Response let respRaw: Response
try { try {
respRaw = await fetch(GUESTBOOK_BASE_URL + "/" + data.page) respRaw = await fetch(`${GUESTBOOK_BASE_URL}/${data.page}`)
} catch (err: any) { } catch (err: any) {
data.getError = `${err.toString()} (is guestbook server running?)` data.getError = `${err.toString()} (is guestbook server running?)`
return data return data

View File

@ -16,10 +16,6 @@
just fill the post in and click on your preferred auth method to post just fill the post in and click on your preferred auth method to post
</p> </p>
<p>rules: be a good human bean pretty please</p> <p>rules: be a good human bean pretty please</p>
<p>
(note: the author name must only include alphanumerical characters or underscore, and must
be less than 32 characters)
</p>
<form method="post"> <form method="post">
<div class="entry entryflex"> <div class="entry entryflex">
<div class="flex flex-row"> <div class="flex flex-row">
@ -34,15 +30,7 @@
required required
/> />
<p class="place-self-end text-sm font-monospace"> <p class="place-self-end text-sm font-monospace">
--- posted by <input --- posted by ...
type="text"
name="author"
placeholder="author"
class="p-0 bg-inherit border-hidden max-w-[16ch] text-right text-sm text-shadow-white placeholder-shown:[text-shadow:none] [field-sizing:content]"
pattern="[_a-zA-Z0-9]+"
maxlength="32"
required
/>
</p> </p>
</div> </div>
<div class="entry flex flex-wrap gap-1.5 p-1"> <div class="entry flex flex-wrap gap-1.5 p-1">