Gravatar of Jeroen Jeroen Pelgrims

Accessing the Firebase Auth user in SvelteKit server-side

Last updated on , originally posted on in software-development, svelte, firebase

Note: If you want to jump straight into the code you can check the github repo.

Why this post?§

Normally when using Firebase Auth you'll only access the user data Client-Side through the client sdk. (Which is a large part of Firebase's benefit as a BAAS so you don't need to build your own backend)

Sometimes however you need to access the user data server-side as well. In my case I wanted to encrypt some data of the user before storing it in a database for my project randompenpal.com.
This encryption can only happen server-side because otherwise I would need to expose the encryption key to the client and the encryption would be pretty pointless.

I had quite some issues figuring out how to access the user's data server side. The Firebase docs on this mention various related things and it's quite hard to figure out what I actually needed to do.
(In hindsight I probably needed this page)

In my search I stumbled on this blog post describing exactly what I wanted to do, but in Next.js instead.
I've applied the same solution described there, but in SvelteKit, with some tweaks because I wanted to use an auth provider instead of username/password authentication.

The idea is that the authentication still happens client-side, but that we'll send info about the user to the backend so we know which user is trying to do something.

Install dependencies§

npm i -s firebase firebase-admin

We'll need both the client and server side firebase libraries.
Client side to perform the authentication, and server side to access firebase services from our backend.

1. Configure client side firebase§

.env file§

Create a .env file with the following contents:

# .env
PUBLIC_FIREBASE_PROJECT_ID=
PUBLIC_FIREBASE_API_KEY=
PUBLIC_FIREBASE_AUTH_DOMAIN=
PUBLIC_FIREBASE_STORAGE_BUCKET=
PUBLIC_FIREBASE_MESSAGE_SENDER_ID=
PUBLIC_FIREBASE_APP_ID=

Set the values for these variables for your firebase project.

You can get these values for these variables from the firebase console.
Click the cog in the top left, project settings, create a new web app if none exists or copy the values of the variables on the bottom of the page.

Client side config code§

Now we'll use the environment variables from the previous step to configure our client side firebase app.
For more information about the use of public environment variables in SvelteKit check the SvelteKit docs.

// src/lib/firebase/client.ts
import { initializeApp, getApps } from "firebase/app";
import { getAuth } from "firebase/auth";
import {
  PUBLIC_FIREBASE_PROJECT_ID,
  PUBLIC_FIREBASE_API_KEY,
  PUBLIC_FIREBASE_AUTH_DOMAIN,
  PUBLIC_FIREBASE_STORAGE_BUCKET,
  PUBLIC_FIREBASE_MESSAGE_SENDER_ID,
  PUBLIC_FIREBASE_APP_ID,
} from "$env/static/public";

function makeApp() {
  const apps = getApps();
  if (apps.length > 0) {
    return apps[0]!;
  }

  return initializeApp({
    apiKey: PUBLIC_FIREBASE_API_KEY,
    authDomain: PUBLIC_FIREBASE_AUTH_DOMAIN,
    projectId: PUBLIC_FIREBASE_PROJECT_ID,
    storageBucket: PUBLIC_FIREBASE_STORAGE_BUCKET,
    messagingSenderId: PUBLIC_FIREBASE_MESSAGE_SENDER_ID,
    appId: PUBLIC_FIREBASE_APP_ID,
    databaseURL: `https://${PUBLIC_FIREBASE_PROJECT_ID}.firebaseio.com`,
  });
}

export const firebase = makeApp();
export const auth = getAuth(firebase);

If you're getting errors for the import of the env variables in VS Code:

  1. Run npm run build.
    This will generate new type files that will contain the env variables.
  2. Ctrl+click on the import path ($env/static/public) so .svelte-kit/ambient.d.ts is opened in VS Code.
    This will also cause VS Code to reload the contents of that file and recognize the env variables in the import line.

2. Configure server-side firebase§

To be able to access the firebase resources server-side we'll need to make a service account.
A service account is a type of user specifically for software (our website) to access Firebase resources.

Create a new service account§

  • Go to the firebase console
  • Open project settings (cog in top left)
  • Open the service accounts tab
  • Click on Create service account if the button shows.
  • Then click on Generate new private key
  • Generate the key and store the json file that will be generated.

Add env variables§

Add 2 new variables to the .env file.

# .env
FIREBASE_ADMIN_PRIVATE_KEY=
FIREBASE_ADMIN_CLIENT_EMAIL=

Note that these variables don't start with PUBLIC_. These variables will only be accessible to server side code (*.server.ts files).

Set the values to the corresponding values in the private key json file you just downloaded.
(FIREBASE_ADMIN_PRIVATE_KEY must be equal to the value of private_key and FIREBASE_ADMIN_CLIENT_EMAIL must be equal to the value of client_email)

The private key MUST be enclosed by double quotes or you will receive the following error: Failed to parse private key: Error: Invalid PEM formatted message.

Configure server side code§

Now we'll configure the firebase-admin app.

// src/lib/firebase/admin.ts
import { cert, getApps, initializeApp } from "firebase-admin/app";
import { getFirestore } from "firebase-admin/firestore";
import { getAuth } from "firebase-admin/auth";
import {
  FIREBASE_ADMIN_PRIVATE_KEY,
  FIREBASE_ADMIN_CLIENT_EMAIL,
} from "$env/static/private";
import { PUBLIC_FIREBASE_PROJECT_ID } from "$env/static/public";

