onDisconnect() delete permissions question

679 views
Skip to first unread message

Daniel Steigerwald

unread,
Jul 13, 2016, 3:35:34 PM7/13/16
to Firebase Google Group
Slightly modified firebase.google.com/docs/database/web/offline-capabilities#section-sample

const createPresenceMonitor = () => {
  let off = null;

  return (firebase, firebaseDatabase, user) => {
    const connectedRef = firebase.child('.info/connected');
    const presenceRef = firebase.child(`users-presence/${user.id}`);
    if (off) off();
    const handler = snap => {
      if (!snap.val()) return;
      const { email, ...userWithoutEmail } = user.toJS();
      presenceRef
        .push({
          authenticatedAt: firebaseDatabase.ServerValue.TIMESTAMP,
          user: userWithoutEmail
        })
        .onDisconnect().remove(error => {
          // Permissions error here.
          alert(error);
        });
    };
    off = () => connectedRef.off('value', handler);
    connectedRef.on('value', handler);
  };
};

Rules
path /users-presence/{uid} is UserPresence[] {
  write() { isViewer(uid) }
}

When some user is unauthenticated .onDisconnect().remove does not work since there is no auth of course.
So the app has to probably remove connectionRef manually.

Not sure whether this is 100 % correct approach. What if user does not invoke unauth, and just lost it? Will be presence state stored in Firebase forever?

Daniel Steigerwald

unread,
Jul 14, 2016, 9:34:44 PM7/14/16
to Firebase Google Group
Can anyone answer please?

Frank van Puffelen

unread,
Jul 14, 2016, 9:41:22 PM7/14/16
to Firebase Google Group
Daniel,

An unauthenticated user can still be connected to the database. So onDisconnect() will not fire in that case. 

You can detect that the user becomes unauthenticated (but is still connected) in your code using an onAuthStateChanged() and remove the node from there.

     puf

Jacob Wenger

unread,
Jul 15, 2016, 4:36:01 AM7/15/16
to fireba...@googlegroups.com
To clarify what Puf said:
  • onDisconnect() is only called when the connection is disconnected and is not tied to auth state changes. So, signing a user out will not fire onDisconnect().
  • This means that if you want to mark an authenticated user as no longer connected upon signing out, you should do that yourself right before you call signOut().
  • If the connection is disconnected, the methods will respect the currently signed-in user and your rules should be respected.
Hope that clears things up a bit.

Jacob

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-tal...@googlegroups.com.
To post to this group, send email to fireba...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/firebase-talk/d0080e83-7b2e-48c1-b7f3-89b729209e69%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Daniel Steigerwald

unread,
Jul 15, 2016, 12:55:00 PM7/15/16
to fireba...@googlegroups.com
Blame my bad english but that wasn't What I was asking for. I will try reformulate it.

How to setup security rules to ensure only authenticated user has access to its connection and the connection is deleted when the user is unauthenticated by something else, not via signOut?
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/tQcYTUXkRnQ/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-tal...@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

Joe White

