Build private chats in Firebase and Javascript - javascript

I am building an app with Firebase that requires private messaging between users.
What I need is to build single chats for 1-1 chats, storing messages in Firestore.
My idea: I guess the best way is to build a single collection for each chat, with the right security rules. Let's say an user with tokenID 1234 wants to talk with user with tokenID 1111, a new collection called 1234_1111 will be created (if not existing), and the security will allow only these two users to read and write.
My question: Is it the right way? And how to do that in Javascript? I'm not sure how to define security rules directly in the JS code, neither how to create a collection with the two users ID.

Security Rules are not defined in your JavaScript code, they are defined separately. What you suggest would be a reasonable approach to take although I'd use a subcollection for it, and a simplified version of your security rules might look something like:
service cloud.firestore {
match /databases/{database}/documents {
match /dms/{idPair}/messages/{msgId} {
allow read, write: if
idPair.split('_')[0] == request.auth.uid ||
idPair.split('_')[1] == request.auth.uid;
}
}
}
Then in your JS code you could do something along the lines of:
// generate idPair
function dmCollection(uid) {
const idPair = [firebase.auth().currentUser.uid, toUid].join('_').sort();
return firebase.firestore().collection('dms').doc(idPair).collection('messages');
}
// send a DM
function sendDM(toUid, messageText) {
return dmCollection(toUid).add({
from: firebase.auth().currentUser.uid,
text: messageText,
sent: firebase.firestore.FieldValue.serverTimestamp(),
});
}
// retrieve DMs
function messagesWith(uid) {
return dmCollection(uid).orderBy('sent', 'desc').get();
}
Note that the idPair is constructed by joining a sorted pair of UIDs so that it will be stable no matter which user sends.

Related

I want to make unique usernames in firebase/firestore

app : {
users: {
"some-user-uid": {
email: "test#test.com"
username: "myname"
}
},
usernames: {
"myname": "some-user-uid"
}
}
I want to make unique usernames, like this: I would store all the usernames in a collection in firebase and than check if that doc with that username exists. This works perfectly fine but when I make a username with the same name on 2 accounts at the same time, user 1 makes the file and then user 2 overwrites that file.
How can I get around this?
This is not a copy of an asked question, all the answers answer how to make unique usernames but this bug is in all of them.
To prevent somebody being able to overwrite an existing username document, you'd use Firebase's security rules:
service cloud.firestore {
match /databases/{database}/documents {
// Allow only authenticated content owners access
match /usernames/{username} {
allow create: if !exists(/databases/$(database)/documents/usernames/$(username))
}
}
}
The above example allows creating the document if it doesn't exist yet. So with this the second write will be rejected.
You'll probably want to expand this to allow a user to only claim with their own UID, and unclaim a name too. To learn more on how to do that, I recommend reading the Firebase documentation on security rules.

Firestore Security Rules - Same Rule Works for Write But Not for Read

