This post series is indexed at delivered some bad news - the Firestore auth object used client-side to supply information about a logged-in user isn't available server-side. This has the following consequences:
Server-side database code must use the Firestore Admin API. This is because Firestore Client API code that makes calls subject to database "rules" that reference
authfails whenauthisn't available. By contrast, Admin API calls don't care about database rules. If you dropped the rules, client API calls would work server-side, but this would leave your database open to cyber attack (you've been working into your live Firestore database ever since you started using your local VSCode terminal - think about it).Server-side code that uses data items such as
userNameanduserEmailderived fromauthmust find another way of getting this information.
This post describes how you overcome these problems to produce a high-performance webapp that runs securely and efficiently server-side.
2. Authenticated Svelte server-side code in practice
If you've already got used to the Client call signatures, the requirement to switch to the Firestore Admin API is a nuisance. But you'll soon get used to this so it shouldn't hold you up significantly.
Getting user data, however, is a different matter. For many applications, access to user properties such as uId is critical to their design. For example, a webapp may need to ensure that users can only see their own data. Unfortunately, arranging this is quite a struggle. Here we go:
- First, on the client, you need to find a way to create an "idToken" package containing everything your server-side code might need to know about a user. Google provides a
getIdToken()mechanism to construct this from the user'sauthsession data. - Then you need to find a way of passing this package to the server. The mechanism used here registers this in a "header" that gets added to client calls to the server.
- Then you need to obtain a Google "Service Account" that enables you to authenticate your use of the Firestore Admin API on a Google server. The keys that define this need to be embedded securely in your project files (recall the
firebaseConfig.envdiscussion in you should see theidTokendisplayed in analertmessage.
The Firebase ID token is a JSON Web Token (JWT). The JSON bit means that it is an object encoded as a character string using "Javascript Object Notation" (if this is your first sight of a "JSON" you might find it useful to ask chatGPT for background). JSONs are widely used where you need to pass Javascript objects around as character strings. The JWT JSON includes everything you might need to know about a user. I'll show you how you extract this information later in this post - it's not complicated.
2.2 Passing theidTokento the server
The mechanism described in this post sends the "IdToken" as a "cookie" in the "request header" that accompanies server requests. An "http header" is a packet of information that passes through the web when a client-based
+page.sveltefile sends a request to a server-based+page.server.jsfile. Such a request will be sent every time you read or write a Firestore document. A "cookie" is a string that gets added to every request header.
This arrangement is complicated but is regarded as secure. From your point of view, as an IT student, it's also interesting and educational because it gives insight into web design internals.
A client-side Javascript program could easily set a "regular" cookie containing the JWT but, for security reasons, you very much do not want to do this. If you can do this then anybody can. A server-side
+page.server.jsfile, on the other hand, can set an "http-only" cookie in the client browser using aset-cookiecall. Here's an example:
CODE// Set a secure, HTTP-only cookie with the `idToken` token
const headers = {
'Set-Cookie': cookie.serialize('idToken', idToken, {
httpOnly: true
})
};
let response = new Response('Set cookie from server', {
status: 200,
headers,
body: { message: 'Cookie set successfully' } // Optional message
});
return response;
The
httpOnly: truesetting above means that, although the cookie is held client-side, it cannot be accessed from Javascript. In this way you can ensure that the value you set here is secure from tampering.
The question you should be asking now is "How can a server-side
+page.server.jsfile launch aSet-Cookiecommand to set anidTokenwhen it doesn't know theidToken?".
Welcome to the Svelte
+server.jsfile. This is server-side code that can be called from client-side code with a Javascriptfetchcommand. Such server-side code is called an "end-point". Afetchcommand is Javascript's native method for submitting arequestto a web-based "end-point". The command enables you to include data in the request and so this is how you get anidTokenvalue onto the server. Here's an example:
CODE// client-side +page.svelte code
const idToken = await user.getIdToken();
// Send token to the server to set the cookie
fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ idToken }),
});
and here's how the recipient
+server.jsfile would retrieve this and extract itsidToken.
CODE// server-side +server.js code
export async function POST({ request }) {
const { idToken } = await request.json();
}
You are probably thinking "Why does this code use a "fetch" command to "send" something?" but there you are. "Fetch" was designed as a versatile API for making many different types of HTTP request. Ask chatGPT for a tutorial if you'd like to get some background. See .
- Navigate to IAM & Admin > Service Accounts and check that this points to your
svelte-devproject (using the pulldown menu at the top left). The IAM (Identity and Access Management) screen lists all the Cloud permissions that control who can do what with the Google Cloud resources for your project. This deserves a 'post' in its own right, but this isn't the time - Switch out of the IAM page and into the Service Accounts page by mousing over the toolbar at the left of the screen and clicking the one labelled "Service Accounts". You should see that a default account has already been created.
- Click the "+ Create Service Account" button at the top of the page and Create a new Service Account with a unique "Service account name such as "svelte-dev" (or whatever takes your fancy - it must be between 6 and 30 characters long and can only see lower-case alphanumerics and dashes). A version of this with a suffix guaranteed to be unique Cloud-wide is propagated into the "Service account ID" field. I suggest you accept whatever it offers.
Now click the "Create And Continue" button and proceed to the "Grant this service account access to the project" section. Start by opening the pull-down menu on the field. This is a little complicated because it has two panels. The left-hand panel (which has a slider bar), allows you to select a product or service. The right-hand one lists the roles that are available for that service. Use the left-hand panel to select the "Firebase" Service and then select the "Admin SDK Administrator Service Agent" role from the right-hand panel. Click "Continue", then "Done" to return to the Service Accounts screen
Finally, click the "three-dot" menu at the RHS of the entry for the "Firebase Admin SDK Service Agent" key that you've just created and select "manage keys". Click "Add Key" > Create new key > JSON > Create and note that a new file has appeared in your "downloads" folder. This is your "Service Account Key". All you have to do now is embed this in your project.
2.3.2 Embedding the downloaded Service Account in your project
- Create a
/secretsfolder in the root of your project to provide a secure location for the Service Account Key. Move the download Service Account file into a/secrets/serviceAccount.jsonfile and add the "/secrets" folder and any editing history for it to your ".gitignore" file:
// .gitignore - fragment
# Secrets
/secrets/
/.history/secrets/
This is another instance of the safeguarding mechanism described previously in . It was used there to show how server-side code attempting to use the Firestore Client API would fail where the collections they were addressing were subject to Firestore database rules. This new version benefits from:
- Service account keys that enable it to use Firestore Admin API commands and thus ignore the database rules
- An
idTokencookie that enables it to obtain details of an authenticated user
// src/routes/products-maintenance-sv/+page.server.js
import admin from 'firebase-admin';
import serviceAccount from '/secrets/service-account-file.json';
import cookie from 'cookie'; // install with "npm install cookie"
import { productNumberIsNumeric } from "$lib/utilities/productNumberIsNumeric";
// Give initialiseApp your project's Service Account Credentials. But make sure you only do this once.
// Things can get messy if you don't do this when your application is deployed in an environment where
// multiple instances of the server or multiple processes are running,
try {
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
}
} catch (error) {
console.error("Failed to initialize Firebase Admin SDK:", error);
}
const adminDb = admin.firestore(); // Create an Admin SDK Firestore instance
export const actions = {
default: async ({ request }) => {
// Get the idToken from the request header
const cookies = cookie.parse(request.headers.get('cookie'));
const idToken = cookies.idToken;
// example of use of idToken to get the userEmail
const decodedToken = await admin.auth().verifyIdToken(idToken);
const userEmail = decodedToken.email;
console.log("userEmail from cookie : " + userEmail);
// capture of form data
const input = await request.formData();
const productNumber = input.get("productNumber");
const productDetails = input.get("productDetails");
// server-side repeat of client-side validation to catch hackers
const validationResult = productNumberIsNumeric(productNumber);
// Add the new record to the database
if (validationResult) {
try {
const productsDocData = { productNumber: parseInt(productNumber, 10), productDetails: productDetails };
const productsCollRef = adminDb.collection("products");
const productsDocRef = productsCollRef.doc(); // Creates a new doc with an auto-generated ID
await productsDocRef.set(productsDocData);
return { validationSuccess: true, databaseUpdateSuccess: true };
} catch (error) {
return { validationSuccess: true, databaseUpdateSuccess: false, databaseError: error.message };
}
} else {
return { validationSuccess: false, databaseUpdateSuccess: null };
}
}
};
Notice the curious way that the code builds the userEmail field
// example of use of idToken to get the userEmail
const decodedToken = await admin.auth().verifyIdToken(idToken);
const userEmail = decodedToken.email;
The verifyIdToken method name might make you wonder whether this is trying to authenticate your user again. Don't worry - it's not. It's just doing a security check on the token's embedded "signatures" to assure itself that it hasn't been tampered with and hasn't expired.
The decodedToken created by verifyIdToken is a simple object containing email and userName properties etc for your authenticated user. The subsequent Firestore code doesn't use any of these, but I'm sure you can easily imagine how it might do so.
I suggest you use the "boiler-plate" approach again when coding Admin API calls - use chatGPT to convert the client code documented in
SOCIAL SHARE CARD GENERATOR