-
Notifications
You must be signed in to change notification settings - Fork 5
Bootcamp Part 6: Firebase Security & Rules
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.
Again, the videos are split up as follows (they are also linked in their respective sections below):
- Firebase Security Rules: How to protect your Firebase Realtime Database with Security Rules
- Intro to Firebase Cloud Functions: Covers cloud functions in the context of security conceptually and how to set them up in our application.
- Firebase Cloud Functions: Write our first cloud function to get homepage data and call it from our React application
You can find the slides here: Slides
- 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
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",
}
},
}
}
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).
- More in depth introduction to Firebase Security rules: https://firebase.google.com/docs/database/security/securing-data
- Full Security Rules Reference: https://firebase.google.com/docs/reference/security/database
Here are some security rules to implement yourself:
-
/flashcards/$deckId
write permission: Users can only write to a flashcards deck if either the existing data has anowner
attribute with value equal to the current user's uid OR there was no existing data and the new data being written has theowner
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?
-
/homepage/$deckId
write permission: should be the same as the flashcards write permission above. -
/users/$uid
write permission: users should only be able to write to their own profile.
(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 istrue/false
orprivate/public
or whatever the binary is there. Also make sure that the value of theowner
attribute is an existing user.
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.
-
In your terminal, run
firebase init
. -
Choose
Functions
by using the arrows and space to select. -
Then, to answer the questions: choose
Javascript
,No
to ESlint, and finallyYes
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).
Link to Video
The code diff for this video: https://github.com/harvard-datamatch/bootcamp/commit/383b1558dc9174861be68206aacb6b4317537a53
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"
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
!
- More on callable cloud functions: https://firebase.google.com/docs/functions/callable#web
- Admin SDK, how to interact with Realtime Database directly: https://firebase.google.com/docs/reference/admin/node/admin.database.DataSnapshot
- Interested in React lifecycle methods? https://reactjs.org/docs/react-component.html#componentdidmount
-
(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 intocreateDeck
, andcreateDeck
will add the new deck to the database, if it's a valid deck object. The main thing to check is if thecards
attribute of the deck object is an array of cards.- How to check if an array is actually an array: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
- After implementing the
createDeck
cloud function, set the security rules for writing to the flashcards path tofalse
, since people should only be creating decks using this newcreateDeck
cloud function (unless you have edit capabilities implemented).
-
(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 usingpopulate
in the CardViewer component, use thisgetUsername
to get the flashcard deck owner's username.- You'll have to use
componentDidUpdate
to check if theowner
attribute of the flashcard deck changed, and once it does, then you'll invoke thegetUsername
on theowner
attribute to retrieve the correct username.
- You'll have to use
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
- 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