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/`
|
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,
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
Loading…
Reference in New Issue
Block a user