Sharing login across subdomains in 2020

Hey everyone.

Is anyone aware of a way to do this? I found a package last updated in 2015 for Meteor 1.3, which appeared not to work at all.

Anyone know how to do this? If a user logs in at www.myapp.com, I’d like to share that login seamlessly with x.myapp.com.

Thanks a lot.

I’m not sure it’s possible for this to be totally seamless, Meteor stores access credentials in localStorage, which is implemented on a per-domain scope (e.g., every subdomain has it’s own database).

There are a few options for you, depending on your exact usecase and how much effort you want:

  1. Swap to cookies for your access credentials - will require that you implement your own auto-login, session expiry, etc.
  2. add an iframe with access to a “root” domain, that handles login itself, the global window can post messages to this iframe and receive back the contents of local storage, giving you the access credentials.
  3. If access to x.myapp.com is always initiated by www.myapp.com (e.g., clicking a link in www opens x) you can append a valid resume token to the URL and use that to login from x.myapp.com
1 Like

SSO using oauth2 wherein the main domain is the identity provider

2 Likes

Thanks guys. RJ, would you mind elaborating a bit please?

That is exactly what facebook login is. Facebook is the identity provider and your app is the consumer. This process uses oauth2

In your case, main domain is the identify provider while the subdomain is the consumer

1 Like

Thanks, I’ve got that much, but would you mind elaborating on a few high-level implementation details? Thank you so much.

Better search about how oauth2 is implemented both as a provider and a consumer. There are a lot of articles online that can explain this better than I could

2 Likes

For #2, are there any known security clause with this approach?

For your third option, If someone copies and shares a URL with a resume token, would a third-party be able to login?

For #3, it depends how you choose to implement it - the safest would be a one time password, where once it’s used once it wont work again.

For #2, none that I can think of - but as with all things, it depends how you implement it

One way I have battled this without using a full oauth2 setup is using the https://github.com/DispatchMe/meteor-login-token package. I have a few different subdomains that share a database, so I use the meteor-login-token package to generate a login token and then window.location redirect to the subdomain with the token as a CGI parameter. The subdomain looks at the cgi token, checks it with the database, and then logs you in using the token.

Create a new method:

import { Meteor } from 'meteor/meteor';
import { LoginLinks } from 'meteor/loren:login-links';

Meteor.methods({
  generateLoginRedirectToken: function () {
    if (!Meteor.userId()) {
      throw new Meteor.Error('not-authorized');
    }
    return LoginLinks.generateAccessToken(Meteor.user(), {
      expirationInSeconds: 5
    });
  }
});

Setup a login route:


Router.route('/login', {
  name: 'app_login',
  template: 'app_login',
  controller: app_Controller,
  layoutTemplate: 'app_UI_login',
  onBeforeAction: function () {
    preLoader();
    if (Router.current() && Router.current().params && Router.current().params.query && Router.current().params.query.redirect) {
      localStorage.setItem('redirect', Router.current().params.query.redirect);
    }
    this.next();
  }
});

Call it on the client onBeforeAction on your route:

function preLoader() {
  if (Meteor.user()) {
    const redirectTo = localStorage.getItem('redirect');
    if (redirectTo) {
      Meteor.call('generateLoginRedirectToken', function (err, redirectLoginToken) {
        if (err) {
          console.error(err);
          return;
        }
        localStorage.removeItem('redirect');
        window.location = `${redirectTo}?token=${redirectLoginToken}`;
      });
    }
  }
  if (this.next) {
    this.next();
  }
}

Then do the actual login on your subdomain

Template.app_login.onRendered(function () {
  if (Router.current() && Router.current().params && Router.current().params.query && Router.current().params.query.token) {
    LoginLinks.loginWithToken(Router.current().params.query.token, (e) => {
      if (e) {
        toastr.error(e.message);
        return;
      }
      Router.go('/home'); // logged in!
    });
  } else {
    // not logged in... redirect to main domain and get a login token
    window.location = `https://maindomain.com/login?redirect=${window.location}`;
  }
});
1 Like

Wow thanks for the code, friend! I’ll have a look soon.

