Meteor authentication problem

Hello guys, recently I’ve been tasked with an authentication problem. The application relies on fingerprinting to authenticate users devices so later on, the user can reuse the same device to login later on without having to re-enter his username/password. The issue at hand is that any slight change to the user’s device alters its fingerprint which renders this method completely obsolete.

I came up with a solution that involves server-sessions using express-session. When a client connects to our app, a new session is created and when a successful login is detected (we check for a login cookie that carries the same value Meteor stores in local storage) we attach the user id to the session and also attach the session id to the user.


import session from 'express-session'
import MongoStore from 'connect-mongo'
const mongoUrl = process.env.MONGO_URL;

const Sessions = new Mongo.Collection('sessions');

// Setup the session middleware
WebApp.connectHandlers.use(session({
    secret: Meteor.settings.SESSION_SECRET, // the secret that's used to sign the cookie 
    resave: false,
    saveUninitialized: true,
    // cookie: { secure: true },
    store: MongoStore.create({ mongoUrl, stringify: false })
  }))

WebApp.connectHandlers.use(function attachUserToTheSession(req, res, next) {
    var userId = getUserId(req);

    if (userId && !req.session?.userId) {
        // Remove any stale sessions that carries the same user id
        // before attaching the user id to a new session
        Sessions.remove({"session.userId": userId});
        
        // Reference the user id on the session object 
        req.session.userId = userId;

        // Also reference the session id on the user object
        Meteor.users.update({_id: userId}, {$set: {sessionId: req.session.id}})
    }
    next()
  });
  
function getUserId(req) {
    const token = req.cookies?.meteor_login_token;

    if (!token) {
      return null;
    }
    
    var user = Meteor.users.findOne({
      "services.resume.loginTokens.hashedToken": Accounts._hashLoginToken(token),
    });
    if (!user) {
        return null;
    }
    return user._id;
}

Copied from https://github.com/kadirahq/fast-render/blob/master/lib/client/auth.js#L36-L41

import { Cookies } from 'meteor/ostrio:cookies';

// getting tokens for the first time
//  Meteor calls Meteor._localStorage.setItem() on the boot
//  But we can do it ourselves also with this
Meteor.startup(function() {
    resetToken();
  });
  
  // override Meteor._localStorage methods and resetToken accordingly
  var originalSetItem = Meteor._localStorage.setItem;
  Meteor._localStorage.setItem = function(key, value) {
    if(key == 'Meteor.loginToken') {
      Meteor.defer(resetToken);
    }
    originalSetItem.call(Meteor._localStorage, key, value);
  };
  
  var originalRemoveItem = Meteor._localStorage.removeItem;
  Meteor._localStorage.removeItem = function(key) {
    if(key == 'Meteor.loginToken') {
      Meteor.defer(resetToken);
    }
    originalRemoveItem.call(Meteor._localStorage, key);
  }
  
  function resetToken() {
    var loginToken = Meteor._localStorage.getItem('Meteor.loginToken');
    var loginTokenExpires = new Date(Meteor._localStorage.getItem('Meteor.loginTokenExpires'));
  
    if(loginToken) {
      setToken(loginToken, loginTokenExpires);
    } else {
      setToken(null, -1);
    }
  }
  
  const cookies = new Cookies();

  function setToken(loginToken, expires) {
    cookies.set('meteor_login_token', loginToken, {
      path: '/',
      expires: expires
    });
  }

In the login handler, we now have to ensure that the session attached to the user who’s trying to login does exist!

const exists = Sessions.findOne({
        'session.userId': user._id
    });

Is this a viable solution to the problem of authenticating devices? And if so, is it executed properly?

Fingerprint API on a phone (Cordova), as far as I remember, returns a true or false for fingerprint check. Meteor has a “keep logged in” period of time, let’s say 30 days.

I think there are 2 concepts here:

  1. fingerprinting to authenticate users - what you might be trying to do.
  2. fingerprinting of authenticated users - user is already authenticated you route him to a screen where a fingerprint check is required. When app gets in focus you ask for a fingerprint check and save the truevalue so that the use can route to pages and when app goes out of focus you reset the value so that the user is being asked to use the fingerprint again. Every 30 days, the use must use the actual password to re-authenticate.

For 1. Do you have multiple users on a device (laptop) and you need to identify the user by its fingerprint or is is just 1 user per device and that needs to be authenticated (which leads to 2.)

Thank you Paul for your help.

For 1. Do you have multiple users on a device (laptop) and you need to identify the user by its fingerprint or is is just 1 user per device and that needs to be authenticated (which leads to 2.)

