Authentification with 2 apps on the same database

#1

I want to know the best practice to create a SSO with Meteor.

My apps structure:

  1. accounts.domain.com which handles register/login
  2. app1.domain.com
  3. app2.domain.com

Currently when a user register | login in accounts.domain.com I drop the userLoginToken in a Cookie. This apps also have a publication: users.current.

In my other apps I subscribe to users.current and when added is called I use Accounts.loginWithToken to login. It works great!

If I logout & logoutOtherClients the user from any other apps the user is disconnected and redirected to a public page. Great again.

My issue is when I login again, the subscription seems to be lost because it doesn’t re-trigger the added observation.

Any idea ?

#2

You do not say it specifically, but when a user logs out I assume you remove the user from users.current? Then when they log back in you insert the user?

What are you publishing in users.current?

#3

This is my users.current publication, is that correct ?

Meteor.publish('users.current', function() {
    if (!this.userId)
        return this.ready();

    return Meteor.users.find({ _id: this.userId });
});

I don’t manually add or remove the user from the publication.

#4

If your user is not authenticated, if (!this.userId), then the return value will be this.ready(); So no userId sent to the user when not authenticated.

If you have the login token, you do not need the userId. loginWithToken only takes a token as input.

Your code could simply check for the existence of a loginToken cookie and use it. If it does not exist, make the user login with password (or whatever).

Make sure your cookie is scoped to your domain, then all your subdomains can access it and not some other domain.

#5

Yes that is what I do an initial load and it works great.

What I want to do is this. If I have 2 windows open: accounts.domain.com and app1.domain.com. Both apps with user logged out. When I login to accounts.domain.com I want app1.domain.com to listen and trigger the loginWithToken.

All logins are manage in accounts.domain.com.

I’m thinking maybe creating an Events = new Mongo.Collection('events'); just to manage custom events publications.

#6

Couple of things to look into:

  • I think your code only works on refresh. To verify, try this scenario.
    • Open both pages, neither is logged in.
    • Log into account.domain.com. You should observe no login on app1.domain.com.
    • Refresh app1.domain.com you will see it log in.
  • Does Meteor.user.find fire when a user logs in? I do not know that it does?
  • Do all servers publish the users.current collection?
    • I assume all servers have the accounts package and are connected to the same mongodb?
    • Or I assume you have opened up multiple DDP connections on each of your client apps?
  • I believe your publication will only send data to signed in clients. You can verify:
    • On the server put a breakpoint in the publish function return this.ready(); line
    • I think that app1.domain.com will not publish the user on app1.domain.com client refresh.

I think this may be a little more complicated for you than you bargained for :slight_smile:

Idea to solve your problem

  • Create a session cookie. When any of your apps starts up it should look to see if a session cookie exists. If a session cookie does not exist then create one with a random number in it (regardless of whether they are logged in), Essentially, this will become your shared session cookie.
  • Notify the server on successful login. Create a method that takes a session cookie and associates the cookie with your login session. You could add the data on the user’s user collection. When you successfully log in on the client; fire the method to notify the server you have a working session. You could fire this in the Accounts.onLogin hook.
  • Create a public event collection. The event collection publish takes the session cookie as an argument. It returns Meteor.user.find({activeSession: sessionCookie})
    • does no check for a logged in user (e.g. this.userId is not set)
    • only publish a small portion of the user collection e.g. userId.
  • Subscribe to the Event collection. On the client subscribe to the event collection. The client then could detect events:
    • When the collection has one entry the user is logged in. At this point user the loginToken cookie to log in.
    • When the collection has no entry the user is logged out. Client can clean up (e.g. go to the home page).
  • Remove Session cookie. On the server hook Accounts.onLogout and remove the cookie.from the user collection. This should cause your event collection to reactively publish and remove the event.
#7

Thanks @brucejo for your answer and ideas. I actually went for a “Facebook connect” like authentication system :wink:

#8

OK, so you solved your problem? Could you explain it a little bit? I may be facing this problem soon too.

#9

What I did is from any app: subdomain.domain.com on the public welcome page I’ve created a Connect button:

connect(): void {
    window.open('accounts.domain.com/connect', 'Connect', 'width=500, height=500');
    window.connect = (token: string) => {
        this.cookies.set('userLoginToken', token);
        Accounts.loginWithToken(token, err => {
            if (err)
                return this.notificationService.error(err);
            return this.stateService.go('app.explore');
        });
    };
}

Then in the opened window:
I first check if the session is active. In this case the login is very quick.

ngOnInit(): void {
    let token: string = this.cookies.get('userLoginToken');

    if (!token)
        return undefined;

    window.opener.connect(token);
    window.close();
}

If not, a login form with email / password is displayed to enable authentication. On success:

let token: string = Accounts._storedLoginToken();
this.cookies.set('userLoginToken', token, { domain: 'domain.com' });
window.opener.connect(token);
window.close();

There is also a Cancel button that simply window.close();

Requirements
In order to prevent Cross-Origin issues you need to explicitly set document.domain = 'domain.com'; on all apps.

#10

Got it,
You use the cookie part and then have the user initiate a connect with a button press rather than examining a Meteor collection to automatically connect.

Thanks!

#11

Exactly, I think it is a better user experience this way because now the user consciously initiate a connect and clearly knows where the authentication come from :slight_smile: