Tutorial: A complete example

Step by stepSecurity A complete example

This example intend to illustrate how you can implement a chat service that uses user/password authentication.

It shows how to write security rules to enfore data model (ie. check message format) and how to ensure that only the user that wrote a message can delete it.

Web application code (html/js)

<html>
  <head>
    <title>Sample chat with security</title>
    <script type='text/javascript' src='https://datasync.orange.com/libjs/latest/webcom.js'></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script> 
  </head>
  <body>
    email: <input type="text" id="emailInput"></input>
    <br/>
    password: <input type="password" id="passwordInput"></input>
    <br/>
    <button id="createUser" type="button">create a user</button> 
    <button id="authButton" type="button">login</button> 
    <button id="unauthButton" type="button">logout</button> 
    <hr/>
    Last info message: <div id="statusMsg"></div>
    <hr/>
    <b>Authentication state:</b> <span id="authStatus"></span>
    <hr/>
    message: <input type="text" id="msgInput"></input>

    <button id="sendChatMsgButton" type="button">send</button> 
    <hr/>
    <ul id="chatMsgs"></ul>


    <script type="text/javascript">

      $(document).ready(function() {

        var authUser = null;
        var ref = new Webcom('https://io.datasync.orange.com/base/<PLEASE_PUT_YOUR_NAMESPACE_NAME_HERE!!>');

        // Register authentication callback one time for all.
        // It will be called first at webapp startup ('resume' method below) and when authentication state changes
        ref.registerAuthCallback(checkAuth);
        // Resume authentication recorded in browser.
        ref.resume(); 

        ref.child("messages").on("child_added",function(snapshot){
            var msgId=snapshot.name();
            var msg=snapshot.val();
            console.log("msg added :", msg);
            $("#chatMsgs").append($("<li/>").attr("id", msgId).text(msg.msg + " --- by " + msg.email).append($("<button/>").text("delete")));
        });

        ref.child("messages").on("child_removed",function(snapshot){
            console.log("child removed :", snapshot.val())
            var mId=snapshot.name();
            $("#"+mId).remove();
        });

        function checkAuth(error, auth) {
          console.log("checkAuth");

          if(error == null){
            authUser = auth;
            if(auth == null) {
                $("#authStatus").text("Not connected");
            }else{
                $("#authStatus").text("Authenticated: " + auth.email);
            }
          }else{
            console.log("Error auth: ", error);        
            $("#statusMsg").text("Error auth: " + error.message);
          }
        }

        $("#chatMsgs").on("click", "button", function(e){
          var mId=$(this).parent().attr("id");
          ref.child("messages").child(mId).remove();
        });

        $("#authButton").on("click", function() {
          console.log("authButton clicked");
          var credentials = {
              email: $("#emailInput").val(),
              password: $("#passwordInput").val()
          };        
          ref.authWithPassword(credentials);
        });


        $("#createUser").on("click", function() {
          console.log("createUser clicked",$("#emailInput").val(),$("#passwordInput").val());
          ref.createUser( $("#emailInput").val(),$("#passwordInput").val(), function(error, data){
             if(error){
                console.log("an error occurred : ",error);
                $("#statusMsg").text("an error occurred code=" + error.code + " message = " + error.message);
             }else{
               $("#statusMsg").text("Successfully created user account : " + data.email)
               console.log("Successfully created user account : ", data);
             }
          });

        });    

        $("#unauthButton").on("click", function() {
          console.log("unauthButton clicked");
              ref.logout();
        });    

        $("#sendChatMsgButton").on("click", function() {
          console.log("sendChatMsgButton clicked - (auth is" + authUser + ")");

            var theUid = (authUser===null ? "anonymous" : authUser.uid);
            var theEmail = (authUser===null ? "anonymous" : authUser.email);
            var refNewMsg = ref.child("messages").push({ msg: $("#msgInput").val(), uid: theUid, email:theEmail }, function(error) {
                if(error){
                    $("#statusMsg").text("sync with server error " + error.message);
                }   else{
                    $("#statusMsg").text("sync with server ok " + refNewMsg.toString());
                }
            });
        });   
      });

    </script>
  </body>
</html>

Security rules

Here are the corresponding security rules:

{
  "rules": {
    "messages": {
      ".read": true,
      "$msgId": {
        ".write": "auth != null && (!data.exists() || auth.uid === data.child('uid').val())",
        ".validate": "!newData.hasChildren() || newData.hasChildren(['msg','uid','email'])",
        "msg": {
          ".validate": "newData.isString() && newData.val().length() < 100"
        },
        "uid": {
          ".validate": "auth.uid === newData.val()"
        },
        "email": {
          ".validate": "auth.email === newData.val()"
        },
        "$other": {
          ".validate": false
        }
      }
    }
  }
}

Explanations:

Read/Write rules:

By default reading and writing are forbidden. In this service read is allowed by everybody in messages.

Messages are written using push(), thus it creates some ids under "messages", that why we need to add a level in security rules: $msgId.

Under $msgId, we allow write operations only for authenticated users (auth != null) and in 2 cases:

  • allow to add a new messages: in this case we check that data does not exists (prevent data overwrite) (!data.exists())
  • allow to delete user own messages: in this case we check that existing data is owned by current connnected user (auth.uid === data.child('uid').val())

Validation rules:

.validate rules are usefull to enfore "data model". Before allowing a write operation, all .validate rules must return true

In $msgId: new data to be written in allowed if:

  • !newData.hasChildren(): delete a message, or
  • newData.hasChildren(['msg','uid','email']) it has 3 required fields msg, uid, email which content will be checked by next rules:
    • "msg" field must be a string with length < 100
    • "uid" field must match the one from authenticated user
    • "email" field must match the one from authenticated user
    • no other field is allowed ("$other": { ".validate": false }). This rules combined with hasChildren() at upper level ensure that each entry as exactly the 3 required fields.