Tutorial: A complete example

Serverless DatabaseSecurity Rules A complete example

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

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

Web application code (html/js)

<html>
  <head>
    <meta charset="UTF-8">
    <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 () {
        let authUser = null;
        const ref = new Webcom('<PLEASE_PUT_YOUR_NAMESPACE_NAME_HERE!!>');
    
        // Register authentication callback one time for all.
        // It will be called first at registration and then each time the authentication state changes
        ref.addAuthCallback(checkAuth);
    
        ref.child("messages").on("child_added", snapshot => {
          const msgId = snapshot.name();
          const 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", snapshot => {
          console.log("child removed :", snapshot.val())
          const 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.providerUid);
            }
          } else {
            console.log("Error auth: ", error);
            $("#statusMsg").text("Error auth: " + error.message);
          }
        }
    
        $("#chatMsgs").on("click", "button", event => {
          const mId = $(event.target).parent().attr("id");
          ref.child("messages").child(mId).remove();
        });
    
        $("#authButton").on("click", () => {
          console.log("authButton clicked");
          const credentials = {
            id: $("#emailInput").val(),
            password: $("#passwordInput").val()
          };
          ref.authInternally("password", credentials).catch(Function.prototype);
        });
    
        $("#createUser").on("click", () => {
          console.log("createUser clicked", $("#emailInput").val(), $("#passwordInput").val());
          ref.addIdentity("password", {id: $("#emailInput").val(), password: $("#passwordInput").val()})
                  .then(data => {
                    $("#statusMsg").text("Successfully created user account : " + data.email)
                    console.log("Successfully created user account : ", data);
                  })
                  .catch(error => {
                    console.log("an error occurred : ", error);
                    $("#statusMsg").text("an error occurred code=" + error.code + " message = " + error.message);
                  });
        });
    
        $("#unauthButton").on("click", () => {
          console.log("unauthButton clicked");
          ref.logout();
        });
    
        $("#sendChatMsgButton").on("click", () => {
          console.log("sendChatMsgButton clicked - (auth is " + authUser + ")");
    
          const theUid = authUser === null ? "anonymous" : authUser.uid;
          const theEmail = authUser === null ? "anonymous" : authUser.providerUid;
          const refNewMsg = ref.child("messages")
                  .push({msg: $("#msgInput").val(), uid: theUid, email: theEmail}, 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.providerUid === newData.val()"
        },
        "$other": {
          ".validate": false
        }
      }
    }
  }
}

Explanations

Read/Write rules

By default, reading and writing are forbidden (at the root level). In this service, reading messages is allowed for everybody (see the messages level).

Messages are written using push(), thus it creates some ids under the messages level, that's why we need to add the $msgId sub-level in security rules.

Under $msgId, we allow writing operations only for authenticated users (auth !== null) in one of the two following cases:

  • allow adding a new messages: in this case we check that no data already exist (!data.exists()) to prevent from overwriting data,
  • allow deleting user's own messages: in this case we check that existing data are owned by the currently authenticated user (auth.uid === data.child('uid').val())

Validation rules

.validate rules are useful to enforce "data model". Before allowing a write operation, all .validate rules must evaluate to true.

At the $msgId level: new data to be written are allowed if:

  • !newData.hasChildren(): we are attempting to delete a message,
  • or newData.hasChildren(['msg','uid','email']): the value to write has the three required fields msg, uid and email, whose content is in turn checked by the next rules:
    • At the msg sub-level: the field value must be a string with a length lesser than 100,
    • At the uid sub-level: the field value must match the identifier of the authenticated user,
    • At the email sub-level: the field value must match the email of the authenticated user,
    • At the $other sub-level: no other field value is allowed ("$other": {".validate": false}).
      This rule combined with the newData.hasChildren(['msg','uid','email']) condition at the upper level ensures that each entry as exactly the three required fields.