Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I'd like for the new session to essentially "log out" of any previous session. 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.

I haven't been able to find any method that allows me to log out of a session "remotely". I know that I can unauth() and goOffline() from within a session. But how do I do it from a different authenticated session of the same user?

Thanks for the help!

Background Info:

  1. I am using simple email/password login for firebase authentication
  2. I don't have security rules setup yet, although this is in the works
  3. I'm using Javascript with Firebase
like image 383
jdchizzle Avatar asked Jan 23 '14 23:01

jdchizzle


2 Answers

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).

like image 175
Kato Avatar answered Oct 22 '22 06:10

Kato


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.

like image 11
zsgomori Avatar answered Oct 22 '22 05:10

zsgomori