I am developing a Firebase project where I am using firestore.
I am querying Firestore to get Chat Messages from the messages collection. I want only messages beloning to a certain conversation:
const messages = await db
.collection('messages')
.where('room', '==', room)
.onSnapshot(snap => {//stuff})
So fa so good. It works. Things go wrong when I set up security rules.
If I do somthing simple, such as:
allow read: if request.auth != null;
everything is fine. But if I want to allow access only to users whose uid is included in the 'partiesIDs' message object property, things go wrong:
allow read: if
request.auth.uid == resource.data.pertiesIDs[0] ||
request.auth.uid == resource.data.parties[1];
The strangest thing of all is that I have in place a very similar rule for update, which works as expected:
allow update: if
(request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['read', 'notified'])) &&
(request.auth.token.name == resource.data.parties[0] ||
request.auth.token.name == resource.data.parties[1]);
The query for the second rule (which works) looks like this:
const update = await db
.collection('messages')
.doc(docid)
.update({read: true, notified: true})
I m stuck! Can anybody shed some light into this mistery?
The problem is that Firestore security rules are not filters. I strongly suggest reading that documentation.
When you write rules to place conditions on the reading of documents within a collection, the client is obliged to make a query that matches exactly the conditions of the rules. When you make a requirement that some data must exist in some field, then your query must match that by filtering for only documents that would satisfy the contents of the fields required by the rules. The rules will not extract only the matching documents to return them. You can think of the client query as demanding the full set of documents that match the given filters, and the rules as rejecting that demand because the conditions are not satisfied.
However, you have a bit of a problem here, because queries don't have a way of specifying array indexes. It's not possible to make a query that requires index 0 of an array field must contain a certain value.
I suggest rethinking your document data and rules, and structure them in such a way that the client app can exactly match their requirements.
Solved!
#Doug Stevenson thanks for having pointed me to the right direction.
The rules I have in place are the following:
allow read: if request.auth.token.name == resource.data.receiver || request.auth.token.name == resource.data.sender;
allow create: if request.auth != null;
allow update: if (request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['read'])) && (request.auth.token.name == resource.data.parties[0] || request.auth.token.name == resource.data.parties[1]);}
allow delete: if false;
With these rules I am able to do the following:
First Load: load chats messages of a given room through a cloud function (it has admin priviledges, so no security rules issues there)
Get Incoming Messages: add an onsnapshot realtime listener to get all messages where receiver is the logged in user and property room is equal to the open room
await db.collection('messages').where("receiver","==",user.displayName).where("room", "==", room).orderBy('timestamp').onSnapshot(snapshot => { //Do Stuff })
Updated Read Status: when a message is displayed, the receiver updates the read property of the mssage in firestore:
const update = await db.collection("messages").doc(id).update({read: true});
Render Read Status in the UI in real time: add a second onsnapshot realtime listener to get all messages where sender is the logged in user and room is equal to the open room
const readUpdate = await db.collection('messages')
.where("sender", "==", user.displayName)
.where("room", "==", room)
.orderBy('timestamp')
.onSnapshot(snapshot => { //Do Stuff })
Send Message: user sends new messages through firestore. On the sender side, new messages are rendered locally through JS (Not through a real time listener).
On the receiving side, new messages are delivered and displayed throught the listener described in 2)
const deliverMSG = await db.collection('messages').doc(newID).set(newMsgObj);
//Render Message
renderMSG({id: newID, data: newMsgObj});
Perhaps this is not the most elegant way to handle this, but it works pretty well and is definitelly secure.
If you have any extra tip, I ll be very happy to read it.

Can a user update the script in a console and access other users' data in my firebase code?

For context, I display my Firebase app's configuration in the app.js file (is this safe?) and in order to give a user their data I list the following code:
db.collection('whispers').where("sender", "array-contains",
userID).orderBy('time').onSnapshot(snapshot => {
let changes = snapshot.docChanges();
changes.forEach(change => {
renderSay(change.doc);
});
});
Could a user remove the 'where' condition and access all data in this collection? I only use anonymous sign in so nothing special required in accessing their own data besides using the same device and not clearing data.
I doubt that could be possible by reverse-engineering but the actual security lies in the security rules.
You can change them from the Firebase Console
Here is a video from Firebase and here is the documentation
service cloud.firestore {
match /databases/{database}/documents {
// Match any document in the 'users' collection
match /users/{userID} {
allow read: if request.auth.uid === userID;
allow write: if request.auth.uid === userID;
}
}
}
Using the above security rules will allow users to read and write their data ONLY.
So even if they reverse-engineer your code, it will harm their data only and other data will be safe.
For more details please check the links above and you will get familiar with wildcard [{userID} in this case]
PS: Only you or anyone with access to your Firebase Console can change the rules
EDIT: This Medium Article has many types of rules explained. Please have a read there too :D

Administrator Views for a Firebase Web Application: How To

My Firebase web app requires administrator access, i.e., the UI should show a few things only for admins (an 'administrator' section). I came up with the below as a means to authorize the UI to display the admin section for valid admins only. My question is, good or bad? Is this a sound means of authorizing? ...so many ways to do this. This particular way requires me to configure admins in the security rules (vs in a node/tree in a db/firestore)
My idea is that if the .get() fails due to unauthorized access, I tell my app logic the user is not an admin, if the .get() succeeds my logic shows the 'admin' sections. Of course, the 'sections' are just HTML skeletons/empty elements populated by the database so even if the end user hacks the JS/logic, no real data will be there - only the empty 'admin section' framework.
function isAdmin(){
return new Promise(function(resolve, reject){
var docRef = firebase.firestore().collection("authorize").doc("admin");
docRef.get().then(function(result) {
if (result) {
resolve (true);
}
}).catch(function(error) {
resolve (false);
});
});
}
The firestore rule specifies the 'admins' by UID.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid == "9mB3UxxxxxxxxxxxxxxxxxxCk1";
}
}
}
You're storing the role of each user in the database, and then looking it up in the client to update its UI. This used to be the idiomatic way for a long time on realtime database, and it still works on Firestore.
The only thing I'd change is to have the rules also read from /authorize/admin, instead of hard-coding the UID in them. That way you only have the UID in one place, instead of having it in both the rules and the document.
But you may also want to consider an alternative: set a custom claim on your admin user, that you can then read in both the server-side security rules (to enforce authorized access) and the front-end (to optimize the UI).
To set a custom claim you use the Firebase Admin SDK. You can do this on a custom server, in Cloud Functions, but in your scenario it may be simpler to just run it from your development machine.
Detailed How To: Firebase has what's called Custom Claims for this functionality as detailed in their Control Access with Custom Claims and Security Rules. Basically, you stand up a separate node server, install the Firebase AdminSDK:
npm install firebase-admin --save
Generate/Download a Private Key from the Service Accounts tab in the Firebase Console and put that on your node server. Then simply create a bare bones node app to assign Custom Claims against each UID (user) that you wish. Something like below worked for me:
var admin = require('firebase-admin');
var serviceAccount = require("./the-key-you-generated-and-downloaded.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://xxxxxxxxxxxxxxxxxxxx.firebaseio.com"
});
admin.auth().setCustomUserClaims("whatever-uid-you-want-to-assign-claim-to", {admin: true}).then(() => {
console.log("Custom Claim Added to UID. You can stop this app now.");
});
That's it. You can now verify if the custom claim is applied by logging out of your app (if you were previously logged in) and logging back in after you update your web app's .onAuthStateChanged method:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
firebase.auth().currentUser.getIdToken()
.then((idToken) => {
// Parse the ID token.
const payload = JSON.parse(window.atob(idToken.split('.')[1]));
// Confirm the user is an Admin.
if (!!payload['admin']) {
//showAdminUI();
console.log("we ARE an admin");
}
else {
console.log("we ARE NOT an admin");
}
})
.catch((error) => {
console.log(error);
});
}
else {
//USER IS NOT SIGNED IN
}
});

