Skip to content

Bootcamp Part 6: Firebase Security & Rules

ashleyzhuang edited this page Oct 6, 2021 · 1 revision

This week we’ll be covering security: how to protect your application’s data and to think thoroughly about who has access to your user's data. Security may not seem like the most interesting topic, but it’s super imperative to consider and understand.

Video

Again, the videos are split up as follows (they are also linked in their respective sections below):

You can find the slides here: Slides

Week 5 Solution

  • Video will be posted soon
  • This is the code diff that I made
  • You can access the rest of the files for that week's solution here

Firebase Security Rules

Link to Video

The rules that I added this video: https://github.com/harvard-datamatch/bootcamp/commit/251d1fd3daf6c5848cc0d42039d1545be3e5197a

Firebase Security rules stand between clients (like our application or someone malicious) and the database and make sure the clients are only reading and writing data that they have access to. Rules are written in JSON format, the same as your database structure:

{
  "rules": {
    "flashcards": {
      ".read": true,
      ".write": false,
    },
    "users": {
      "$uid": {
        ".read": "auth.uid == $uid",
        ".write": "auth.uid == $uid",
      }
    },
  }
}

Important Points from Video

Rule Cascading: if you allow/disallow read/write permissions at a certain path, then it allows/disallows reads at that certain path and ALL children paths. This means that you should try to allow/disallow permissions at the most finest-grain (deepest) path.

User-based assertions: the keyword is auth and you'll probably be using auth.uid only. This gives the authentication data of the current user.

Wildcard matching: in the example shown above, the dollar sign $uid denotes a wildcard. The specific wildcard above matches all database paths of the form /users/$uid, so $uid = "asdf1234" if the database path is /users/asdf1234. We can use the wildcard value in our assertions, like "auth.uid == $uid", so this assertion/condition relies on the value of the wildcard $uid (the specific database path the user is trying to access). The rule above allows read/write permissions to /users/$uid if the current user has the same user ID as $uid.

Content-based assertions: the keywords are data (existing data already in the database) and newData (only applies to writes, but is the data that is about to be written).

Resources

TASK #1

Here are some security rules to implement yourself:

  1. /flashcards/$deckId write permission: Users can only write to a flashcards deck if either the existing data has an owner attribute with value equal to the current user's uid OR there was no existing data and the new data being written has the owner attribute value equal to the current user's uid.

    • Thought exercise: why do we need to have 2 cases, where the data already exists or the data doesn't exist?
  2. /homepage/$deckId write permission: should be the same as the flashcards write permission above.

  3. /users/$uid write permission: users should only be able to write to their own profile.

Additional (optional) Tasks

(Easy) Using a ".validate" rule, make sure that new flashcard decks that are being added have the cards, owner and visibility attributes.

  • (Medium) Make sure that the value of the visibility attribute is true/false or private/public or whatever the binary is there. Also make sure that the value of the owner attribute is an existing user.

Intro to Firebase Cloud Functions + Set-up

Link to Video

In the context of security, Firebase Cloud Functions are functions that run Javascript code in isolated servers and have full access to our Firebase Realtime Database. If security rules are too difficult to write (or impossible to), cloud functions provide the necessary data protection and security needed to keep sensitive data out of the reach of malicious hands and only present cleaned, sanitized data.

Cloud Functions Set-up

  1. In your terminal, run firebase init.

  2. Choose Functions by using the arrows and space to select.

  3. Then, to answer the questions: choose Javascript, No to ESlint, and finally Yes to install dependencies with npm.

You'll know if you successfully initialized cloud functions if you have a functions folder and inside the functions folder, you have a index.js file.

I didn't touch upon this in the video, but if Cloud Functions are so isolated and protective of our data, why do we even use Security Rules in the first place? One of the cons of using cloud functions is their performance (in terms of speed)–you have to send a request to the cloud function, the cloud function has to talk with the Realtime Database, etc. there are a lot of network requests and so calling cloud functions tend to take longer than just asking for data in the Realtime Database. Cloud Functions also cost money, based on the number of function invocations, whereas Security Rules are free and most of the time can do the same job (at a much less performance cost).

Firebase Cloud Functions

Link to Video

The code diff for this video: https://github.com/harvard-datamatch/bootcamp/commit/383b1558dc9174861be68206aacb6b4317537a53

Environment Variables

