Best practice for loading and persisting current user in an authenticated system


#1

Disclaimer: I know variations on this topic exists here and on SO, but the answers are many, varied, out of date, and have caveats.

What I’d like to discuss is best practice for retrieving and storing currentUser in an authenticated ember-cli system. Its one of those vital aspects of an authenticated web application thats more elusive than it should be with Ember.

These are the requirements I can think of:

  • Authentication system-independent (I’m using ember-simple-auth but the currentUser solution should be authetnitcator-agnostic)
  • Does not require alteration of standards-based authentication responses (such as OAuth 2, which includes access_token and token_type, but not user_id)
  • currentUser is available to all (authenticated) routes and controllers
  • currentUser is a real User object
  • Does not rely on server-injected data
  • It should be ready for ember 2.0 (not controller-centric)

I’ve seen the following solutions:

  • Load currentUser in an initializer (this is the weirdest one IMO, since its not reactive)
  • Extend ember-simple-auth session object by looking up currentUser based on additional data return from token login (undesirable)
  • Various custom solutions

Here’s the controller-centric mixin solution that I’ve been toying with:

mixins/current-user.js:

import Ember from 'ember';

// Sets currentUser on controller when session has been authenticated
export default Ember.Mixin.create({
  setupController: function(controller, model) {
    this._super(controller, model);
    if (controller.session.isAuthenticated) {
      this.store.find('user','me').then(function(user) {
        controller.set('currentUser', user);
      });
    }
  }
});

Note: If I was only mixing this into authenticated routes, I wouldn’t need the controller.session.isAuthenticated check.

And here’s an example of one of my authenticated routes:

routes/dashboard.js:

import Ember from 'ember';
import AuthenticatedRouteMixin from 'simple-auth/mixins/authenticated-route-mixin';
import CurrentUserMixin from '../mixins/current-user';

export default Ember.Route.extend(AuthenticatedRouteMixin, CurrentUserMixin);

I’d really love some feedback and collab. Cheers,


#2

I ended up dropping my last approach and am now extending ember-simple-auth’s Session object as per http://blog.willrax.com/fetching-the-current-user-with-simple-auth/. However, instead of expecting back an additional user ID from the OAuth2 token validation, I make a custom find call as below:

import Ember from "ember";  
import Session from "simple-auth/session";

export default {
  name: "current-user",
  before: "simple-auth",
  initialize: function(container) {
    Session.reopen({
      setCurrentUser: function() {
        var accessToken = this.get('access_token');
        var self = this;

        if (!Ember.isEmpty(accessToken)) {
          return container.lookup('store:main').find('user', 'me').then(function(user) {
            self.set('currentUser', user);
          });
        }
      }.observes('access_token')
    });
  }
};

The API responds to requests to users/me with a JSON record of the current user. Views can now access currentUser via {{session.currentUser.name}}

This has been working well with two caveats:

  1. It is not authentication system independent.
  2. I am now left with a User object in my identity map with an ID of ‘me’. I have tried unloading it but with no success.

#3

Why don’t just fetch it by email?

this.store.find('user', {email: this.get('session.user_email')})
      .then(function(users){
        return users.get('firstObject');
      });

#4

@dirtyhippie Where/When do you set the user_email on the session object?


#5

the answer is in the one post above


#6

Just an update for those who are using 0.8.0-beta.1 of ember-simple-auth and run into issues.

Due to changes in storage strategy, the above solution doesn’t work anyomore. You need to use the following:

import Ember from "ember";  
import Session from "simple-auth/session";

export default {
  name: "current-user",
  before: "simple-auth",
  initialize: function(container) {
    Session.reopen({
      setCurrentUser: function() {
        var accessToken = this.get('secure.access_token');
        var self = this;

        if (!Ember.isEmpty(accessToken)) {
          return container.lookup('store:main').find('user', 'me').then(function(user) {
            self.set('content.currentUser', user);
          });
        }
      }.observes('secure.access_token')
    });
  }
};

The most important part about this is, that you need to set content.currentUser explicitly, otherwise it will lead to an infinite callback loop. This seems to be a bug with Ember.ObjectProxy, but I haven’t dug into that deeper yet.


#7

@mmuehlberger thank you for update! For sure if it is a bug in the framework we should open an issue.


#8

Using something similar to @mmuehlberger’s code, I’m wondering how to wait until user data is loaded before EmberData kicks into action on the next route.

Example: I’m building a sports management game using Ember. User logs in and is taken to a team dashboard. However, to retrieve the model related to their team, info is required from the find(‘user’,‘me’) object. How do I make the team route wait until the currentUser is available?

For now, after successful login I transition to the team route, but it fails because the session’s currentUser object isn’t available yet.


#9

I would either provide the necessary data in the session response if the needed information applies to more than just the dashboard (e.g. the user’s id) or use promises (e.g. find('user', 'me').then()) in the dashboard route and wait for them to resolve before fetching data.


#10

When is setCurrentUser() called?


#11

How do I get the current user in a route?


#12

I’ve modified / combined the above comments to create my version:

import Ember from "ember";
import Session from "simple-auth/session";

export default {
  name: "current-user",
  before: "simple-auth",
  initialize: function(container) {
    Session.reopen({

      setCurrentUser: function() {
        if (this.get('isAuthenticated')) {
          return container.lookup('service:store').queryRecord('user', {me: ''}).then((user) => {
            this.set('content.currentUser', user);
          });
        }
      }.observes('isAuthenticated')
    });
  }
};

This has some improvements:

  • It prevents the unwanted user with id of ‘me’ in the store like dirtyhippie suggested.
  • Using ES6 arrow function it eliminates the need of self = this;
  • Fixes deprecation warning by replacing store:main with service:store
  • Observes isAuthenticated property, which is nicer I think

I wonder if there is a way to lose the observer for an action or something. Watching the talk of Steffan Penner called The observer tip-jar made me wary of using observers.

@oliverbarnes setCurrentUser() is called when the isAuthenticated property changes. When the user logs in or out for example.

@NullVoxPopuli The session service is injected everywhere I believe. So for example in a template you can use {{session.currentUser.id}}. In a route you can use console.log(this.get('session.currentUser.id'));

If anyone has more improvements, please add them.


#13

The simple-auth change to ember-simple-auth, and the session.js changed too, is there more solutions?