diff --git a/src/components/islands/profile.jsx b/src/components/islands/profile.jsx index 88c6431a..5cdb246e 100644 --- a/src/components/islands/profile.jsx +++ b/src/components/islands/profile.jsx @@ -79,4 +79,4 @@ export default function Profile({ isAuthed, userData, authUrl }) { ); -} \ No newline at end of file +} diff --git a/src/lib/actions/do-auth.js b/src/lib/actions/do-auth.js index 7928b1da..39485009 100644 --- a/src/lib/actions/do-auth.js +++ b/src/lib/actions/do-auth.js @@ -22,7 +22,6 @@ export default async function doAuth(astroGlobal) { /** * Generate OAuth Url to start authorization flow - * @todo improvement: store `state` in cookie for later retrieval/comparison with auth `state` in `github/oauth/callback` * @param {{ path: string }} state */ function getAuthUrl(state) { @@ -39,10 +38,16 @@ export default async function doAuth(astroGlobal) { if (otherStates.length > 0) parsedState += `|${otherStates}`; } - const { url } = app.oauth.getWebFlowAuthorizationUrl({ + const { url, state: generatedState } = app.oauth.getWebFlowAuthorizationUrl({ state: parsedState, }); + cookies.set("oauthState", generatedState, { + expires: resolveCookieExpiryDate(600), + path: "/", + encode: (value) => encrypt(value), + }); + return url; } @@ -60,6 +65,14 @@ export default async function doAuth(astroGlobal) { } } + const storedState = cookies.get("oauthState", { + decode: (value) => decrypt(value), + }); + + if (storedState !== searchParams.get("state")) { + throw new Error("Invalid OAuth state"); + } + const userOctokit = app.getUserOctokit({ token: accessToken.value }); const { data } = await userOctokit.request("GET /user"); diff --git a/src/pages/api/github/oauth/callback.js b/src/pages/api/github/oauth/callback.js index 38d00a68..05a830d7 100644 --- a/src/pages/api/github/oauth/callback.js +++ b/src/pages/api/github/oauth/callback.js @@ -1,8 +1,10 @@ +import { decrypt } from "../../../../lib/utils/crypto.js"; + /** * GitHub OAuth Callback route handler * @param {import("astro").APIContext} context */ -export async function GET({ url: { searchParams }, redirect }) { +export async function GET({ url: { searchParams }, redirect, cookies }) { const code = searchParams.get("code"); const state = searchParams.get("state"); @@ -10,6 +12,16 @@ export async function GET({ url: { searchParams }, redirect }) { return new Response(null, { status: 400 }); } + const storedState = cookies.get("oauthState", { + decode: (value) => decrypt(value), + }); + + if (storedState !== state) { + return new Response("Invalid OAuth state", { status: 400 }); + } + + cookies.delete("oauthState"); + const path = state.includes("path") && state.split("|")[0].split(":")[1]; if (path) return redirect(`${path}?code=${code}`); diff --git a/src/pages/logout.astro b/src/pages/logout.astro index dddaeabf..7e3ef205 100644 --- a/src/pages/logout.astro +++ b/src/pages/logout.astro @@ -1,8 +1,14 @@ --- -import doLogout from "../lib/actions/do-logout.js"; +import doAuth from "../lib/actions/do-auth.js"; const { url: { searchParams }, redirect } = Astro; -const { isLoggedOut } = await doLogout(Astro); -if (isLoggedOut) return redirect(searchParams.get("return_to") || "/"); ---- \ No newline at end of file +const { isAuthed, getAuthUrl } = await doAuth(Astro); + +if (isAuthed) { + const { isLoggedOut } = await doAuth(Astro, { action: "logout" }); + if (isLoggedOut) return redirect(searchParams.get("return_to") || "/"); +} else { + return redirect(getAuthUrl({ path: "/logout" })); +} +---