How to have Secure Rules on Firebase, and allow Auth Users to Read/Write

At the time of writing, there are built-in components for Firebase Authentication (Kodular) and extensions for App Inventor, that allow for users of an app to sign up and login in as an authenticated user to a Firebase realtime database. So far, so good. However many users have then expressed problems that setting the security rules in Firebase to anything other than " read:true / write:true ", prevents users, who should be authenticated, from any reading or writing.

The issue appears to be, for the extensions or built-in components, that the idToken required to "authenticate" the user/app is not being carried over into the blocks.

There is a workaround for this, to enable an authenticated user to read, write and delete data, by using the web component and Firebase's REST api.

Before I go any further, I must recommend the following reading, in order to help understand the requirements and the background:



I am going to use some fairly basic settings in Firebase for this example, the security rules will be set for any authenticated user to have read/write access to everything, and the sign up method will be a simple email and password, without any 2 factor verification requirements. These can all be expanded upon once everything is up and running. The aim is to grab the local ID created for the authenticated user, and use this as the base "key" in the data structure - with the intention that the user will only be able to access their data from the app (regardless of the security on Firebase)

Using the web component, instead of the firebase blocks, you need to construct a url which sets the node level and includes the idToken:

https://<firebaseUrl>/<ProjectBucket>/<userID>/<node.json?auth=<idToken>

Data Formatting:

If using this method, the web component delivers data to Firebase in a slightly different way than the built in Firebase component blocks. 

A single value would normally look like this on Firebase:     

key:"\"value\""

whilst with the web component it looks like this:

key:"value"

Both appear to come back to the app OK and convert to lists using the json decode block, but if they are mixed, then you can end up with double double quotes around values. This can be handled with text manipulation in the app, but it may better to stick with one method. 

You will need to use the web component method if you want secure rules and authentication!

Note: where I show the use of the firebase js files, make sure you go off and look for the latest versions.

Now to work:

On Firebase:


On App Inventor (using AI2 to for this example):

The Blocks:

We start off with the Sign Up process. The user enters a valid email and their chosen password and clicks the button. If you have the Firebase console open, you can check that the user has been created. (You may need to refresh the page).

Now the user can log in, entering the email address and password and clicking the Sign In button

At this point the app will gather the local ID for the user (on Firebase) and the idToken (which is required for authentication). These two are stored in global variables in the app for later use. The app shows the local ID and email address in the top label to indicate that the user is logged in.

The (now authenticated) user can enter some data. Enter a key/tag name and a value and press the Submit Data button. If using the Firebase console to check progress, select the database, and you should see the new entry; a node with the local ID, and below this a child node for the tag/key and its value. This uses a PUT HTTP request (I couldn't get POST to work...)

Now the user can press the Read Data button to return some data. If you look at the blocks, it will return everything under the current User (local ID) node as a json list. This uses a GET HTTP request.

To delete data we have to utilise web headers, using the X-HTTP-Method-Override=DELETE key / pair. In the example the key/pair that will be deleted is set in the Textbox3 at the level below the currentUser. Note again that we have to set the web headers to blank in the read and put blocks to prevent all the data being deleted. This is actually a POST HTTP request (see the documentation) but it is using GET.

For all three web HTTP requests we return responseContent. If content is not a list is handled in the blocks (when deleting you just get back "null")

Once you have security rules in place, Firebase will kick out a "Permission denied" error from the database. The authentication side will also produce errors if the email address supplied is not in the correct format or the password is not at least 8 characters, and it will also tell you if you try to Sign Up with an email that is already in use. These and any other errors are presented in the top label using the event blocks for the database and the authentication.

The Designer Screen

Sign Out / Log off is handled when the app is closed, but the idToken will expire after one hour, the user will need to Sign in again.

If you read through the firebase documentation, you will see it is possible to return ALL the data in the project or Project Bucket as a json, or to download it as a text file.

Here is some sample data as shown on Firebase. you can see how the local ID's are used as the "parent" nodes for each users data, below the project bucket. You can also see that Firebase is still reporting insecure rules, because any authenticated user could change anything.

Security

So far we have been running firebase in development mode, but now it is time to lock things down to authenticated users only.

Development Rules:

{

  "rules": {

    ".read": true,

    ".write": true

  }

}

Authenticated Rules: 

(just requires a sign in to read / write data)

Change the rules to the ones below, sign in again with a user, and test read/write. it should still work.

{

  "rules": {

        ".read": "auth != null",

        ".write": "auth != null"

   }

}

This has all been tested on AppInventor 2 using a GenyMotion Emulated Pixel 3XL (Android 9) and a free Firebase account.

A compiled version has also been tested on an HTC10 (Android 7)

CREDITS: Mrixtrem for the FirebaseAuth Extension

FBAuthSignUPIN_empty.aia (all firebase requirements are empty)

MORE:-

Taglist:

Data Changed

For dataChanged we need to use a different method, it will require a webviewer, and html file that monitors the state of the data. All data is returned by Firebase. We need to get a the firebase config file from Project Overview/Settings:

We then use the information to pass to the html to fill the firebase config file and provide the other variables (email,password, project bucket in order to return the data related to the user

The html file will return the data as a stringified JSON to the webviewstring on change. in this example I have have loaded the firebase js files required directly to the assets


<!DOCTYPE html>

<html>

<meta name=“viewport” content=“width=device-width, initial-scale=1.0”>

<meta charset="utf-8">


<head>

  <title>DataChanged</title>

 <script src="firebase-app.js"></script>

 <script src="firebase-auth.js"></script>

 <script src="firebase-database.js"></script>

</head>


<body>

 

<script>

 

var wvstr = window.AppInventor.getWebViewString();


var email = wvstr.split(",")[0];

var pass = wvstr.split(",")[1];

var setPB = wvstr.split(",")[2];

var uid = wvstr.split(",")[3];


var firebaseConfig = {

  apiKey: wvstr.split(",")[4],

  authDomain: wvstr.split(",")[5],

  databaseURL: wvstr.split(",")[6],

  projectId: wvstr.split(",")[7],

  storageBucket: wvstr.split(",")[8],

};


firebase.initializeApp(firebaseConfig);

var auth = firebase.auth();

auth.signInWithEmailAndPassword(email,pass);

  

auth.onAuthStateChanged(function(user) {

  if (user) {

    // User is signed in.

    getData();

  } else {

    // No user is signed in.

  }

});


function getData() {

var dbRefObject = firebase.database().ref().child(setPB +"/"+ uid);

dbRefObject.on('value', snap => { window.AppInventor.setWebViewString(JSON.stringify(snap.val(), null, 3)); }); 

}

 

</script>

</body>


</html>