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?