How to deny access for fetch operations

I'm trying to create an access control system at the document level in Meteor, and I seem to be missing how to prevent users from fetching documents.
I've read the documentation around collection.allow and collection.deny. Via these objects we can control "who" can update, remove and insert. The problem is that Meteor doesn't seem to supply similar functionality for fetch operations. What is proper way to deny unauthorized users from reading documents?
Extra requirement: this needs to happen server side so we don't leak documents to unauthorized users over the network.
There is no way to deny reads to collection data once it has arrived on the client. In theory, there's no way to actually enforce anything on the client because the user could modify the code. You can, however, enforce which documents get published.
A publish function can handle authorization rules of arbirtary complexity. Here's a simple example where we want to publish documents from the Messages collection only to users who are members of the given group:
Meteor.publish('messagesForGroup', function(groupId) {
check(groupId, String);
var group = Groups.findOne(groupId);
// make sure we have a valid group
if (!group)
throw new Meteor.Error(404, 'Group not found');
// make sure the user is a member
if (!_.contains(group.members, this.userId))
throw new Meteor.Error(403, 'You are not a member of the group');
return Messages.find({groupId: groupId});
});
1) Remove autopublish package from your app.
2) Create your own publish and deny access to unauthorized users
Meteor.publish("userData", function () {
if (this.userId) { // Check user authorized
return MyCollection.find(); // Share data
} else {
this.ready(); // Share nothing
}
});

Categories