Security & Privacy
App security Guidelines
22 min
how to make a secure app using parse introduction ahoy back4app community! this is a guest tutorial from joren winge at startup soul http //startup soul com/ we help startups build and launch their products fast our friends @ back4app asked us to show you how to build a secure app on top of back4app in this post, we’ll walk you through the steps to make a secure to do app on back4app security is important if your app takes off at all you will need to make sure your app’s data is secure and that your system can’t be hacked security features on parse lets first talk about the first level of security, acls (access control list) acls are basically just rules you set when you create an object let’s say you create a to do item, at the time of creation you can say who the item can be read by, and who it can be written by you can assign certain users to be able to read that item or write to that item or you can set either one to the public which allows access to anyone but acls don’t always work there are instances where you might need to have some more sophisticated logic instead of a simple acl sometimes you can also paint yourself in a corner where you may need to give someone access to an object on a conditional basis instead of an absolute basis like with acls so let’s skip using acls they are too rigid and at the same time allow for too much access to the data so i’m about to give you the secret to building a secure app on back4app and parse server you ready? class level permissions! yes, we will set class level permissions for each table in our database and the level of permission we will set is no permission at all we will lock down each and every table so that no read or write access is allowed to anyone! sounds extreme, i know, but this is the first step in creating a secure app the only permission we will allow is on the user table which will be for the creation of new user objects and for the user to view his own data which is required for refreshing the current user we will secure the user from being able to view other users data by using acls this is the one time we will use acls so i guess they are not totally useless they are good to have but don’t rely on them to do everything but how will we access the data you ask? good question, glad you are thinking about it! well, the secret to letting clients access the data in a controlled manner is to make each and every single interaction between the client and database filtered through a cloud code function yes, anytime you do anything with your app it will now be through a custom cloud code function no more client based pfqueries you pretty much skip using the entire client based parse sdk except for sign up functions, sign in functions, forgot password functions, and log out functions for these, we still will use the native client sdks it’s just easier have you ever written cloud code before? no, you say? well, it’s pretty easy it’s just javascript and it uses the parse javascript sdk but internally on your own app’s server in fact, since parse server is based on node js, it’s pretty similar to writing routes with express, but even easier since your query language is already installed and cloud code functions are a lot easier to write than an entire node js express app so here is what we will do we will use an ios todo app that i have already created we won’t bother with showing you how i created it instead, we will focus on writing cloud code and securing the database the todo app will be a secure app where you can only access your own todos and you can only write your own todos the data will be secure on the server safe from malicious rogue clients i will also show you how to write a secure parse background job basically, the same thing as a cron job so that you can have automated services send manipulate your data on a schedule sounds complicated but it’s not just imagine little server robots that do whatever you want on an automated schedule sounds cool right? ok, so here we go!!!!!! let’s set up the back4app secure todo app 1\) create an app on back4app create a new app on back4app call the app ‘secure todo app’ note follow the new parse app tutorial to learn how to create an app at back4app go into the app’s core settings page and then click on edit app details disable the checkbox called ‘allow client class creation’ to disable client class creation and hit save we want to limit what the client can do as a rule 2\) set class level security permissions for the user class next we will set the permissions for the user class go into the back4app database dashboard and click on the user class then click on the security tab, then click on the gear icon in the top right you should see a menu that says simple/advanced flip the slider to advanced you should then see the full class level permissions for this class disable the find checkbox disable the update and delete checkbox finally, disable the add field checkbox then hit save your security settings should look like this 3\) create the todo class hit create a class and call it todo set the class type as custom 4\) set class level security permissions for the todo class next we will set the permissions for the todo class go into the back4app database dashboard and click on the todo class then click on the security tab, then click on the gear icon in the top right you should see a menu that says simple/advanced flip the slider to advanced you should then see the full class level permissions for this class disable everything then hit save your security settings should look like this 5\) let’s add some columns to the todo class first let’s join the todo class to the user class we will do that by adding 2 columns the first column will be called ‘user’ and will be a pointer back to the user class next let’s create a column for the user’s object id who created it it will be a string type and will be called ‘userobjectid’ next let’s create a column to hold our actual todo information it will also be a string and will be call ‘tododescription’ let’s create a boolean to hold the state of the todo let’s call it ‘finished’ finally let’s add one more column to hold the date you finished your todo let’s call it ‘finisheddate’ and set it to a date type your todo class should look like this 6\) let’s go over the client the client is a pretty basic to do app it uses the built in parse functions to login, create a new user, and reset your password besides that everything is cloud code based and secure the user’s acl’s are also set as soon as they login or signup just to be 100% sure the system is secure let’s start by writing the cloud code function to set the user’s acl upon logging in or signing up at any time, you can access the complete ios project built with this tutorial at this github repository you can also access the main js cloud code file built for this tutorial at this github repository 1 in the client go to todocontroller swift and look for the function setusersaclsnow this function is called when you login or view the loggedinviewcontroller swift the function checks to see if you are logged in and if you are it calls the cloud function to setup your personal user acl’s todocontroller swift 1 func setusersaclsnow(){ 2 if pfuser current() != nil{ 3 let cloudparams \[anyhashable\ string] = \["test" "test"] 4 pfcloud callfunction(inbackground setusersacls, withparameters cloudparams, block { 5 (result any?, error error?) > void in 6 if error != nil { 7 //print(error debugdescription) 8 if let descrip = error? localizeddescription{ 9 print(descrip) 10 } 11 }else{ 12 print(result as! string) 13 } 14 }) 15 } 16 } 2 now let’s write the cloud code function parse server 3 x 1 parse cloud define('setusersacls', async(request) => { 2 let currentuser = request user; 3 currentuser setacl(new parse acl(currentuser)); 4 return await currentuser save(null, { usemasterkey true }); 5 }); parse server 2 x 1 parse cloud define('setusersacls', function (request, response) { 2 var currentuser = request user; 3 currentuser setacl(new parse acl(currentuser)); 4 currentuser save(null, { 5 usemasterkey true, 6 success function (object) { 7 response success("acls updated"); 8 }, 9 error function (object, error) { 10 response error("got an error " + error code + " " + error description); 11 } 12 }); 13 }); 3 this cloud code uses two key features of making your app secure, request user and masterkey request user let’s you access the user who is making the cloud code call and allows you to limit access for that user in this case, we are using it to set the user’s acl’s to limit read access to the current user only this way only the user can read their own information the class level permissions prevent write access even for the current user in this way users cannot modify their own information they can only change things about their own user through cloud code it is possible to import false information when the user first signs up, but i would reccomend writing a cloud code function to check the user’s information after a new user is created the built in parse function for creating a new user is really helpful so i think it’s a decent tradeoff, but you can always set the default values for the user via cloud code right after they sign up there are lots of fail safes you can also write into cloud code and have them run automatically and continously using background jobs to detect any malicious user information that was imported when the user was first created if you want to be really secure, you can store any sensitive information like membership status or payment information on a separate table from the user table that way the user cannot spoof any sensitive information on user creation 4 next let’s look at creating a todo in the client go to todocontroller swift and look for the function savetodo this function is called when you create a new todo the function takes a string that decribes the todo and saves it in the database todocontroller swift 1 func savetodo(todostring\ string, completion @escaping ( result bool, message\ string, todoarray \[todo]) >()){ 2 var resulttodoarray \[todo] = \[] 3 let cloudparams \[anyhashable\ any] = \["todostring"\ todostring] 4 pfcloud callfunction(inbackground createtodosforuser, withparameters cloudparams, block { 5 (result any?, error error?) > void in 6 if error != nil { 7 if let descrip = error? localizeddescription{ 8 completion(false, descrip, resulttodoarray) 9 } 10 }else{ 11 resulttodoarray = result as! \[todo] 12 completion(true, "success", resulttodoarray) 13 } 14 }) 15 } 5 now let’s write the cloud code function to save the todo in the database parse server 3 x 1 parse cloud define("createtodosforuser", async(request) => { 2 let currentuser = request user; 3 let todostring = request params todostring; 4 let todo = parse object extend("todo"); 5 let todo = new todo(); 6 todo set("user", currentuser); 7 todo set("userobjectid", currentuser id); 8 todo set("tododescription", todostring); 9 todo set("finished", false); 10 return await todo save(null, { usemasterkey true }); 11 }); parse server 2 x 1 parse cloud define("createtodosforuser", function(request, response) { 2 var currentuser = request user; 3 var todostring = request params todostring; 4 var todo = parse object extend("todo"); 5 var todo = new todo(); 6 todo set("user", currentuser); 7 todo set("userobjectid", currentuser id); 8 todo set("tododescription", todostring); 9 todo set("finished", false); 10 todo save(null, { 11 usemasterkey true, 12 success function (object) { 13 response success(\[todo]); 14 }, 15 error function (object, error) { 16 response error("got an error " + error code + " " + error description); 17 } 18 }); 19 }); 6 this cloud code function creates a todo object and sets the current user as the owner of the object this is important so that only the user who created it can find it or modify it by not allowing todos to be created in the client we are forcing the todo object to conform to our standards and making sure the todos are owned by the user who created them 7 next let’s look at retrieving the todos that you created from the server in the client go to todocontroller swift and look for the function gettodosfordate this function is called when you retrieve your todos the function takes a date as a parameter and uses it to retrieve a list of todos that were created by you before that date in descending order using a date is a great way to write a lazy loading query that doesn’t use skips skip can sometimes fail on a large dataset todocontroller swift 1 func savetodo(todostring\ string, completion @escaping ( result bool, message\ string, todoarray \[todo]) >()){ 2 var resulttodoarray \[todo] = \[] 3 let cloudparams \[anyhashable\ any] = \["date"\ date] 4 pfcloud callfunction(inbackground gettodosforuser, withparameters cloudparams, block { 5 (result any?, error error?) > void in 6 if error != nil { 7 if let descrip = error? localizeddescription{ 8 completion(false, descrip, resulttodoarray) 9 } 10 }else{ 11 resulttodoarray = result as! \[todo] 12 completion(true, "success", resulttodoarray) 13 } 14 }) 15 } 8 now let’s write the cloud code function to retrieve todos from the database based on a starting date we query for todos that are created before the parameter date so we use ‘query lessthan’ because dates are basically numbers that get larger the farther in the future you are i also included some tricky code here say we are including the user object who created the todo but we don’t want to share sensitive information about that user with other users we need to strip it from the json response so we have a for loop where we take the user object out of the todo, remove the email and username from the json and then put it back in the todo this is handy for removing sensitive data from an api call in situations where you cannot control what fields you return such as an included user object in this case we don’t really need it because this function will only return todos you created yourself we do this by using currentuser again to query for only todos created by the currentuser that was attached to the request the results are returned in descending order so that the latest todos appear first when you need to lazy load another batch of todos you take the createdat date from the last todo and use it as the date parameter for the next request parse server 3 x 1 parse cloud define("gettodosforuser", async(request) => { 2 let currentuser = request user; 3 let date = request params date; 4 let query = new parse query("todo"); 5 query equalto("user", currentuser); 6 query lessthan("createdat", date); 7 query descending("createdat"); 8 query limit(100); 9 query include("user"); 10 let results = await query find({ usemasterkey true }); 11 if(results length === 0) throw new error('no results found!'); 12 13 let resultsarray = \[]; 14 for (let i = 0; i < results length; i++) { 15 let todo = results\[i]; 16 let tempuser = todo get("user"); 17 let jsonuser = tempuser tojson(); 18 delete jsonuser email; 19 delete jsonuser username; 20 21 jsonuser type = "object"; 22 jsonuser classname = " user"; 23 24 let cleanedtodo = todo tojson(); 25 cleanedtodo user = jsonuser; 26 cleanedtodo type = "object"; 27 cleanedtodo classname = "todo"; 28 resultsarray push(cleanedtodo); 29 } 30 return resultsarray; 31 }); parse server 2 x 1 parse cloud define("gettodosforuser", function(request, response) { 2 var currentuser = request user; 3 var date = request params date; 4 var query = new parse query("todo"); 5 query equalto("user", currentuser); 6 query lessthan("createdat", date); 7 query descending("createdat"); 8 query limit(100); 9 query include("user"); 10 query find({ 11 usemasterkey true, 12 success function (results) { 13 var resultsarray = \[]; 14 for (var i = 0; i < results length; i++) { 15 var todo = results\[i]; 16 var tempuser = todo get("user"); 17 var jsonuser = tempuser tojson(); 18 delete jsonuser email; 19 delete jsonuser username; 20 21 jsonuser type = "object"; 22 jsonuser classname = " user"; 23 24 var cleanedtodo = todo tojson(); 25 cleanedtodo user = jsonuser; 26 cleanedtodo type = "object"; 27 cleanedtodo classname = "todo"; 28 resultsarray push(cleanedtodo); 29 } 30 response success(resultsarray); 31 }, 32 error function (error) { 33 response error(" error " + error code + " " + error message); 34 } 35 }); 36 }); 9 now that we have the todos we can see them in the app and mark them as completed if we want lets cover that next 10 to mark a todo as completed just hit the ‘mark as completed’ button on any of the todos you created this will fire off a method in the todocontroller swift called ‘marktodosascompletedfor’ that takes the todo you selected as a parameter it sends the todo objectid to the server as a parameter and then returns the updated todo as a result todocontroller swift 1 func marktodosascompletedfor(todo\ todo, completion @escaping ( result bool, message\ string, todoarray \[todo]) >()){ 2 var resulttodoarray \[todo] = \[] 3 let cloudparams \[anyhashable\ any] = \["todoid"\ todo objectid ?? ""] 4 pfcloud callfunction(inbackground marktodoascompletedforuser, withparameters cloudparams, block { 5 (result any?, error error?) > void in 6 if error != nil { 7 if let descrip = error? localizeddescription{ 8 completion(false, descrip, resulttodoarray) 9 } 10 }else{ 11 resulttodoarray = result as! \[todo] 12 completion(true, "success", resulttodoarray) 13 } 14 }) 15 } 11 now we’ll write the cloud code to update this todo it looks for the todo to update based on the objectid but it also uses the currentuser to make sure that the todo that is associated with the objectid was created by the user making the query this makes sure that you can only view todos that you created and is thus secure we include a limit of 1 result to make sure the server doesn’t continue to search after finding the todo there’s another method for finding an object based on an objectid but i don’t like to use it since it can return weird results if it doesn’t find the object associated with the objectid we are also setting the ‘finisheddate’ with the current date when the object was updated by having the finisheddate set by this function only we’ve made sure the finisheddate is secure and cannot be faked or changed we also used ‘query equalto(“finished”, false)’ to make sure that only an unfinished todo can be marked as finished and have the finisheddate set that means once a todo has been marked as finished it can never be marked finished again at a later date parse server 3 x 1 parse cloud define("marktodoascompletedforuser", async(request) => { 2 let currentuser = request user; 3 let todoid = request params todoid; 4 let query = new parse query("todo"); 5 query equalto("user", currentuser); 6 query equalto("objectid", todoid); 7 query equalto("finished", false); 8 let todo = await query first({ usemasterkey true }); 9 if(object keys(todo) length === 0) throw new error('no results found!'); 10 todo set("finished", true); 11 let date = new date(); 12 todo set("finisheddate", date); 13 try { 14 await todo save(null, { usemasterkey true}); 15 return todo; 16 } catch (error){ 17 return("getnewstore error " + error code + " " + error message); 18 } 19 }); parse server 2 x 1 parse cloud define("marktodoascompletedforuser", function(request, response) { 2 var currentuser = request user; 3 var todoid = request params todoid; 4 var query = new parse query("todo"); 5 query equalto("user", currentuser); 6 query equalto("objectid", todoid); 7 query equalto("finished", false); 8 query limit(1); 9 query find({ 10 usemasterkey true, 11 success function (results) { 12 if (results length > 0) { 13 var todo = results\[0]; 14 todo set("finished", true); 15 var date = new date(); 16 todo set("finisheddate", date); 17 todo save(null, { 18 usemasterkey true, 19 success function (object) { 20 response success(\[todo]); 21 }, 22 error function (object, error) { 23 response error("got an error " + error code + " " + error description); 24 } 25 }); 26 } else { 27 response error("todo not found to update"); 28 } 29 30 }, 31 error function (error) { 32 response error(" error " + error code + " " + error message); 33 } 34 }); 35 }); 7\) wrap up! and that’s it you have built a secure todo app again, the key to making a secure app on parse server is disabling all class level permissions for all classes except for the user class on the user class, you disable all permissions except create, and get also make sure to set all user’s acl’s so that the user can only get their own data then all your interactions go through cloud code and are filtered using the request user aka the currentuser so there ya go, you can now build secure systems on top of parse server and back4app but wait you say? what about background jobs and live queries well, you have a good point, so i will cover that in two bonus sections next 8\) bonus sections 1 background jobs sometimes you need to create a background job to run every hour, or every day or every week if you are running with all you class level permissions turned off, your background job will not be able to query the database unless it’s set up correctly this is kind of tricky to do so i want to include an example of it here in this case we will create a background job that checks the database for unfinshed todos that are more than 1 year old and then automatically marks them as finished the trick here is using ‘usemasterkey’ correctly it has to be added to the query before the then promise just follow this template and you should be able to write secure background jobs easily you always start with writing a query that you want to iterate over the entire database and then make sure to include status error if there is an error and end it with a status success to make sure it completes you can watch the logs on back4app to see the background job working while you run it parse server 3 x 1 parse cloud job("markunfinishedtodosolderthan1yearasfinished", async(request) => { 2 let date = new date(); 3 let intyear = date getfullyear() 1; 4 let query = new parse query("todo"); 5 query equalto("finished", intyear); 6 query lessthan("createdat", date); 7 8 let todo = await query find({ usemasterkey true }); 9 for (let i = 0; i < results length; i++) { 10 let todo = results\[i]; 11 todo set("finished", true); 12 todo set("finisheddate", date); 13 try { 14 await todo save(null, { usemasterkey true}); 15 } catch (error){ 16 console log("getnewstore error " + error code + " " + error message); 17 } 18 } 19 return "migration completed successfully "; 20 }); parse server 2 x 1 parse cloud define("marktodoascompletedforuser", function(request, response) { 2 var currentuser = request user; 3 var todoid = request params todoid; 4 var query = new parse query("todo"); 5 query equalto("user", currentuser); 6 query equalto("objectid", todoid); 7 query equalto("finished", false); 8 query limit(1); 9 query find({ 10 usemasterkey true, 11 success function (results) { 12 if (results length > 0) { 13 var todo = results\[0]; 14 todo set("finished", true); 15 var date = new date(); 16 todo set("finisheddate", date); 17 todo save(null, { 18 usemasterkey true, 19 success function (object) { 20 response success(\[todo]); 21 }, 22 error function (object, error) { 23 response error("got an error " + error code + " " + error description); 24 } 25 }); 26 } else { 27 response error("todo not found to update"); 28 } 29 30 }, 31 error function (error) { 32 response error(" error " + error code + " " + error message); 33 } 34 }); 35 }); 2 live queries sometimes you need to use parse’s live query feature for something like a live chat app you will want to use the live query to see when new messages for your user are created live query is basically just parse’s way of using sockets to get live updates it’s pretty handy but it will not work with a class who’s find permissions have been turned off so in this case we will turn the find permissions back on for the message class and instead we will assign acl’s for that message directly the acl should be set so that only the recipient can use a find to get the message from the server then you run your pf live query in your client looking for messages for your user and it will work flawlessly if you are dealing with group messages, it’s a bit different you can assign multiple people to be on the acl but, it really doesn’t scale instead, there is a better way you set the acl to be based on a role parse role and then any user you want to have access to that message you just assign them to that parse role if you want to stop them from reading the messages for that group, you remove them from that role this is way easier than removing them from every single message’s acl and it scales for super large groups this is the correct way to do it i’m not going to leave a code sample for this as it’s too complex for this tutorial, but maybe i’ll explain how to do it in my next one thank you for reading this tutorial on security with parse and back4app if you have questions feel free to contact me and i’ll be happy to answer them thanks! joren