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('<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, ornewData.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 withhasChildren()
at upper level ensure that each entry as exactly the 3 required fields.