Recently, I have been designing an e-commerce web application using Google Firebase, React, and NodeJS. My thoughts about how to design a secure shopping experience came through much research. I had a Google Cloud Firebase app and I wanted the user to add and remove items to the shopping cart at the same time making secure fetch calls to save the shopping cart in a database. One way to ensure a secure shopping experience is to use an encrypted JSON web token (JWT) when performing each call. This is specifically what I did with my shopping cart.
So in order to store the shopping cart in my database, I used Google Cloud Functions written in NodeJS to perform these actions. Doing any JWT encryption usually takes a backend like NodeJS, but specifically when using a Google Firebase web application and Google Firebase's Realtime database, a cloud function will be necessary to encrypt, decrypt and handle database actions when all done together. In this post, I will cover how to create a JWT and use the Google Cloud Functions to store data in the database.
First, you will need to enable Google Cloud Functions in your Firebase project. They have some very detailed tutorials you can find here:
Firebase Cloud Functions tutorials
In the tutorials, they suggest starting with Typescript, which is what I did. And just a note if you use Typescript, you might need to spend some time understanding how TSlint works because the Typescript lint will throw an error if you don't have everything written within the proper rules. You may have to adjust according to your needs.
When first enabling cloud functions, your index.ts file contains the following code:
import * as functions from 'firebase-functions';
export const helloWorld = functions.https.onRequest((request, response) => {
response.send("hello world!")
});
Breaking this code down, this is how a basic cloud function works. Instead of having your NodeJS app listen to a certain port, Firebase creates a cloud function to respond to your requests. In order to access the function, you need to make a request to a site formatted like this:
https://us-central1-yourprojectname.cloudfunctions.net/helloWorld
So this is a simple GET request, but what if I want a post, get, and a patch request like I do with my shopping cart? Google cloud functions allow you to export an expressJS app to that cloud function.
Here is an example from Google Firebase's documentation:
const express = require('express');
const cors = require('cors');
const app = express();
// Automatically allow cross-origin requests
app.use(cors({ origin: true }));
// Add middleware to authenticate requests
app.use(myMiddleware);
// build multiple CRUD interfaces:
app.get('/:id', (req, res) => res.send(Widgets.getById(req.params.id)));
app.post('/', (req, res) => res.send(Widgets.create()));
app.put('/:id', (req, res) => res.send(Widgets.update(req.params.id, req.body)));
app.delete('/:id', (req, res) => res.send(Widgets.delete(req.params.id)));
app.get('/', (req, res) => res.send(Widgets.list()));
// Expose Express API as a single Cloud Function:
exports.widgets = functions.https.onRequest(app);
So Google Cloud functions actually allow you to create an express application all with one cloud function. All the rest of the NodeJS code should be familiar for those who have previously used it.
The only part that is unique to Google Cloud functions is the export. Unfortunately, I wasn't able to start a node server while exporting the Google Cloud functions. For my case, in order to inspect and debug the interface, I had to use the Firebase emulator. Here is a tutorial for this in this link below.
I had to create a key with application credentials in order to start debugging. Here is a great resource about how to setup a debugger:
Debugging Firebase Cloud Functions
So for my next topic, I will cover how to get the Firebase database setup in order to start adding items to cart. You will first need to initialize the firebase admin SDK as shown below.
import * as admin from 'firebase-admin'
admin.initializeApp()
After the application is initialized, if order to make a database call, just create a reference just like you would on the client-side Firebase application. This is what I how I created the reference:
const cartsRef = admin.database().ref('carts/' + requestParams.uid);
After you create the ref, you can update, set, or remove child just like you would with the client-side Firebase application. For my case, I wanted to first post an item to cart.
On my front end, it was a simple fetch call. Here is what it looked like.:
export function postCart(userId, lineItem) {
return (dispatch) => {
return fetch(`https://myfirebaseapp.cloudfunctions.net/carts`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
uid: userId,
lineItem: lineItem
})
})
.then(resp => resp.json())
.then(data => {
localStorage.setItem('JWT', data.jwtToken)
})
}
}
On a side note, I used Redux thunk in order to complete my post fetch request. You can find more about Redux thunk here
What I intend to do here is to pass my user id and their lineitem into the body of my fetch. My cloud function will receive that as part of my request params. I created a cloud function called 'carts' below.
const jwt = require('jsonwebtoken');
const cart = express();
cart.post('/', (req: any, res: any) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3002");
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
const requestParams = req.body;
const cartsRef = admin.database().ref('carts/' + requestParams.uid);
}
cartsRef.child(requestParams.lineItem.title).set({
item: requestParams.lineItem,
}).then(resp => {
const jwtToken = jwt.sign({ uid: requestParams.uid }, 'supersecretJWT');
res.status(200).send({jwtToken})
}).catch(err => {
res.json({ error: err });
res.status(500).send();
});
exports.carts = functions.https.onRequest(cart)
This is what I have before I start operating on my request. I make sure to set my response headers, and now I have a reference to the user ID that came from my fetch request. I also set the line item in my cart as a child which contains the item name and quantity. From that, I need to create my JSON web token which I stored in jwtToken which encrypts the user ID and sends it back to the user, which in turn will store the encrypted user ID as a JWT in local storage. I will later use that encrypted user ID when I want to get the shopping cart information. Make sure that your JWT secret is indeed kept secret because that is the key to keep it encrypted.
So after the line item in my cart is posted, I want to get an item from the cart, so what I did was send the encrypted JWT user ID back as an authorization header, decode in expressJS, then send back the cart information to the user. This is how my fetch request looked like from React/Redux:
export function fetchCart(userId) {
return (dispatch) => {
const token = localStorage.getItem('JWT')
return fetch(`https://yourproject.cloudfunctions.net/carts`, {
credentials: "include",
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
}
})
// fetch(`http://localhost:3002/arts.json`)
.then(resp => resp.json())
.then(data => {
dispatch({type: 'GET_JWT_CART', payload: data.lineItems})
})
}
}
When I make the fetch GET request, I want to setup my express app to get the token, decrypt it, then send back the items in the cart.
cart.get('/', (req: any, res: any) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3002");
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
const authHeader = req.headers.authorization;
const token = authHeader.split(' ')[1]
jwt.verify(token, 'supersecretJWT', (err :any , data :any) => {
if(err){
res.sendStatus(403)
}
else if(data.uid){
req.uid = data.uid
const cartsRef = admin.database().ref('carts/' + data.uid)
cartsRef.once('value').then(snap => {
res.send(JSON.stringify({lineItems: snap.val()}))
}).catch(errorData => {
res.json({error: errorData})
})
}
})
})
So this get request will return to the user the line items that are currently in the user's cart. In my get method, after decoding the JWT token, I created the reference to the cart based on the user ID, then called "once" to get the line items from the database. And that's how I implemented the shopping cart. Happy coding!
Top comments (0)