How to prevent simultaneous logins of the same user with Firebase?

The general idea is that you want to create some meta data in Firebase which tells you how many locations a user is logged in from. Then you can restrict their access using this information.

To do this, you'll need to generate your own tokens (so that the information is available to your security rules).

1) Generate a token

Use custom login to generate your own tokens. Each token should contain a unique ID for the client (IP Address? UUID?)

var FirebaseTokenGenerator = require("firebase-token-generator");
var tokenGenerator = new FirebaseTokenGenerator(YOUR_FIREBASE_SECRET);
var token = tokenGenerator.createToken({ id: USER_ID, location_id: IP_ADDRESS });

2) Use presence to store the user's location_id

Check out the managing presence primer for details:

var fb = new Firebase(URL);

// after getting auth token back from your server
var parts = deconstructJWT(token);
var ref = fb.child('logged_in_users/'+token.id);

// store the user's location id
ref.set(token.location_id);

// remove location id when user logs out
ref.onDisconnect().remove();

// Helper function to extract claims from a JWT. Does *not* verify the
// validity of the token.
// credits: https://github.com/firebase/angularFire/blob/e8c1d33f34ee5461c0bcd01fc316bcf0649deec6/angularfire.js
function deconstructJWT(token) {
  var segments = token.split(".");
  if (!segments instanceof Array || segments.length !== 3) {
    throw new Error("Invalid JWT");
  }
  var claims = segments[1];
  if (window.atob) {
    return JSON.parse(decodeURIComponent(escape(window.atob(claims))));
  }
  return token;
}

3) Add security rules

In security rules, enforce that only the current unique location may read data

{
  "some_restricted_path": {
     ".read": "root.child('logged_in_users/'+auth.id).val() === auth.location_id"
  }
}

4) Control write access to logged_in_users

You'll want to set up some system of controlling write access to logged_in_users. Obviously a user should only be able to write to their own record. If you want the first login attempt to always win, then prevent write if a value exists (until they log out) by using ".write": "!data.exists()"

However, you can greatly simplify by allowing the last login to win, in which case it overwrites the old location value and the previous logins will be invalidated and fail to read.

5) This is not a solution to control the number of concurrents

You can't use this to prevent multiple concurrents to your Firebase. See goOffline() and goOnline() for more data on accomplishing this (or get a paid plan so you have no hard cap on connections).


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.


So I just ran across this issue and believe I have solved it in a much easier way than anything mentioned here so I thought I'd let you know what I ended up doing. Firstly I am using a "realtime database" so this example uses that. I have a "users" table with an entry for each user. The entries, among other things, include a "lastLogin" timestamp. When a user logs in I update the fields in that table and put in a lastLogin timestamp. Yes I know there is a login timestamp in the user metaData, but you will see why I did this in a moment.

Once a user logs in I then simply setup a "watch" on that table. E.G.

   var firstTime = true;
   var ref = firebase.database.ref(PATH_TO_TABLE_OR_FIELD);
    ref.on('value',(snapshot)=> {
      if(!firstTime) {
      // CHECK THE SNAPSHOT FOR THE LOGIN FIELD. SINCE IT HAS CHANGED THAT MEANS ANOTHER USER LOGGED IN WITH SAME USERNAME 
      } else {
        firstTime = false;
      }
    });

The "firstTime" value is because the first time you setup an "on" it gets called immediately. So we don't want to count that as the value being changed. After that, this will be called whenever a value in the indicated table changes.