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 fieldsmsg
,uid
andemail
, 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 thenewData.hasChildren(['msg','uid','email'])
condition at the upper level ensures that each entry as exactly the three required fields.
- At the