简体   繁体   中英

Real-time database onDisconnect not executing after logging out

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:

  • do not allow any reading of the presence data
  • allow users to add/modify data only under their auth directory
  • allow any user to delete records (they would need to know the unguessable path)
    "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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM