Simple password protection in SvelteKit with hooks not working

Hello,

I’m trying to password protect (just password, no username) a simple page using Cloudflare pages. This seems near impossible and I’m going insane.

My page is

  • A SvelteKit https://kit.svelte.dev app using typescript
  • Using the Cloudflare Adapter https://www.npmjs.com/package/@sveltejs/adapter-cloudflare

Authentication mechanism

  • Is based on this coudflare-pages-auth https://dev.to/charca/password-protection-for-cloudflare-pages-8ma article that adds a middleware https://github.com/Charca/cloudflare-pages-auth/blob/main/functions/_middleware.ts
  • But rewritten to use sveltekit hooks https://kit.svelte.dev/docs/hooks#server-hooks since the Cloudflare adapter doesnt seem to support middlewares and svelte running simultaneously https://github.com/sveltejs/kit/tree/master/packages/adapter-cloudflare#notes

Behavior
When running locally using npm run dev everything is working flawless. Pages are protected, entering the simple password sets the cookie and I’m allowed to enter the page.

When deployed as a Cloudflare page however

  1. I enter my password and gets redirected to /cfp_login

  2. Which then redirects me back to / if I use the correct password, or /?error=1 if I’m using the wrong password. This behavior is so far correct.

  3. The weird thing is

    • If I enter wrong password, I’m expected to render an error message due to the error query param, this is not happening and I’m stuck at the enter password dialog
    • If I enter the correct password, nothing is happening, I’m stuck at the enter password dialog anyway

I have tried the following

  • I know about Access https://www.cloudflare.com/products/zero-trust/access/ and I tried it but it does not suit my use case at all as they don´t support simple password protection. I just wan´t to send out an invitation with a password for all guests to the page (not any per-account password…instead one for all)

  • I have tried hardcoding the password in the code so I know it´s not an issue with getting env vars

  • I have tried running a local production build npm run build then npm run preview and it also worked

  • I have tried removing Secure and httpOnly attributes from the cookie

  • I have tried settging SameSite=Lax on the cookie

  • I have tried adding a _headers file with and disabling CORS using the following content

    /*
        Access-Control-Allow-Origin: *
    

Questions

  • Is there some weird super caching going on which makes the login page not re-rendering?
  • Can I get logs somehow?!
  • Why am I not allowed to use markdown links in posts?

Simply put…what am I missing!?

svelte.config.js

import adapter from '@sveltejs/adapter-cloudflare';
import preprocess from 'svelte-preprocess';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	// Consult https://github.com/sveltejs/svelte-preprocess
	// for more information about preprocessors
	preprocess: preprocess(),

	kit: {
		adapter: adapter({
			platform: 'node'
		}),

		// hydrate the <div id="svelte"> element in src/app.html
		target: '#svelte'
	}
};

export default config;

src/hooks.ts

import { CFP_ALLOWED_PATHS } from '$lib/login/constants';
import { getCookieKeyValue } from '$lib/login/utils';
import { getTemplate } from '$lib/login/template';

/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
    const password =
        event.platform?.env?.CFP_PASSWORD?.toLowerCase()
        ?? process?.env['CFP_PASSWORD']?.toLowerCase();
	const { pathname, searchParams } = new URL(event?.url);
	const { error } = Object.fromEntries(searchParams);
	const cookie = event.request.headers.get('cookie') || '';
	const cookieKeyValue = await getCookieKeyValue(password);

	if (cookie.includes(cookieKeyValue) || CFP_ALLOWED_PATHS.includes(pathname) || !password) {
		// Correct hash in cookie, allowed path, or no password set.
		// Continue to next middleware.
		return resolve(event);
	} else {
		// No cookie or incorrect hash in cookie. Redirect to login.
		return new Response(getTemplate({ withError: error === '1' }), {
			headers: {
				'content-type': 'text/html'
			}
		});
	}
}

src/routes/cfp_login/index.ts

import { CFP_COOKIE_MAX_AGE } from '$lib/login/constants';
import { sha256, getCookieKeyValue } from '$lib/login/utils';

/** @type {import('./$types').RequestHandler} */
export async function post({ request, platform }) {
    const cfp_password =
        platform?.env?.CFP_PASSWORD?.toLowerCase()
        ?? process?.env['CFP_PASSWORD']?.toLowerCase();
	const body = await request.formData();
	const { password } = Object.fromEntries(body);
	const hashedPassword = await sha256(password.toString());
	const hashedCfpPassword = await sha256(cfp_password);

  if (hashedPassword === hashedCfpPassword) {
		// Valid password. Redirect to home page and set cookie with auth hash.
		const cookieKeyValue = await getCookieKeyValue(cfp_password);

		return new Response('', {
			status: 302,
			headers: {
                'Set-Cookie': `${cookieKeyValue}; Max-Age=${CFP_COOKIE_MAX_AGE}; Path=/; HttpOnly; Secure`,
				'Cache-Control': 'no-cache',
				Location: '/'
			}
		});
	} else {
		// Invalid password. Redirect to login page with error.
		return new Response('', {
			status: 302,
			headers: {
				'Cache-Control': 'no-cache',
				Location: '/?error=1'
			}
		});
	}
}