TL;DR
https://pastebin.com/jWYu53Up
We've come across this topic as well. Despite the thread's obsolescence and the fact that it doesn't entirely outline our exact same desire we wanted to achieve, yet we could soak some of the general concepts of @kato's answer up. The conceptions have roughly remained the same but this thread definitely deserves a more up-to-date answer.
Heads-up: before you read this explanation right off the bat, be aware of the fact you'll likely find it a bit out of context because it doesn't entirely cover the original SO question. In fact, it's rather a different mental model to assemble a system to prevent multiple sessions at the same time. To be more precise, it's our mental model that fits our scenario. :)
For example, when you are in an authenticated session in one computer, starting a new session on another computer and authenticating with firebase on our app will log out the other session on the first computer.
Maintaining this type of "simultaneous-login-prevention" implies 1) the active sessions of each client should be differentiated even if it's from the same device 2) the client should be signed out from a particular device which AFAICT Firebase isn't capable of. FWIW you can revoke tokens to explicitly make ALL of the refresh tokens of the specified user expired and, therefore, it's prompted to sign in again but the downside of doing so is that it ruins ALL of the existing sessions(even the one that's just been activated).
These "overheads" led to approaching the problem in a slightly different manner. It differs in that 1) there's no need to keep track of concrete devices 2) the client is signed out programmatically without unnecessarily destroying any of its active sessions to enhance the user experience.
Leverage Firebase Presence to pass the heavy-lifting of keeping track of connection status changes of clients(even if the connection is terminated for some weird reason) but here's the catch: it does not natively come with Firestore. Refer to Connecting to Cloud Firestore to keep the databases in sync. It's also worthwhile to note we don't set a reference to the special .info/connected path compared to their examples. Instead, we take advantage of the onAuthStateChanged() observer to act in response to the authentication status changes.
const getUserRef = userId => firebase.database().ref(`/users/${userId}`);
firebase.auth().onAuthStateChanged(user => {
if (user) {
const userRef = getUserRef(user.uid);
return userRef
.onDisconnect()
.set({
is_online: false,
last_seen: firebase.database.ServerValue.TIMESTAMP
})
.then(() =>
// This sets the flag to true once `onDisconnect()` has been attached to the user's ref.
userRef.set({
is_online: true,
last_seen: firebase.database.ServerValue.TIMESTAMP
});
);
}
});
After onDisconnect() has been correctly set, you'll have to ensure the user's session if it tries kicking off a sign-in alongside another active session, for which, forward a request to the database and check against the corresponding flag. Consequently, recognizing multiple sessions takes up a bit more time than usual due to this additional round-trip, hence the UI should be adjusted accordingly.
const ensureUserSession = userId => {
const userRef = getUserRef(userId);
return userRef.once("value").then(snapshot => {
if (!snapshot.exists()) {
// If the user entry does not exist, create it and return with the promise.
return userRef.set({
last_seen: firebase.database.ServerValue.TIMESTAMP
});
}
const user = snapshot.data();
if (user.is_online) {
// If the user is already signed in, throw a custom error to differentiate from other potential errors.
throw new SessionAlreadyExists(...);
}
// Otherwise, return with a resolved promise to permit the sign-in.
return Promise.resolve();
});
};
Combining these two snippets together results in https://pastebin.com/jWYu53Up.