From 95e5beb70e4242bc9593a3cdc2ff45bacd6460dc Mon Sep 17 00:00:00 2001 From: Sven Sauleau Date: Tue, 7 Mar 2023 15:45:52 +0000 Subject: [PATCH] refactor UI auth --- backend/src/activitypub/actors/index.ts | 3 ++ backend/src/utils/auth/isUserAdmin.ts | 30 --------------- frontend/src/entry.cloudflare-pages.tsx | 51 +++++++++++++++++++++++-- frontend/src/routes/layout.tsx | 6 +-- frontend/src/utils/adminLoader.ts | 12 ++---- frontend/src/utils/authLoader.ts | 13 +------ 6 files changed, 59 insertions(+), 56 deletions(-) delete mode 100644 backend/src/utils/auth/isUserAdmin.ts diff --git a/backend/src/activitypub/actors/index.ts b/backend/src/activitypub/actors/index.ts index 6a6b324..fa8dc69 100644 --- a/backend/src/activitypub/actors/index.ts +++ b/backend/src/activitypub/actors/index.ts @@ -8,6 +8,7 @@ import { Buffer } from 'buffer' const PERSON = 'Person' const isTesting = typeof jest !== 'undefined' export const emailSymbol = Symbol() +export const isAdminSymbol = Symbol() export function actorURL(domain: string, id: string): URL { return new URL(`/ap/users/${id}`, 'https://' + domain) @@ -23,6 +24,7 @@ export interface Actor extends APObject { alsoKnownAs?: string [emailSymbol]: string + [isAdminSymbol]: boolean } // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person @@ -298,6 +300,7 @@ export function personFromRow(row: any): Person { return { // Hidden values [emailSymbol]: row.email, + [isAdminSymbol]: row.is_admin === 1, ...properties, name, diff --git a/backend/src/utils/auth/isUserAdmin.ts b/backend/src/utils/auth/isUserAdmin.ts deleted file mode 100644 index 704959f..0000000 --- a/backend/src/utils/auth/isUserAdmin.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { emailSymbol } from 'wildebeest/backend/src/activitypub/actors' -import { Database } from 'wildebeest/backend/src/database' -import { getJwtEmail } from 'wildebeest/backend/src/utils/auth/getJwtEmail' -import { getAdmins } from './getAdmins' -import { isUserAuthenticated } from './isUserAuthenticated' - -export async function isUserAdmin( - request: Request, - jwt: string, - accessAuthDomain: string, - accessAud: string, - database: Database -): Promise { - let email: string - - try { - const authenticated = await isUserAuthenticated(request, jwt, accessAuthDomain, accessAud) - if (!authenticated) { - return false - } - - email = getJwtEmail(jwt) - } catch { - return false - } - - const admins = await getAdmins(database) - - return admins.some((admin) => admin[emailSymbol] === email) -} diff --git a/frontend/src/entry.cloudflare-pages.tsx b/frontend/src/entry.cloudflare-pages.tsx index 3dd51a4..fa113c9 100644 --- a/frontend/src/entry.cloudflare-pages.tsx +++ b/frontend/src/entry.cloudflare-pages.tsx @@ -4,13 +4,58 @@ * It's the entry point for cloudflare-pages when building for production. * * Learn more about the cloudflare integration here: - * - https://qwik.builder.io/qwikcity/adaptors/cloudflare-pages/ + * - https://qwik.builder.io/integrations/deployments/cloudflare-pages/#cloudflare-pages-entry-middleware * */ import { createQwikCity } from '@builder.io/qwik-city/middleware/cloudflare-pages' import qwikCityPlan from '@qwik-city-plan' import render from './entry.ssr' +import type { Env } from 'wildebeest/backend/src/types/env' +import type { ContextData } from 'wildebeest/backend/src/types/context' +import { parse } from 'cookie' +import * as access from 'wildebeest/backend/src/access' +import { getJwtEmail } from 'wildebeest/backend/src/utils/auth/getJwtEmail' +import * as errors from 'wildebeest/backend/src/errors' +import * as actors from 'wildebeest/backend/src/activitypub/actors' +import { getDatabase } from 'wildebeest/backend/src/database' +import type { Person } from 'wildebeest/backend/src/activitypub/actors' -const onRequest = createQwikCity({ render, qwikCityPlan }) +const qwikHandler = createQwikCity({ render, qwikCityPlan }) -export { onRequest } +type QwikContextData = { + connectedActor: Person | null, +} + +// eslint-disable-next-line +export const onRequest: PagesFunction = async (ctx) => { + const cookie = parse(ctx.request.headers.get('Cookie') || '') + const jwt = cookie['CF_Authorization'] + + const data: QwikContextData = { + connectedActor: null, + } + + if (jwt) { + const validate = access.generateValidator({ + jwt, + domain: ctx.env.ACCESS_AUTH_DOMAIN, + aud: ctx.env.ACCESS_AUD, + }) + await validate(ctx.request) + + let email = '' + try { + email = getJwtEmail(jwt ?? '') + } catch (e) { + return errors.notAuthorized((e as Error)?.message) + } + + const db = await getDatabase(ctx.env) + data.connectedActor = await actors.getPersonByEmail(db, email) + } + + // eslint-disable-next-line + ;(ctx.env as any).data = data + + return qwikHandler(ctx) +} diff --git a/frontend/src/routes/layout.tsx b/frontend/src/routes/layout.tsx index 81b8e56..44e2719 100644 --- a/frontend/src/routes/layout.tsx +++ b/frontend/src/routes/layout.tsx @@ -1,15 +1,13 @@ import { component$, Slot } from '@builder.io/qwik' import { loader$ } from '@builder.io/qwik-city' -import { isUserAuthenticated } from 'wildebeest/backend/src/utils/auth/isUserAuthenticated' type AuthLoaderData = { loginUrl: URL isAuthorized: boolean } -export const authLoader = loader$>(async ({ platform, request, cookie }) => { - const jwt = cookie.get('CF_Authorization')?.value ?? '' - const isAuthorized = await isUserAuthenticated(request, jwt, platform.ACCESS_AUTH_DOMAIN, platform.ACCESS_AUD) +export const authLoader = loader$>(async ({ platform, request }) => { + const isAuthorized = platform.data.connectedActor !== null // FIXME(sven): remove hardcoded value const UI_CLIENT_ID = '924801be-d211-495d-8cac-e73503413af8' const params = new URLSearchParams({ diff --git a/frontend/src/utils/adminLoader.ts b/frontend/src/utils/adminLoader.ts index 4ed3606..7915890 100644 --- a/frontend/src/utils/adminLoader.ts +++ b/frontend/src/utils/adminLoader.ts @@ -1,14 +1,10 @@ import { loader$ } from '@builder.io/qwik-city' -import { parse } from 'cookie' -import { getDatabase } from 'wildebeest/backend/src/database' -import { isUserAdmin } from 'wildebeest/backend/src/utils/auth/isUserAdmin' +import { isAdminSymbol } from 'wildebeest/backend/src/activitypub/actors' import { getErrorHtml } from './getErrorHtml/getErrorHtml' -export const adminLoader = loader$(async ({ request, platform, html }) => { - const database = await getDatabase(platform) - const cookie = parse(request.headers.get('Cookie') || '') - const jwtCookie = cookie.CF_Authorization ?? '' - const isAdmin = await isUserAdmin(request, jwtCookie, platform.ACCESS_AUTH_DOMAIN, platform.ACCESS_AUD, database) +export const adminLoader = loader$(async ({ platform, html }) => { + const isAuthorized = platform.data.connectedActor !== null + const isAdmin = isAuthorized && platform.data.connectedActor[isAdminSymbol] if (!isAdmin) { return html(401, getErrorHtml('You need to be an admin to view this page')) diff --git a/frontend/src/utils/authLoader.ts b/frontend/src/utils/authLoader.ts index 92d4374..605b528 100644 --- a/frontend/src/utils/authLoader.ts +++ b/frontend/src/utils/authLoader.ts @@ -1,17 +1,8 @@ import { loader$ } from '@builder.io/qwik-city' -import { parse } from 'cookie' -import { isUserAuthenticated } from 'wildebeest/backend/src/utils/auth/isUserAuthenticated' import { getErrorHtml } from './getErrorHtml/getErrorHtml' -export const authLoader = loader$(async ({ request, platform, html }) => { - const cookie = parse(request.headers.get('Cookie') || '') - const jwtCookie = cookie.CF_Authorization ?? '' - const isAuthenticated = await isUserAuthenticated( - request, - jwtCookie, - platform.ACCESS_AUTH_DOMAIN, - platform.ACCESS_AUD - ) +export const authLoader = loader$(async ({ platform, html }) => { + const isAuthenticated = platform.data.connectedActor !== null if (!isAuthenticated) { return html(401, getErrorHtml("You're not authorized to view this page"))