How to best provide one login across multiple Meteor apps

I am currently investigating this topic, as I have multiple apps that should share the same login.

I was thinking about two possible approaches here:

A - Replicating the Meteor.users collections

good

  • Not much to “code”, more of a deployment configuration
  • I have the guarantee, that logins will work out of the box (do I?)

bad

  • feels somehow wrong to just copy-paste users (anyone has serious doubts on this?)
  • not sure about scalability here, too (consider one or two more Meteor apps joining the party…)

B - Providing an oAuth authentication server (to Provide Meteor.loginWith)

good

  • scalability / performance

bad

  • as far as I can see, there is no package or similar out there for that, so it seems that I have to implement the server + the packages myself --> lots of code and time to invest

What do you guys think? Anyone here with experience on that or a third option?

Hmmh. You could set up one app as the single source of truth and then write a very simple API on this server to authenticate users.

Like this:

import { Accounts } from 'meteor/accounts-base';
...
Meteor.startup(() => {
  Accounts.registerLoginHandler("custom", (loginRequest) => {
    if(!loginRequest.custom) return undefined;
    ...
    const username = loginRequest.user;
    const password = loginRequest.password;
    ...
    //do auth stuff here against your single source of truth like 
    const result = HTTP.call("GET", "https://authserver.foo/auth",
        {
            auth: `${username}:${password}`
        }
    );
    ...
    //if successfully authed
    const servicename = "foo";
    const serviceData = {
        id: username,
        //add more data if you wish
    }
    return Accounts.updateOrCreateUserFromExternalService(
        serviceName,
        serviceData
    )
  }
}

That’s the server code for the other apps. The https://authserver.foo/auth endpoint you have to write yourself (and probably also one for changing password, adding/removing users)

On the clients you then define this function:

import { Accounts } from 'meteor/accounts-base';
...
const tryToLogin = (user, pass, callback) => {
        const loginRequest = {
            custom: true,
            user: user,
            password: pass
        }
        Accounts.callLoginMethod({
            methodArguments: [loginRequest],
            userCallback: callback
        });
    }
}

and you actually call it like this (like in an event handler for a form submit):

tryToLogin(username, password, (err, res) => {
                if(err) {
                    //deal with the error, likely a wrong user/pass
                } else {
                    //logged in successfully, deal with that
                }
            })

This is a very barebones solution, though and doesn’t deal with edge cases.

It’s basically an abbreviated OAuth variant - you do not need to go full OAuth because you fully control all the servers you’re talking to.

Edit: And, yeah, don’t use the accounts-password package if you want to implement this.

4 Likes

Thank you for this input, it helps a lot to get pointed into a direction sometimes :slight_smile: . I will have to implement the full range of what Accounts provide (register, login, change password, emails, delete users etc.), so it will take a while until this runs completely.

The other remaining thing is, that I will still need to have the user docs being synced between the apps, since I need the names, profile images etc. Wouldn’t this require me anyway to replicate the user collection?

To me it sounds like your apps should share the database?

2 Likes

This one is ready to use solution for auth server.

2 Likes

I’m not seeing any user management stuff in there.

By investigating your link, I found that Rocketchat maintains an up to date Meteor OAuth2 Server (using the linked node-oauth2-server): https://github.com/RocketChat/rocketchat-oauth2-server

I will check this out, maybe they have already solved the issue with multi-app user logins a long time ago

Not within the scope of authentication but in syncing the user profiles.

1 Like

In the past I solved this by sharing the DB but in this case I will not be able to do that. The only options in terms of “sharing” would be using remote DDP connections.

I’d be interested to hear the reasons? We’re in a similar situation, in the sense that we’re trying to decide whether to share the database or expose some of the common data via Meteor methods or a REST API. So your thoughts could have great value for me :slight_smile:

We have two applications with mostly independent data, except the users that will share the same account across the app-network. It may happen, that some users will use one or another or both and we can’t tell the distribution yet.

Thus, we aim to decouple the applications as much as possible in order to develop, deploy and maintain them independently from each other.

Also they may scale totally different (due to the flexible user distribution) and we came to the conclusion, that we will focus on a good working user auth-management in the first place and keep everything else separated.

This is also encouraged a lot in Microservice development, where you trade the extra offset in the beginning (extra database, sometimes redundant data in two apps, exchange lots of data) for a better long term maintenance (apps run independent from each other, scale more efficiently, easier to split dev teams into the services and let them develop more independently).

I also have to add, that we have experienced a lot of technical debt using a shared database when our apps became larger and new features had to be added. These features were not foreseen in the beginning (the classic one…) and suddenly two apps became tightly coupled due shared collection models. This time we want to avoid it in the first place. I hope this makes this topic more understandable :slight_smile:

@rhywden I tried to implement your example against a rocketchat oauth2 meteor app (which works fine standalone, setup was 5min :sunny: ) but it seems, that tryLogin still points to the default password loginhandler, because the callback returns an error, that the custom field is invalid:

Exception while invoking method 'login' { Error: Match error: Unknown key in field custom
  at check (packages/check/match.js:36:17)
  at MethodInvocation.<anonymous> (packages/accounts-password/password_server.js:290:3)
  at tryLoginMethod (packages/accounts-base/accounts_server.js:460:31)
  at tryLoginMethod (packages/accounts-base/accounts_server.js:1294:14)

I checked the Accounts and the handler is registered:

_loginHandlers: 
  [ { name: 'resume', handler: [Function] },
    { name: 'password', handler: [Function] },
    { name: 'password', handler: [Function] },
    { name: 'custom', handler: [Function] } ],

And I think it fails, because it never reaches the custom handler and fails already at password. Is this a bug in the Accounts system or did I miss something here?

Edit: nevermind, I removed the accounts-password and it worked :slight_smile:

1 Like

I have been thinking about this for one of our projects. The apps could share DB but it doesnt make sense to do that just for the users collection. One app that is not critital can affect the other one and they should not.

I was thinking about the updating the user part by doind something similar as the facebook integration does. You update the user data on DB on each login, that way is almost always updated. Still not sure if only this solves the issue. I will need to test it later when I start developing it.

It would be great to know how you ended up solving this, when you do.

I think the best way is to create a center user database with oauth supports.
It may take some time to build at the beginning. But everything will be easier later.

1 Like

I think the best way is to create a center user database with oauth supports.

This is exactly what I am currently doing.

It would be great to know how you ended up solving this, when you do.

I will post some updates, once I am at a working state.

2 Likes

I’m not sure why you’re deadset on OAuth. If everything is under your own control you do not need it.

OAuth is a protocol designed for authentication against 3rd party auth providers not under your control. For example, I’m using what I detailed above to auth against an LDAP under my control. Why would I want to introduce an OAuth shim there?

1 Like

Yes, you can because you control everything for now. But it doesn’t mean you should do.
In a long run, your systems may change. You may delegate your work to someone else, maybe even a team. Maybe your apps with Meteor weren’t there anymore, maybe you or other people replace them by other technologies. At that point you will see it’d be easier if you have a standard system.

1 Like

I think you both @rhywden and @minhna have a valid argument.

On the one hand it bloats an app and makes it rather insecure if there is a whole bunch of code added that is rarely to never be in use. On the other hand a long term strategy should cover potential changes a minimize technological debt.

I came to the following point, that I am using an OAuth server mainly because there might a good chance, that we may expand our work with some industry partners and thus later let users share resources (they own on our network) with their third parties.

The idea is to make a custom loginWith that can be configured to initially just cover minimal features (authentication, see the example from @rhywden ) and can then be extended to authorizing resources. Does this makes sense?

Just FYI:

I already forked the rocket chat oauth2 server, decaffeinated, updated to node-oauth2-server 3.0, removed the express deps and make it more configurable.

When I have a running setup that solves my issue, described in the initial post I will publish it on GitHub as example project, too.

1 Like