Anyone see any cracks in this code, security-wise?

It doesn’t hash the token (e.g., the token you submit over the wire is stored in the DB) which is considered bad practice, but it does check whether the token has been used - you could make it more secure if you wanted by also storing the client IP address and only allowing logins from that IP (if that’s how you want to use it).

How would one properly has the token? :slight_smile:

You’d need to manage the tokens yourself, this package doesnt seem to support it - or you could clone this package and add support there. But something along the lines of: const hashed = Accounts._hashLoginToken(token); then that’s what gets inserted/compared

1 Like

FWIW not really sure what hashing the login token will do for added security. At the end of the day you’re passing the token or passing the hashed token as a one time password… It wouldn’t matter if it was hashed or not.

The token has a timeout of 5 seconds, so you’d have 5 seconds to guess that token to get logged in to their account. You’d have LESS than 5 seconds because the window.location redirect is going to happen the moment the token is returned to the client and then the login token will be removed after the first use.

I agree adding in IP checking may be beneficial, but could also run into issues on some networks like LTE if the network happens to change IPs in that moment (very unlikely… I think they use sticky IPs these days?)

Anyway, just my 2 cents on hashing the login token.

1 Like

I didn’t spot the 5 second timeout - that for sure mitigates things. The benefit is the same as a password - what you store vs what you use should be different. So you hash the password in the DB, but the client submits the unhashed variant - this is subtly different from passwords, but it means that someone who has access to your DB, but not your application code cannot use what they see

We have enduser and admin apps that are different builds, of course living on different servers. There is one central database, so users are in one place. We implemented per this Meteor Guide article.

@drone1 What we’re doing is basically set the login token to a cookie on a successful login for a subdomain, then in the other subdomain always try to fetch the token from the cookie and use Meteor.loginWithToken if one is found. Assuming services backend for your subdomains all use the same users collection, of course.

1 Like

We’re doing what @gdc3 is doing, which is that we have the user’s app and the admin’s app, connected to the same database. I searched high and low and wanted to do it without a package (many didn’t seem well-maintained), and many of the articles I found were incomplete. Here’s where I landed.

Early in the lifecycle we import /imports/api/ddp-connect.js. That file looks like:

import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { DDP } from 'meteor/ddp-client'

var remoteUrl; 
if(Meteor.isProduction){ 
    remoteUrl = 'https://subdomain.example.com';
} else {
    remoteUrl = 'http://localhost:3000';
}
Meteor.remoteConnection = DDP.connect(remoteUrl);
Accounts.connection = Meteor.remoteConnection;

Meteor.users = new Mongo.Collection('users', {
    connection: Meteor.remoteConnection
});

if(Meteor.isClient){
    var loggingIn = false;
    Accounts.onLogin(() => {
        var loginToken = Accounts._storedLoginToken();
        // Use the accounts loginToken to login 
        if (loginToken) {
            if(!loggingIn){
                loggingIn = true; // prevents multiple attempts from this re-running 
                Meteor.loginWithToken(loginToken, function(loginErr, result){
                    if (loginErr) {
                        console.log(loginErr.message);
                    } else {
                        if (result.type === 'resume') {
                            FlowRouter.go('/main');
                        }
                    }
                });
            }
        } else {
            console.log('no log in token');
        }
    });
}

export var remote = Meteor.remoteConnection;

As for collections, those look like:

import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { remote } from './ddp-connect.js';

export var someCollection = new Mongo.Collection('someCollection', {
    connection: remote
});

Two other important bits: 1.) we have no defined Methods on the Admin app. We just call the main app’s Methods:

Meteor.remoteConnection.call('theOtherServerMethod', function(err, res){ 
    // stuff 
});

And 2.) the secondary app is started by:

MONGO_URL=mongodb://127.0.0.1:3001/meteor meteor --port 4000

I think the difference here compared to your setup is that logging into the main app does not auto-authenticate you into the Admin app. We wanted extra authentication steps for Admin, so that was fine for us. But this approach allowed us to fully use the “two apps one db” scenario.

(p.s. If anyone sees anything insecure about this approach, I’d love to be made aware.)

2 Likes