ciscoheat / sveltekit-flash-message

Send temporary data after redirect, usually from endpoints. Works with both SSR and client.
https://www.npmjs.com/package/sveltekit-flash-message
MIT License
268 stars 6 forks source link

[SOLVED] Overwriting 'set-cookie' header via server hook prevents flash message #16

Closed ProphetLamb closed 1 year ago

ProphetLamb commented 1 year ago

TL;DR: Caution to users using the +hooks.server.ts Handle function!

Notice that response.headers.set('set-cookie', ... discards the flash message cookie. It is imperative that instead response.headers.append('set-cookie', ... must be used. Multiple set-cookie headers are supported.

I propose adding a documentation specifically warning adventurous users from overwriting the cookie.


I have had a bit of trouble getting flash messages to work. In the +layout.server.ts ServerLoad I print the cookies before the flash wrapper is executed:

import type { ServerLoad } from '@sveltejs/kit';

import { loadFlash } from 'sveltekit-flash-message/server';
import type { LayoutServerData } from './$types';

export const load: ServerLoad = (event) => {
    console.log('ServerLoad cookies', event.request.headers.get('cookie'));
    return loadFlash(async ({ locals }) => {
        return {
            user: locals.user,
            categories: locals.categories,
        } satisfies LayoutServerData;
    })(event);
};

In the +layout.svelte I render the flash

<script lang="ts">
import { showFlash } from '$lib/Toast/toast.client';

showFlash(page);
</script>

Show flash reactively prints client cookies

import type { Page } from '@sveltejs/kit';
import type { Readable } from 'svelte/motion';
import { getFlash } from 'sveltekit-flash-message/client';
import { toast } from './toast';

export function showFlash(page: Readable<Page<Record<string, string>, string | null>>) {
    page.subscribe((page) => {
        console.log('page: ', page.data.flash);
    });
    const flash = getFlash(page);
    flash.subscribe((msg) => {
        console.log('getFlash: ', msg);
        if (msg) {
            toast.show(msg);
        }
        flash.set(undefined);
    });
}

In my login form `+page.server.ts I show a toast upon failed login

import type { Actions } from './$types';
import { loginSchema, sanitizeLoginForm } from './schema';
import { ZodError } from 'zod';
import { createRedirectTo, ensureRootPath } from '$lib/url';
import type { ToastMessage } from '$lib/Toast/toast';
import { redirect, setFlash } from 'sveltekit-flash-message/server';

export const actions: Actions = {
    default: async (event) => {
        const { locals, request, url } = event;
        const formData = Object.fromEntries(await request.formData());
    // redacted for brevety. Below the login failed

    setFlash(
        {
            type: 'surface',
            message: ['Lost your password?', { name: 'Reset it here', url: `/auth/reset-password?redirectTo=${createRedirectTo(url)}` }],
        } satisfies ToastMessage,
        event
    );
    console.log('setFlash: ', event.cookies.get('flash'));
    return {
        data: sanitizeLoginForm(formData),
        errors: {
            nameOrEmail: ['Invalid username or password'],
            password: ['Invalid username or password'],
        },
    };

Interacting with the form should yield the following debug output:

server: setFlash:  {"type":"surface","message":["Lost your password?",{"name":"Reset it here","url":"/auth/reset-password?redirectTo=/"}]}
server: ServerLoad cookies: flash=%7B%22type%22:%22surface%22,%22message%22:%5B%22Lost%20your%20password?%22,%7B%22name%22:%22Reset%20it%20here%22,%22url%22:%22/auth/reset-password?redirectTo=/%22%7D%5D%7D
client: page: {"type":"surface","message":["Lost your password?",{"name":"Reset it here","url":"/auth/reset-password?redirectTo=/"}]}
client: getFlash: {"type":"surface","message":["Lost your password?",{"name":"Reset it here","url":"/auth/reset-password?redirectTo=/"}]}

...but the server sided cookie is lost during the redirect, so the store is never updated, and the toast does not arrive at the client side.

server: setFlash:  {"type":"surface","message":["Lost your password?",{"name":"Reset it here","url":"/auth/reset-password?redirectTo=/"}]}
server: ServerLoad cookies: null
client: page: undefined

I traced this issue back to my bogus authentication implementation hook:

import { getCategories } from '$lib/server/cat';
import type { Handle, RequestEvent } from '@sveltejs/kit';
import PocketBase from 'pocketbase';

type HandleEvent = RequestEvent<Partial<Record<string, string>>, string | null>;

export const handle: Handle = async ({ event, resolve }) => {
    await Promise.all([addPocketBase(event), addProduct(event)]);
    // handle event
    const response = await resolve(event);

    // set authentication cookie
    response.headers.set('set-cookie', event.locals.pb.authStore.exportToCookie({ secure: false }));

    return response;
};

Notice that response.headers.set('set-cookie', ... discards the flash message cookie. It is imperative that instead response.headers.append('set-cookie', ... must be used. Multiple set-cookie headers are supported.

I propose adding a documentation specifically warning adventurous users from overwriting the cookie.

ciscoheat commented 1 year ago

Thanks, will add that as a note in the documentation. Closing this when it's done.

ciscoheat commented 1 year ago

Added to documentation now with v2.1.0.