I have implemented the Firebase Real-Time Database presence system as shown in the official Firebase documentation. I would like to make the database secure so that logged-in users can only write to their own presence entries in the DB. So, on login, the user writes to the reference path /auth/{authId}/connections
and at the same time sets up the onDisconnect
to remove the value.
Here is the code from the Android app that is setting presence in rtdb:
getFirebaseDatabase().goOnline();
DatabaseReference.goOnline();
// Since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
final FirebaseDatabase database = getFirebaseDatabase();
final DatabaseReference myConnectionsRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/connections");
// Stores the timestamp of my last disconnect (the last time I was seen online)
final DatabaseReference lastOnlineRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/lastOnline");
connectedRef = database.getReference(".info/connected");
presenceChangeListener = connectedRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
boolean connected = snapshot.getValue(Boolean.class);
if (connected) {
DatabaseReference con = myConnectionsRef.push();
// When this device disconnects, remove it
con.onDisconnect().removeValue()
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
// Add this device to my connections list
// this value could contain info about the device or a timestamp too
con.setValue("ANDROID");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.d(TAG, "### Failed to set onDisconnect ###");
e.printStackTrace();
}
});
// When I disconnect, update the last time I was seen online
lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);
}
}
@Override
public void onCancelled(DatabaseError error) {
Log.w(TAG, "Listener was cancelled at .info/connected");
}
});
The problem that I am having is that if the user logs out, the onDisconnect
doesn't execute unless I first manually disconnect from rtdb. I'm assuming that the code running on the Real-Time DB gets a permission denied since the auth is no longer valid.
//If I don't go offline first the record in rtdb will not be removed.
DatabaseReference.goOffline();
AuthUI.getInstance().signOut(this)
.addOnCompleteListener(new OnCompleteListener<Void>() {
public void onComplete(@NonNull Task<Void> task) {
// user is now signed out
Log.d(TAG, "Logged out");
application.clearData();
DatabaseReference.goOffline(); //This doesn't cause a presence update here
finish();
}
});
Above is the work-around I'm using, first telling the database to goOffline
then to logout. If the user ever gets logged out by another means (the web app is seeing if multiple tabs are using the app and one logs out) the user will be left with a connection not removed.
If I don't call the goOffline()
prior to logout, the connection in rtdb will not be removed, even if I force close the application.
I have also verified that I can get everything working fine if I change my rtdb rules to be ".write": "true"
<-which is no good. This tells me that there is a permission denied with the onDisconnect
running when a user logs out of the auth.
I would like my real-time rules to be something like this.
{
"rules": {
"auth": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": "auth != null && auth.uid == $uid"
}
}
}
}
I would have hoped that the onDisconnect
would still be able to execute with the auth of the user when the onDisconnect
was setup.
When you attach a onDisconnect()
handler, you're registering a delayed write on the Firebase servers. Whether that write is allowed is checked both when you attach the handler, and when the handler is triggered. And since your user is signed out when the write is triggered, it get rejected by your rules. There is no configuration option to change this behavior, so you'll have to come up with a different approach.
So, because 1.) the onDisconnect()
execution is evaluated against the rules of the RTDB, 2.) the user who setup the onDisconnect()
may lose authentication, and 3.) I would like to make the presence system secure for my auth'ed users... I came up with the following solution:
First, write the presence entries to the RTDB under a path that contains both the user's authId and a UUID to make the location "unguessable".
"/presence/" + {auth-uid} + "/connections/" + {UUID}
and setup a .onDisconnect()
to remove this value stored at the unguessable location.
Then, setup the RTDB rules to do the following:
"presence": {
".read": "false",
".write": "false",
"$auth_id": {
"connections": {
"$uuid": {
".write": "(newData.exists() && $auth_id === auth.uid) || !newData.exists()"
}
}
}
}
Finally, setup a trigger function on the RTDB to read the .ref('/presence/{authid}')
location and push the user's presence to another user accessible location (I'm pushing it to my Firestore DB). Also, if the user is changing from "online" to "offline" update a lastOnline timestamp to the current time.
This seems like the best solution given my requirements of having reliable and secure presence system. I hope this helps others.
It is an old question but it made me think about a possible solution and came with the following...
use onDisconnect.setValue/removeValue as long as you have control/awareness over the application
use onDisconnect.cancel and delete to data before logging out
I took @FrankvanPuffelen code and modified it but didn't tested it so....
//getFirebaseDatabase().goOnline();
//DatabaseReference.goOnline();
// Since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
final FirebaseDatabase database = getFirebaseDatabase();
final DatabaseReference myConnectionsRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/connections");
// Stores the timestamp of my last disconnect (the last time I was seen online)
final DatabaseReference lastOnlineRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/lastOnline");
connectedRef = database.getReference(".info/connected");
presenceChangeListener = connectedRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
boolean connected = snapshot.getValue(Boolean.class);
if (connected) {
// simple solution to reuse the old unique key-name otherwise current solution is like performing new registration of a new client over and over on the same client. we should use the old unique key-name until logout is performed
String keyName = SharedPrefUtil.INSTANCE.getFirebaseConnectionKeyName(context);
DatabaseReference con;
if (TextUtils.isEmpty(keyName)) {
con = myConnectionsRef.push();
SharedPrefUtil.INSTANCE.setFirebaseConnectionKeyName(context.getApplicationContext(), con.getKey());
}else{
con = myConnectionsRef.child(keyName);
}
// When this device disconnects, remove it
con.onDisconnect().removeValue()
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
// Add this device to my connections list
// this value could contain info about the device or a timestamp too
con.setValue("ANDROID");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.d(TAG, "### Failed to set onDisconnect ###");
e.printStackTrace();
}
});
// When I disconnect, update the last time I was seen online
lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);
}
}
@Override
public void onCancelled(DatabaseError error) {
Log.w(TAG, "Listener was cancelled at .info/connected");
}
});
in the logout method we need to cancel the disconnect
String keyName = SharedPrefUtil.INSTANCE.getFirebaseConnectionKeyName(context);
if (!TextUtils.isEmpty(keyName)) {
final FirebaseDatabase database = getFirebaseDatabase();
final DatabaseReference myConnectionsRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/connections/" + keyName);
// Stores the timestamp of my last disconnect (the last time I was seen online)
final DatabaseReference lastOnlineRef = database.getReference("/auth/" + getFirebaseAuth().getUid() + "/lastOnline");
// This client/user doesn't need the disconnect functionality
myConnectionsRef.onDisconnect().cancel();
// now we are on our own so we need to remove the key-name from the rmdb
myConnectionsRef.setValue(null);
// remove the key-name from the preferences so we will create a new one in the next login session
SharedPrefUtil.INSTANCE.removeFirebaseConnectionKeyName(context);
// we will not forget to disconnect last time updates
lastOnlineRef.onDisconnect().cancel()
}
AuthUI.getInstance().signOut(this)
I didn't tested it and it will not run as it is missing the SharedPrefUtil implementation
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.