I guess 1 user per device thought the same problem remains in the two scenarios which is that any slight change to the users device completely alters the fingerprint which makes it impractical. And that’s why using cookies to store some user specific info and retrieve it later on is the route we chose.

What do you mean by “any slight change to the users device”?

What does the cookie implementation do that is different from the Meteor login token?

The fingeprint is based on the useragent so any change to the user environment like a different browser setup would yield different results.

It doesn’t do anything special. It copies the Meteor login token into a cookie so it can be sent in web requests and thus be used to validate the user.

Can you help me understand the use case how the cookie validation will work?

Is there a use case wherein the cookie validation works but the login token in local storage will not?

A different browser is (from the security perspective) a different client. Is PWA (Safari for IOS and Chrome for Android) an option for you so the user enters the app consistently via the same browser?

Using finger print, you can register a secret string. You store this string on server for the current user.
Then everytime user login will finger print, you call a method with this secret string. The method should validate the secret and make user login, return a token. On the client, you call Meteor.loginWithToken method to log user in.

Can you help me understand the use case how the cookie validation will work?

We store the local storage Meteor login token into a cookie so it can be retrieved in subsequent requests as local storage can’t be accessed or sent along unlike cookies. Then in the API call we get the user associated with that cookie and attach his Id to the server stored session so at any given point later on. Even if he logs out we shall be able to determine who he’s based on the cookie stored in his device.

Is there a use case wherein the cookie validation works but the login token in local storage will not?

Definitely in REST API calls, that’s why fast-render and meteor-rest had to rely on cookie mechanism to authenticate users.

You’re right. Definitely using a different browser counts as a different device though what I intended to mention is that even if he made changes to his very same browser setup it’d yield different results that’s why I strived for something more persistent using cookies.

Using finger print, you can register a secret string. You store this string on server for the current user.
Then everytime user login will finger print, you call a method with this secret string. The method should validate the secret and make user login, return a token. On the client, you call Meteor.loginWithToken method to log user in.

Now, we’re getting somewhere. Using a server stored secret is what express-session does for me without the hassle of managing it. And when he logs in, I attach his id to the session

WebApp.connectHandlers.use(function attachUserToTheSession(req, res, next) {
    var userId = getUserId(req);

    if (userId && !req.session?.userId) {
        // Remove any stale sessions that carries the same user id
        // before attaching the user id to a new session
        Sessions.remove({"session.userId": userId});
        
        // Reference the user id on the session object 
        req.session.userId = userId;

        // Also reference the session id on the user object
        Meteor.users.update({_id: userId}, {$set: {sessionId: req.session.id}})
    }
    next()
  });

So in theory, the solution I came up with is correct, no?

Just to be clear guys, I’m trying to validate this solution according to the use case we’re currently facing which I explained earlier.

The application relies on fingerprinting to authenticate users devices so later on, the user can reuse the same device to login later on without having to re-enter his username/password. The issue at hand is that any slight change to the user’s device alters its fingerprint which renders this method completely obsolete.

This can help in your initial post to understand how you are using the app

I mean you’re trying to apply a complicated method/mechanism. I would try using existing/built-in Meteor features. Just need to create a Meteor method to validate secret and return the token. On the client side, you call the built in Meteor.loginWithToken methos.
It’s simple, easy to maintain.

import { Accounts } from 'meteor/accounts-base'

Meteor.methods({
// login with secret string
  async 'loginWithSecretString'({ secret}: { secret: string; }) {
    // you validate the secret string here

    // create resume token
    const token = Accounts._generateStampedLoginToken()
    debug('the token', token)

    // hash the token
    const hashedToken = Accounts._hashStampedToken(token)
    debug('hash stamped token', hashedToken)

    // insert hashed token to user.services.resume
    try {
      Accounts._insertHashedLoginToken(userId, hashedToken)
    } catch (error) {
      return { status: 'failed', message: `Inserting token error: ${error.message}` }
    }

    // finally return the token
    return token
  }

on the client side, you call the method

const result = await Meteor.callAsync('loginWithSecretString', { secret: 'SOME STRING' });
// check for error
// then call
Meteor.loginWithToken(result.token)

1 Like

I hope this isn’t something where you’re expecting a high degree of security. Typically, authentication is done using something you know (e.g. password) and something you have (e.g. a hardware key or biometric). Unless I’ve misunderstood, what you’ve proposed uses only “what you have” for authentication, so anybody with access to the device/browser would have access to the app.

Perhaps you should have a look at the WebAuthn API, which isn’t that difficult to implement and integrates just fine with Meteor.