Here is the export environment variables command in other operating systems!

  • UNIX/Mac OSX:

    export GOOGLE_APPLICATION_CREDENTIALS=/Users/joshuapan/Desktop/projects/bootcamp/functions/bootcamp-ce748-firebase-adminsdk-5oihh-a190589107.json
    
  • Windows (non Powershell users):

    setx GOOGLE_APPLICATION_CREDENTIALS "C:\Users\joshuapan\Desktop\projects\bootcamp\functions\bootcamp-ce748-firebase-adminsdk-5oihh-a190589107.json"
    
  • Windows (Powershell users):

    $env:GOOGLE_APPLICATION_CREDENTIALS="C:\Users\joshuapan\Desktop\projects\bootcamp\functions\bootcamp-ce748-firebase-adminsdk-5oihh-a190589107.json"
    

Important Points from Video

Creating a cloud function: The following is a simple cloud function

exports.helloWorld = functions.https.onCall((data, context) => {
  return { hello: 'world' };
});

The name of our cloud function is the exported name, in this case helloWorld. Whatever function we want the cloud function to run, we pass as a parameter into functions.https.onCall. The function that is passed into functions.https.onCall gets passed in 2 parameters: data, which is the data that gets passed into the cloud function (when it's invoked/called), and context, which has the authentication of the user who invoked the cloud function (you can access the authentication data via context.auth and the uid from context.auth.uid). Whatever is returned by the function that is passed into functions.https.onCall is returned to the React application wrapped in a data attribute, like the following.

{
  data: { hello: 'world' }
}

Calling a Cloud Function in React: Make sure that you import 'firebase/functions' in your index.js file. Then you'll have access to your cloud function by name, helloWorld in this case, const helloWorld = this.props.firebase.functions().httpsCallable('helloWorld') (assuming you're already using the firebaseConnect HOC to give your component access to Firebase props). Then you can use helloWorld like any other function and pass data into it as an object (which the cloud function will have access to with its data parameter). Note that since your React application and Firebase Cloud Functions are on different servers on the internet, it takes time to fulfill the request, so you must await for your cloud function calls, like await helloWorld();.

Local functions emulator/Deploying cloud functions: Since cloud functions are run on their own isolated server, to test them locally, we must run them locally on our computer. To do this, make sure you're in the functions folder of your bootcamp project and run npm run serve. This will boot up the cloud functions emulator, which mimics what would happen in the Firebase cloud except locally on your computer. To tell your React application that you are using a local emulator, you'll have to add firebase.functions().useFunctionsEmulator('http://localhost:5001'); (or wherever the cloud function emulator host/port is).

To deploy a cloud function to the Firebase cloud, since other people will not be able to use the cloud function unless it is deployed to the cloud, in your functions folder run npm run deploy (not firebase deploy). This will deploy your Firebase cloud functions only (not your hosted website). Once you have deployed your Firebase Cloud Function, you can then comment out the useFunctionsEmulator line to use the deployed instance of the cloud function (instead of your locally emulated cloud function). You must comment this line out before you deploy your React Application because your application will break since it cannot find localhost:5001.

Be careful with running firebase deploy now! This will deploy both your cloud functions AND your hosted website. If you only want to deploy your hosted website, run firebase deploy --only hosting!

Resources

Additional (optional) Tasks

  1. (Medium-Hard) Create a cloud function createDeck that creates a new deck. So instead of updating Firebase Realtime Database from our React application, it passes the whole deck object as the parameter into createDeck, and createDeck will add the new deck to the database, if it's a valid deck object. The main thing to check is if the cards attribute of the deck object is an array of cards.

  2. (Medium-Hard) Create a cloud function getUsername that takes as input the uid of a user and returns the username of that user. Instead of using populate in the CardViewer component, use this getUsername to get the flashcard deck owner's username.

    • You'll have to use componentDidUpdate to check if the owner attribute of the flashcard deck changed, and once it does, then you'll invoke the getUsername on the owner attribute to retrieve the correct username.

Once you're done

Fill out this form when you're done! Make sure to deploy your app with firebase deploy --only hosting, so I can take a look. It'll notify me to review your work and let me know you finished.

Since this is the last week of material, I'd love your feedback on the whole bootcamp as a whole! This form also asks if you want to be added to the Datamatch Web team (and be considered for the Datamatch Web Lead role) Please fill out that form here: https://forms.gle/ytiHqzhxuS45gjnv8

Week 6 Solution

  • Video will be posted soon
  • This is the code diff that I made
    • Note that these updated rules don't make our database bulletproof, there are still some security holes that are vulnerable to attacks. Feel free to patch these however you like!
  • You can access the rest of the files for that week's solution here