fix: actually validate oauth sob
This commit is contained in:
parent
dfeda15cd6
commit
f036998fad
@ -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<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 = {
|
||||
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<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
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
@ -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
|
||||
|
@ -16,10 +16,6 @@
|
||||
just fill the post in and click on your preferred auth method to post
|
||||
</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">
|
||||
<div class="entry entryflex">
|
||||
<div class="flex flex-row">
|
||||
@ -34,15 +30,7 @@
|
||||
required
|
||||
/>
|
||||
<p class="place-self-end text-sm font-monospace">
|
||||
--- posted by <input
|
||||
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
|
||||
/>
|
||||
--- posted by ...
|
||||
</p>
|
||||
</div>
|
||||
<div class="entry flex flex-wrap gap-1.5 p-1">
|
||||
|
Loading…
Reference in New Issue
Block a user