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:
- 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).
- 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.)
- ".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?