function makeApp() {
  const apps = getApps();
  if (apps.length > 0) {
    return apps[0]!;
  }

  return initializeApp({
    credential: credential.cert({
      privateKey: FIREBASE_ADMIN_PRIVATE_KEY,
      clientEmail: FIREBASE_ADMIN_CLIENT_EMAIL,
      projectId: PUBLIC_FIREBASE_PROJECT_ID,
    }),
    databaseURL: `https://${PUBLIC_FIREBASE_PROJECT_ID}.firebaseio.com`,
  });
}
export const firebase = makeApp();
export const auth = getAuth(firebase);
export const firestore = getFirestore();

NOTE: This code used to use the following import:
import { apps, initializeApp, credential } from "firebase-admin";
But a few days after writing this post this interface seems to have changed.
The code above has been updated to use the new interface.

Now that we've configured both the firebase client and server we can configure how we let the server know which user is trying to do something.
We'll do this by:

  • Monitoring the firebase authentication state
  • If a user logs in, save it in a Svelte store and get their Firebase ID token
  • This token will be stored in a cookie (which will automatically be sent tot the server on each request)
  1. First install the library we'll use to serialize the cookies with:
    npm i -s cookie
  2. Then configure the store:
// src/lib/stores/auth.ts
import cookie from "cookie";
import { browser } from "$app/environment";
import {
  GoogleAuthProvider,
  type User,
  signInWithRedirect,
} from "firebase/auth";
import { writable } from "svelte/store";
import { auth } from "../firebase/client";

export const user = writable<User | null>(null);

export async function signOut() {
  return auth.signOut();
}

export async function signIn() {
  await signInWithRedirect(auth, new GoogleAuthProvider());
}

if (browser) {
  auth.onIdTokenChanged(async (newUser) => {
    const tokenCurrentlySet =
      cookie.parse(document.cookie)["token"] !== undefined;
    const token = newUser ? await newUser?.getIdToken() : undefined;
    document.cookie = cookie.serialize("token", token ?? "", {
      path: "/",
      maxAge: token ? undefined : 0,
    });
    user.set(newUser);

    if (!tokenCurrentlySet && token) {
      document.location.reload();
    }
  });

  // refresh the ID token every 10 minutes
  setInterval(async () => {
    if (auth.currentUser) {
      await auth.currentUser.getIdToken(true);
    }
  }, 10 * 60 * 1000);
}

Why document.location.reload()?§

This has to do with the fact that we're using login by redirection. On the initial render after getting redirected from Google's login page the token cookie is not set yet, so the server-side render of the page can't use the user data yet. (the uid in +page.svelte).
Therefore we perform a page reload to make sure the cookie gets sent to the server and we get a proper authenticated server-side render.

Alternatives to avoid this issue:

  • use signInWithPopup()
  • use signInWithEmailAndPassword()

These don't use redirection so you won't end up in the situation where your server doesn't get a token, but your client side is authenticated.

Why the setInterval()?§

The ID token only lasts an hour by default, so as long as the website is open we want to refresh the token so the backend won't get an expired token after a while.

4. Use the code in an application§

Layout file§

// src/routes/+layout.server.ts
import { auth } from "$lib/firebase/admin";
import { redirect } from "@sveltejs/kit";
import type { LayoutServerLoadEvent } from "./$types";

export async function load({ cookies }: LayoutServerLoadEvent) {
  try {
    const token = cookies.get("token");
    const user = token ? await auth.verifyIdToken(token) : null;
    return {
      uid: user?.uid,
    };
  } catch {
    // The token is set but invalid or expired
    cookies.set("token", "", { maxAge: -1 });
    throw redirect(307, "/");
  }
}

In the layout file we'll parse the cookie that's sent to us to get the token.
We'll then verify the token to check if it's valid.
There's multiple properties on this token like email, email_verified, phone_number or picture, but I only need the user's ID, which is stored in the uid property.

If the token is set but invalid or expired, we'll unset it and redirect to the root page.
An alternative could be to redirect to a login page with a parameter containing the current page we're on so the user can resume after logging in.

Because we're doing this in the layout file, this data and auth protection will be accessible to all the other pages that use that layout as well. In previous versions of SvelteKit you would use the session for this.

If you only want to protect a sub route of your site you could only check the token in that route's layout file instead of the root layout file.
For example: src/routes/secret/+layout.server.ts to protect the /secret route.

Loading server data for our page§

import type { PageLoadEvent } from "./$types";

export async function load({ parent }: PageLoadEvent) {
  const { uid } = await parent(); // comes from +layout.server.ts
  return { uid };
}

Creating a login/authenticated page§

Finally, what it all comes down to is a page where we can show our login/logout buttons and some data when the user is authenticated.

$user.displayName comes from the client side firebase app, and the $page.data.uid comes from the server.
We could also use this uid to do stuff server side like encrypting some data and writing the data + the UID to a Firestore collection.

<script>
	import { user, signOut, signIn } from '$lib/stores/auth';
	import { page } from '$app/stores';
</script>

{#if $user}
	{$user.displayName} ({$page.data.uid})
	<button on:click={() => signOut()}>Log out</button>
{:else}
	Not logged in.
	<button on:click={() => signIn()}>Log in</button>
{/if}

Update 2023-05-08:§

There's an alternative solution that might be cleaner.
Check out this comment on Reddit.

The following sites link back to this post

This post is written by Jeroen Pelgrims, an independent software developer who runs Digicraft.eu.

Hire Jeroen