everything should be tied to the current user authorization on the server side, to ensure security. Using subdomains is nice besides adding a customized user experience, it also allows you to send basic/branding type information to unauthenticated users, especially for customer portals, customized login pages. It can also be used to help set context if a user needs to login to a specific tenant/company.
We also use the subdomain as an additional security layer, the subdomain/currently authenticated user and other other items must match up before we allow a successful auth. This helps add another layer to ensure tenants/users don’t mix.
After a user connects on the server, a unique session id is created server side. You can also write to that Meteor session/connection object. So after it authenticates once and does several expensive lookups to the database, we save some helpful information to that connection object (only on server). We have a small method that can reference that info when needed on the client.
// Here is an example of the connection object, after we add the _app key/object
id: 'zPuT8L8ydK7Jj5PKn',
close: [Function: close],
onClose: [Function: onClose],
clientAddress: '127.0.0.1',
httpHeaders: {
'x-forwarded-for': '127.0.0.1',
'x-forwarded-proto': 'ws',
host: 'localhost:3050',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36',
'accept-language': 'en-US,en;q=0.8,da;q=0.6'
},
clientIp: '127.0.0.1',
_app: {
_tenantId: 'wZ24xNQydL48t9hHm',
_userId: '6tSa2jvnsSLfNEHRo',
_connectionId: 'zPuT8L8ydK7Jj5PKn'
}
}
//
function getTenantIdFromUserId (userId) {
if (typeof userId === 'string') {
const userDoc = Meteor.users.findOne({_id: userId});
if (userDoc && userDoc._id === userId && typeof userDoc.tenant === 'string') {
return userDoc.tenant;
}
}
};
function makeAppTenantUserObj (userId, tenantId, connectionId) {
if (typeof userId === 'string' && typeof tenantId === 'string' && typeof connectionId === 'string') {
return {
_tenantId: tenantId,
_userId: userId,
_connectionId: connectionId
};
} else {
return null;
}
};
function verifyAppTenantObj (appObj = {}, connectionId) {
const result = typeof connectionId === 'string' && Match.test(appObj, {
_userId: String,
_tenantId: String,
_connectionId: String
});
return result && appObj._connectionId === connectionId;
};
Accounts.onLogin(function(user) {
const appObj = makeAppTenantUserObj(user.user._id, user.user.tenant || getTenantIdFromUserId(user.user._id), user.connection.id);
if (appObj && verifyAppTenantObj(appObj), user.connection.id) {
user.connection._app = appObj;
console.log('appObj valid, setting user tenant');
} else {
user.connection._app = null;
console.log('err - appObj not valid, setting user tenant to null');
}
});
Meteor.onConnection(function(res) {
// console.log('res? in onConnection', res);
try {
const base = new AuditBase('connection', res);
base.insertEvent();
} catch(e) {
rlog(e, 'err in Meteor.onConnection Audit event');
}
});
Meteor.methods({
getUserTenant: function() {
check(this.userId, String);
check(this.connection.id, String);
this.unblock();
if (verifyAppTenantObj(this.connection._app, this.connection.id)) {
return this.connection._app;
} else {
throw new Meteor.Error('err - invalid tenant for user:', this.userId);
}
}
});