firebase-tools icon indicating copy to clipboard operation
firebase-tools copied to clipboard

Automatically deployed cloud function strips headers breaking any attempt at authentication

Open mafifi opened this issue 5 months ago • 1 comments

[REQUIRED] Environment info

firebase-tools: firebase --version 13.16.0

Platform: uname -a Darwin MYHOSTNAME.localdomain 23.6.0 Darwin Kernel Version 23.6.0: Mon Jul 29 21:14:30 PDT 2024; root:xnu-10063.141.2~1/RELEASE_ARM64_T6000 arm64

[REQUIRED] Test case

This is difficult to minimise.

  1. Use sveltekit
  2. Add a login page that's sets the session cookie server side
  3. Ensure users on the site are verified and redirect them to the login page if they're not
export const actions: Actions = {
	login: async ({ request, cookies, locals }) => {
		const formData = await request.formData();
		const idToken = formData.get('idToken');

		if (!idToken) {
			return {
				status: 401,
				body: { error: 'Unauthorized' }
			};
		}

		try {
			// Verify the ID token
			const decodedIdToken = await adminAuth.verifyIdToken(idToken.toString());

			// Create a session cookie
			const sessionCookie = await adminAuth.createSessionCookie(idToken.toString(), {
				expiresIn: 60 * 60 * 24 * 5 * 1000
			}); // 5 days

			// Set the session cookie in the response
			cookies.set('session', sessionCookie, {
				path: '/',
				maxAge: 60 * 60 * 24 * 5 * 1000, // 5 days
				httpOnly: true,
				secure: true,
				sameSite: 'none',
				domain: 'MYDOMAIN.com'
			});
			locals.session = sessionCookie;
			return {
				status: 200,
				headers: {
					session: sessionCookie
				},
				body: { user: decodedIdToken }
			};
		} catch (error) {
			console.error('Failed to create session cookie:', error);
			return {
				status: 401,
				body: { error: 'Unauthorized' }
			};
		}
	}
}

On the hooks.server.ts side, ensure there is a session that's verified, otherwise redirect.

// A lot of the console.errors is to allow me to trace the behaviour in the function logs.
// The multiple attempts to retrieve a session is a shotgun approach to make this work on firebase without luck
export async function handle({ event, resolve }) {
	let session =
		event.locals.session || event.cookies.get('session')
	console.error('session:', session);

	if (!session) {
		const cookieHeader = event.request.headers.get('cookie');
		console.error('cookieHeader:', cookieHeader);

		if (cookieHeader) {
			// Parse the Cookie header manually to extract the session cookie
			const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
				const [key, value] = cookie.trim().split('=');
				acc[key] = value;
				return acc;
			}, {});
			
			console.error('cookies:', cookies);
			session = cookies['session'];
		}
	}

	if (!session && event.url.pathname !== '/login') {
		console.error('No session cookie found');
		return new Response(null, { status: 302, headers: { location: '/login' } });
	}

	if (session && event.url.pathname !== '/login') {
		try {
			const decodedClaims = await adminAuth.verifySessionCookie(session);
			event.locals.user = decodedClaims;
		} catch (error) {
			Sentry.captureException(error);
			console.error('Error verifying session cookie:', error);
			// disable right now otherwise with the current issues this will loop indefinitley.
			// return new Response(null, { status: 302, headers: { location: '/login' } });
		}
	}

	const response = await resolve(event);
	return response;
}

[REQUIRED] Steps to reproduce

  1. In addition to setting up sveltekit and the above authentication code,
  2. Ensure that firebase auth is set-up correctly
  3. Ensure that the /login/+page.svelte page calls auth.signIn()
  4. Use frameworksBackend, see full firebase.json file below
  5. Browse to any SSR url
  6. Verify the session gets sent in the dev tools in your browser
  7. Verify that the headers never get passed onto your server-side code through the logs.

Note the rewrite is needed to ensure the client sends the session cookie, without it uses a cross-domain URL and the session never gets sent across.

{
	"hosting": {
		"source": ".",
		"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
		"frameworksBackend": {
			"region": "europe-west1"
		},
		"rewrites": [
			{
				"source": "**",
				"function": "MYFUNCTIONAME"
			}
		]
	}
}

[REQUIRED] Expected behavior

hooks.server.ts should not redirect me The session should be passed onto my server side code. At a minimum, the headers should not be cleared by firebase.

[REQUIRED] Actual behavior

DEFAULT 2024-09-11T08:07:47.109493Z session: undefined DEFAULT 2024-09-11T08:07:47.109652Z cookieHeader: null DEFAULT 2024-09-11T08:07:47.109685Z No session cookie found

mafifi avatar Sep 11 '24 08:09 mafifi