unread,
Jul 15, 2016, 2:01:04 PM7/15/16
to Firebase Google Group
Daniel -- as noted earlier in this chain, you should register an "authorization listener" and listen for a state change for the authorization of the user.  (See documentation at https://firebase.google.com/docs/reference/android/com/google/firebase/auth/FirebaseAuth.AuthStateListener).  

That listener will fire when the connected user becomes "unauthorized" and you can then do whatever actions you wish. 

Daniel Steigerwald

unread,
Jul 15, 2016, 4:52:10 PM7/15/16
to fireba...@googlegroups.com

On Fri, Jul 15, 2016 at 9:15 PM, Daniel Steigerwald <dan...@steigerwald.cz> wrote:
"and you can then do whatever actions you wish"

No I can't if delete rule is set to auth in security rules. That's why I asking, it failed. But when I allow anyone to write, it works, but it's obviously bad allow anyone to write.

Does it make sense already?






On Friday, 15 July 2016, Joe White <jos...@velocikey.com> wrote:
Daniel -- as noted earlier in this chain, you should register an "authorization listener" and listen for a state change for the authorization of the user.  (See documentation at https://firebase.google.com/docs/reference/android/com/google/firebase/auth/FirebaseAuth.AuthStateListener).  

That listener will fire when the connected user becomes "unauthorized" and you can then do whatever actions you wish. 

This message (and any associated files) may contain VelociKey confidential and/or privileged information. If you are not the intended recipient or authorized to receive this for the intended recipient, you must not use, copy, disclose or take any action based on this message or any information herein. If you have received this message in error, please advise the sender immediately by sending a reply e-mail and delete this message. Thank you for your cooperation.

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/tQcYTUXkRnQ/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-tal...@googlegroups.com.
To post to this group, send email to fireba...@googlegroups.com.

Daniel Steigerwald

unread,
Jul 15, 2016, 4:52:10 PM7/15/16
to fireba...@googlegroups.com
"and you can then do whatever actions you wish"

No I can't if delete rule is set to auth in security rules. That's why I asking, it failed. But when I allow anyone to write, it works, but it's obviously bad allow anyone to write.

Does it make sense already?






On Friday, 15 July 2016, Joe White <jos...@velocikey.com> wrote:
Daniel -- as noted earlier in this chain, you should register an "authorization listener" and listen for a state change for the authorization of the user.  (See documentation at https://firebase.google.com/docs/reference/android/com/google/firebase/auth/FirebaseAuth.AuthStateListener).  

That listener will fire when the connected user becomes "unauthorized" and you can then do whatever actions you wish. 

This message (and any associated files) may contain VelociKey confidential and/or privileged information. If you are not the intended recipient or authorized to receive this for the intended recipient, you must not use, copy, disclose or take any action based on this message or any information herein. If you have received this message in error, please advise the sender immediately by sending a reply e-mail and delete this message. Thank you for your cooperation.

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/tQcYTUXkRnQ/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-tal...@googlegroups.com.
To post to this group, send email to fireba...@googlegroups.com.

Jacob Wenger

unread,
Jul 18, 2016, 1:24:52 AM7/18/16
to fireba...@googlegroups.com
Hey Daniel, I think I understand what you are getting at now. You want to clean up the /users-presence/{uid} node when an action like change password or change email is called and we auto-expire the auth token. Is that right?

You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-tal...@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

Daniel Steigerwald

unread,
Jul 18, 2016, 11:05:47 AM7/18/16
to fireba...@googlegroups.com

Daniel Steigerwald

unread,
Jul 20, 2016, 6:56:47 PM7/20/16
to Firebase Google Group
I still don't know how to protect user session in  /users-presence/{uid} against deletion from anyone, or delete it automatically for just unauthed user.
Correct me if I'm wrong, but I think it's impossible now. 


On Monday, July 18, 2016 at 5:05:47 PM UTC+2, Daniel Steigerwald wrote:
Yes. 

On Monday, 18 July 2016, Jacob Wenger <ja...@firebase.com> wrote:
Hey Daniel, I think I understand what you are getting at now. You want to clean up the /users-presence/{uid} node when an action like change password or change email is called and we auto-expire the auth token. Is that right?
On Sat, Jul 16, 2016 at 2:21 AM, Daniel Steigerwald <dan...@steigerwald.cz> wrote:
On Fri, Jul 15, 2016 at 9:15 PM, Daniel Steigerwald <dan...@steigerwald.cz> wrote:
"and you can then do whatever actions you wish"

No I can't if delete rule is set to auth in security rules. That's why I asking, it failed. But when I allow anyone to write, it works, but it's obviously bad allow anyone to write.

Does it make sense already?



On Friday, 15 July 2016, Joe White <jos...@velocikey.com> wrote:
Daniel -- as noted earlier in this chain, you should register an "authorization listener" and listen for a state change for the authorization of the user.  (See documentation at https://firebase.google.com/docs/reference/android/com/google/firebase/auth/FirebaseAuth.AuthStateListener).  

That listener will fire when the connected user becomes "unauthorized" and you can then do whatever actions you wish. 

This message (and any associated files) may contain VelociKey confidential and/or privileged information. If you are not the intended recipient or authorized to receive this for the intended recipient, you must not use, copy, disclose or take any action based on this message or any information herein. If you have received this message in error, please advise the sender immediately by sending a reply e-mail and delete this message. Thank you for your cooperation.

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/tQcYTUXkRnQ/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-talk+unsubscribe@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/firebase-talk/8f49ddf2-0e5a-4158-bd80-5055091f56bc%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-talk+unsubscribe@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/tQcYTUXkRnQ/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-talk+unsubscribe@googlegroups.com.

Kato Richardson

unread,
Aug 5, 2016, 1:28:16 PM8/5/16
to Firebase Google Group
Hi Daniel,

It doesn't seem like onDisconnect(), by itself, is going to be able to meet your needs here. One alternative would be to make the records "expire" after some time. There are a few ways this could work. For example, if we stored a timestamp in the presence data, and had the client update it every 15s (but only if they are authenticated), then we could do one of the following:

1) Set the security rules to allow any client to delete the record, if the timestamp has "expired." Thus, it's only accessible by the authenticated user for read/write, but can be deleted by anyone.

2) Run a server script to query for stale/expired entries and remove them.

This might seem like it's an effort, but we're really just handling the edge case here (i.e. the user logged out abnormally because their token expired, and then loss connectivity before re-authenticating). If the logout is normal (i.e. the user presses a logout button or reauthenticates before disconnecting) then the current process you have in place would already handle this.

I know it's not exactly what you asked for, but I hope that helps!

☼, Kato


For more options, visit https://groups.google.com/d/optout.



--

Kato Richardson | Developer Programs Eng | kato...@google.com | 775-235-8398

Alan deLespinasse

unread,
Dec 5, 2016, 1:04:21 AM12/5/16
to Firebase Google Group
I'm going to follow up on this older thread instead of the newer one because I think this one has more of the relevant context. They're pretty much the same question.

As far as I can tell, any onDisconnect trigger that writes to the database in a way that requires authentication will fail if the user has been logged in for more than an hour. This seems to be because the trigger (which lives on the server) keeps the (short-lived) auth token in use at the time, and checks it again when a disconnect happens. If it's expired, the write doesn't happen.

Here's some JS code I used to test this:

var initialized = false;


firebase
.auth().onAuthStateChanged(function(user) {
 
if (!user) {
    console
.log('User is signed out');
   
return;
 
}
  console
.log('User is signed in: ' + JSON.stringify(user, undefined, 2));
  console
.log('Token expires at ' + new Date(JSON.parse(JSON.stringify(user)).stsTokenManager.expirationTime));
 
// Only initialize once, the first time the user is signed in.
 
if (initialized)
   
return;
  console
.log('Initializing user presence system');
  initialized
= true;

 
// The rest of this is copied directly from
 
// https://firebase.google.com/docs/database/web/offline-capabilities#section-sample
 
// (except the console.logging)
 
 
// since I can connect from multiple devices or browser tabs, we store each connection instance separately
 
// any time that connectionsRef's value is null (i.e. has no children) I am offline
 
var myConnectionsRef = firebase.database().ref('users/joe/connections');

 
// stores the timestamp of my last disconnect (the last time I was seen online)
 
var lastOnlineRef = firebase.database().ref('users/joe/lastOnline');

 
var connectedRef = firebase.database().ref('.info/connected');
  connectedRef
.on('value', function(snap) {
    console
.log('.info/connected became ' + snap.val());
   
if (snap.val() === true) {
     
// We're connected (or reconnected)! Do anything here that should happen only if online (or on reconnect)

     
// add this device to my connections list
     
// this value could contain info about the device or a timestamp too
     
var con = myConnectionsRef.push(true);

     
// when I disconnect, remove this device
      con
.onDisconnect().remove();

     
// when I disconnect, update the last time I was seen online
      lastOnlineRef
.onDisconnect().set(firebase.database.ServerValue.TIMESTAMP);
   
}
 
});
}, function(error) {
  console
.log(error);
});


It's mostly copied straight from the sample code in the docs, except that it waits until a user is signed in to set up the triggers, and it does some logging. All I do with this test is log in once and let the tab sit there idle, and eventually close the tab. (Let me know if you'd like the index.html and login.html that I'm using too. They just do the usual, obvious things.)

If the database allows unauthenticated writes, it works fine (you end up with multiple "connections" logged in the database because of something I'll note below, but they all get removed at disconnect, so it's fine).

If the database requires authentication for writes (auth !== null), the onDisconnect does not work after the token has expired and auto-refreshed. With the above code, a new value gets stored on each token refresh (see below), but then old ones never go away.

I don't really understand why an expired token would have this effect, and I'm wondering if it's even intentional. But maybe there's some attack vector that would open up if onDisconnect could last forever. (I'm not talking about if the database rules change; I'd want that to be able to prevent a write. I just think that if a user was authorized when the onDisconnect event was created, that authorization should basically last indefinitely on the server.)

Or maybe I'm making a mistake and it doesn't actually work that way, but the code I posted above sure makes it seem that way to me.

Interestingly, every time the token refreshes (generally once an hour), the following things happen:
  1. Special reference ".info/connected" becomes false, triggering a "value" event on it (but any events set up through onDisconnect do NOT get executed if they require authentication).
  2. An onAuthStateChanged event is fired, with the same user info as before, except of course with a new token. (There is NOT an onAuthStateChanged event with user=null before this.)
  3. ".info/connected" becomes true again, triggering another event on the callback for that.
That's why I'm getting multiple values in the list of connections, as mentioned above, even though I only open one tab with one connection. I'm not sure why ".info/connected" toggles briefly to false, but it does. If this triggered the onDisconnect events that were previously set up, I think the example would work correctly. I think it looks like they do, but only if the writes don't require authentication.

I'm planning to try to work around this by storing the token's expiration time for each connection, instead of just "true". Then I can tell whether a user is online by whether there are any stored expiration times that are still in the future. This is vulnerable to clock skew, but that's not a big deal for my use case.

(And then there's the question of how to find the token's expiration time. If I look at JSON.stringify(user), it's right there in user.stsTokenManager.expirationTime, but I can't access that from the code. For now I'm actually converting it to JSON and back. I'm afraid the only officially supported way to do this client-side might be to call user.getToken() then decode the token using a third-party JWT library, which seems like it shouldn't be necessary.)

Am I missing something?
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-tal...@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

Alan deLespinasse

unread,
Dec 5, 2016, 10:24:02 AM12/5/16
to Firebase Google Group
Oh, I forgot to mention the possibility of just removing the record of the previous connection whenever the token refreshes... which I think could work for the normal case of tokens refreshing, although I was having some trouble with it. I can't even remember all the things I've tried or all the reasons I discovered why they wouldn't work.

But anyway, I think the "store the token expiration time" method that I described above is the way to go. It lets you have a pretty reliable indicator of whether a user is online without having to run any background processes to handle edge cases.

Also, I'd like to suggest that the sample in the documentation be updated. At the very least, it should mention that it doesn't work if the database writes require authentication.
Reply all
Reply to author
Forward
0 new messages