diff --git a/design-decisions/example.pentive.secrets.sh b/design-decisions/example.pentive.secrets.sh index 52965b75..62d94103 100644 --- a/design-decisions/example.pentive.secrets.sh +++ b/design-decisions/example.pentive.secrets.sh @@ -3,34 +3,42 @@ export productionAlphaKey= export cloudflareAccountId= export developmentPlanetscaleDbUrl= export productionPlanetscaleDbUrl= -# generate 32 bits of random data in base64, cut off new line character, add it to the clipboard -# openssl rand -base64 32 | head -c -1 | pbcopy -export developmentMediaTokenSecret=you+should+replace+this+with+a+real+base64+= -export productionMediaTokenSecret=you+should+replace+this+with+a+real+base64+= -export developmentHubSessionSecret=secret/you+can+run+the+command+which+kinda+= -export productionHubSessionSecret=secret/you+can+run+the+command+which+kinda+= -export developmentJwsSecret=looks+like+openssl+rand+base64+32+comma+but= -export productionJwsSecret=looks+like+openssl+rand+base64+32+comma+but= -export developmentCsrfSecret=with+spaces+and+hyphens+and+stuff+ZZZZZZZZZ= -export productionCsrfSecret=with+spaces+and+hyphens+and+stuff+ZZZZZZZZZ= -export developmentOauthStateSecret=also+this+is+the+same+32+bits+of+base64+joy= -export productionOauthStateSecret=also+this+is+the+same+32+bits+of+base64+joy= -export developmentOauthCodeVerifierSecret=also+this+is+the+same+32+bits+of+base64+joy= -export productionOauthCodeVerifierSecret=also+this+is+the+same+32+bits+of+base64+joy= -export developmentHubInfoSecret=also+this+is+the+same+32+bits+of+base64+joy= -export productionHubInfoSecret=also+this+is+the+same+32+bits+of+base64+joy= -export developmentDiscordId=create at https://discord.com/developers/applications -export productionDiscordId=Redirects (callbacks) are https://pentive.localhost:3014/api/auth/callback/discord or https://pentive.com/api/auth/callback/discord + +# the following secrets may be generated as follows: +# openssl rand -base64 32 | head -c -1 | pbcopy +# this generates 32 bits of random data in base64, cuts off the new line character, and adds it to the clipboard +export developmentMediaTokenSecret= +export productionMediaTokenSecret= +export developmentJwsSecret= +export productionJwsSecret= +export developmentCsrfSecret= +export productionCsrfSecret= +export developmentOauthStateSecret= +export productionOauthStateSecret= +export developmentOauthCodeVerifierSecret= +export productionOauthCodeVerifierSecret= +export developmentHubInfoSecret= +export productionHubInfoSecret= + +# create at https://discord.com/developers/applications +# Redirects (callbacks) are https://pentive.localhost:3014/api/auth/callback/discord or https://pentive.com/api/auth/callback/discord +export developmentDiscordId= +export productionDiscordId= export developmentDiscordSecret= export productionDiscordSecret= -export developmentGithubId=create at https://github.com/settings/developers -export productionGithubId=Authorization callback URL is https://pentive.localhost:3014/api/auth/callback/github or https://pentive.com/api/auth/callback/github + +# create at https://github.com/settings/developers +# Authorization callback URL is https://pentive.localhost:3014/api/auth/callback/github or https://pentive.com/api/auth/callback/github +export developmentGithubId= +export productionGithubId= export developmentGithubSecret= export productionGithubSecret= + export developmentAppOrigin=https://app.pentive.localhost:3013 export productionAppOrigin=https://app.pentive.com export developmentHubOrigin=https://pentive.localhost:3014 export productionHubOrigin=https://pentive.com + # If you add any `VITE_PRODUCTION_*`, also update `cicd.yml` export VITE_DEVELOPMENT_AG_GRID_LICENSE= export VITE_PRODUCTION_AG_GRID_LICENSE= diff --git a/hub/src/components/nav.tsx b/hub/src/components/nav.tsx index bd8d4bdf..500010ee 100644 --- a/hub/src/components/nav.tsx +++ b/hub/src/components/nav.tsx @@ -9,7 +9,7 @@ function Nav(): JSX.Element { async (_, { request }) => await getUserId(request) ) const [, { Form }] = createServerAction$( - async (f: FormData, { request }) => await logout(request) + async (_: FormData) => await logout() ) return (
diff --git a/hub/src/entry-server.tsx b/hub/src/entry-server.tsx index cba17c69..69803d7b 100644 --- a/hub/src/entry-server.tsx +++ b/hub/src/entry-server.tsx @@ -12,7 +12,6 @@ export default createHandler( /* eslint-disable solid/reactivity -- event.env and event.responseHeaders should never change at runtime */ setKysely(event.env.planetscaleDbUrl) setSessionStorage({ - sessionSecret: event.env.hubSessionSecret, jwsSecret: event.env.jwsSecret, csrfSecret: event.env.csrfSecret, hubInfoSecret: event.env.hubInfoSecret, diff --git a/hub/src/env.d.ts b/hub/src/env.d.ts index f8470137..3070bcc4 100644 --- a/hub/src/env.d.ts +++ b/hub/src/env.d.ts @@ -5,7 +5,6 @@ import { type Base64 } from "shared" export interface EnvVars { planetscaleDbUrl: string - hubSessionSecret: Base64 jwsSecret: Base64 csrfSecret: Base64 alphaKey: string diff --git a/hub/src/routes/n/[nook]/submit.tsx b/hub/src/routes/n/[nook]/submit.tsx index 7ec85792..7528b718 100644 --- a/hub/src/routes/n/[nook]/submit.tsx +++ b/hub/src/routes/n/[nook]/submit.tsx @@ -6,12 +6,7 @@ import { createServerData$, redirect, } from "solid-start/server" -import { - requireCsrfSignature, - requireSession, - requireJwt, - isInvalidCsrf, -} from "~/session" +import { requireCsrfSignature, requireJwt, isInvalidCsrf } from "~/session" export function routeData({ params }: RouteDataArgs) { const nook = (): string => params.nook @@ -70,7 +65,6 @@ export default function Submit(): JSX.Element { if (Object.values(fieldErrors).some(Boolean)) { throw new FormError("Some fields are invalid", { fieldErrors, fields }) } - const session = await requireSession(request) const jwt = await requireJwt(request) if (await isInvalidCsrf(csrfSignature, jwt.jti)) { const searchParams = new URLSearchParams([ @@ -81,7 +75,7 @@ export default function Submit(): JSX.Element { await insertPost({ id: ulidAsHex(), - authorId: session.userId, + authorId: jwt.sub, title, text, nook, diff --git a/hub/src/session.ts b/hub/src/session.ts index 327e0d7c..e12302d8 100644 --- a/hub/src/session.ts +++ b/hub/src/session.ts @@ -9,38 +9,16 @@ import { } from "shared" import { base64ToArray } from "shared-edge" import { redirect } from "solid-start/server" -import { createCookieSessionStorage } from "solid-start/session" import { type Cookie, type CookieOptions } from "solid-start/session/cookies" -import { type Session, type SessionStorage } from "solid-start/session/sessions" import { createPlainCookie } from "~/createPlainCookie" -const sessionUserId = "userId" -const sessionNames = [sessionUserId] as const -type SessionName = (typeof sessionNames)[number] -export type UserSession = { [K in SessionName]: string } - export function setSessionStorage(x: { - sessionSecret: Base64 jwsSecret: Base64 csrfSecret: Base64 hubInfoSecret: Base64 oauthStateSecret: Base64 oauthCodeVerifierSecret: Base64 }): void { - // highTODO consider removing this when adding Auth.js. We need cross-domain auth, and I'm not sure why this exists if we're using a JWT - storage = createCookieSessionStorage({ - cookie: { - name: "__Host-session", - secure: true, - secrets: [x.sessionSecret], - sameSite: "lax", - path: "/", - maxAge: 60 * 60 * 24 * 30, // 30 days - httpOnly: true, - // domain: "", // intentionally missing https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#cookie-with-__host-prefix - // expires: "", // intentionally missing because docs say it's calculated off `maxAge` when missing https://github.com/solidjs/solid-start/blob/1b22cad87dd7bd74f73d807e1d60b886e753a6ee/packages/start/session/cookies.ts#L56-L57 - }, - }) jwsSecret = base64ToArray(x.jwsSecret) csrfSecret = x.csrfSecret hubInfoSecret = base64ToArray(x.hubInfoSecret) @@ -140,8 +118,6 @@ export function setSessionStorage(x: { hubInfoCookie = createPlainCookie(hubInfoCookieName, hubInfoCookieOpts) } -// @ts-expect-error session calls should throw null error if not setup -let storage = null as SessionStorage // @ts-expect-error calls should throw null error if not setup let jwtCookie = null as Cookie // @ts-expect-error calls should throw null error if not setup @@ -167,10 +143,6 @@ let csrfSecret = null as string // @ts-expect-error calls should throw null error if not setup let hubInfoSecret = null as Uint8Array -export async function getUserSession(request: Request): Promise { - return await storage.getSession(request.headers.get("Cookie")) -} - export async function getCsrfSignature( request: Request ): Promise { @@ -204,7 +176,7 @@ export async function getOauthCodeVerifier(request: Request) { } export interface Jwt { - sub: string + sub: UserId jti: string } @@ -219,16 +191,14 @@ export async function getJwt(request: Request): Promise { return jwt == null ? null : { - sub: jwt.payload.sub ?? throwExp("`sub` is empty"), + sub: (jwt.payload.sub as UserId) ?? throwExp("`sub` is empty"), jti: jwt.payload.jti ?? throwExp("`jti` is empty"), } } export async function getUserId(request: Request) { - const session = await getUserSession(request) - const userId = session.get(sessionUserId) as unknown - if (typeof userId !== "string" || userId.length === 0) return null - return userId as UserId + const jwt = await getJwt(request) + return jwt?.sub ?? null } export async function requireUserId( @@ -243,24 +213,6 @@ export async function requireUserId( return r } -export async function requireSession( - request: Request, - redirectTo: string = new URL(request.url).pathname -): Promise { - const session = await getUserSession(request) - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const r = {} as UserSession - for (const sessionName of sessionNames) { - const sessionValue = session.get(sessionName) as unknown - if (typeof sessionValue !== "string" || sessionValue.length === 0) { - const searchParams = new URLSearchParams([["redirectTo", redirectTo]]) - throw redirect(`/login?${searchParams.toString()}`) as unknown - } - r[sessionName] = sessionValue - } - return r -} - export async function requireCsrfSignature( request: Request, redirectTo: string = new URL(request.url).pathname @@ -285,10 +237,8 @@ export async function requireJwt( return jwt } -export async function logout(request: Request): Promise { - const session = await storage.getSession(request.headers.get("Cookie")) +export async function logout() { const headers = new Headers() - headers.append("Set-Cookie", await storage.destroySession(session)) // lowTODO parallelize headers.append("Set-Cookie", await destroyJwtCookie.serialize("")) // lowTODO parallelize headers.append("Set-Cookie", await destroyCsrfSignatureCookie.serialize("")) // lowTODO parallelize return redirect("/login", { @@ -300,11 +250,8 @@ export async function createUserSession( userId: string, redirectTo: string ): Promise { - const session = await storage.getSession() - session.set(sessionUserId, userId) const [csrf, csrfSignature] = await generateCsrf() const headers = new Headers() - headers.append("Set-Cookie", await storage.commitSession(session)) // lowTODO parallelize const jwt = await generateJwt(userId, csrf) headers.append("Set-Cookie", await jwtCookie.serialize(jwt)) // lowTODO parallelize // https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie diff --git a/mkenv.sh b/mkenv.sh index b587df74..1c7c7c05 100755 --- a/mkenv.sh +++ b/mkenv.sh @@ -26,7 +26,6 @@ source ../PentiveSecrets/secrets.sh # echo $productionGithubId | npx wrangler secret put githubId --name hub # echo $productionGithubSecret | npx wrangler secret put githubSecret --name hub # echo $productionPlanetscaleDbUrl | npx wrangler secret put planetscaleDbUrl --name hub -# echo $productionHubSessionSecret | npx wrangler secret put hubSessionSecret --name hub # echo $productionCsrfSecret | npx wrangler secret put csrfSecret --name hub # echo $productionOauthStateSecret | npx wrangler secret put oauthStateSecret --name hub # echo $productionOauthCodeVerifierSecret | npx wrangler secret put oauthCodeVerifierSecret --name hub