Help with JWT Auth with Ember Simple Auth, Ember Simple Auth Token, and Knock on Rails Backend


#1

Hi there,

I am having trouble with setting up JWT authentication with Ember Simple Auth, Ember Simple Auth Token, and Knock on a Rails Backend.

The only part that doesn’t seem to be working is that the user does seem to be loaded into the session after being authenticated.

In summary, here’s what I have:

  1. Ember: login form sends email and password to Rails Backend
    1. Login form is using Ember Simple Auth Token’s JWT authenticator
  2. Rails + Knock: authenticates the credentials and returns a JWT token
  3. Ember: receives the JWT token and says it’s been authenticated

However, when I look at the Ember Data Store, there is no user loaded. Am I missing something here?


#2

Not sure what you mean by “user” in this context, assuming it’s a model with some user data for the currently logged-in user, but ESA and the authentication system don’t really have anything to do with fetching a user record. The function of ESA is to use some method of authentication to fetch some sort of authorization information (usually a token, in this case a JWT). Fetching user data isn’t really included in this domain of concern. Many apps may not want to do that and even if they did the API/model formats would vary greatly.

So all that to say, it sounds like your authentication setup is working great and the user fetching is just a roll your own kinda thing. One API I worked with in the past had an endpoint that was something like /users/me for fetching your own user record, but you could also get it via ID or email or whatever. Really depends on your backend. I think in some implementations you could include the user id in the JWT that you get, and then once you have the JWT you could look up the user record.


#3

Thanks!

To your question, by user, I’m referring to both the Ember model and Rails model that is used for authentication and authorization.

When I log in with the credentials for a user (e.g., email and password), I expect the Rails backend to authenticate the credentials and return a JWT token, and then for Ember/ESA/ESAT to use that token for future requests. In addition, I was expecting ESA/ESAT to automatically load the user with whom’s credentials I used to be loaded into the Ember Data Store.

As you mentioned, loading the currently logged in user into the Ember Data Store is not an automatic thing but rather “roll your own.” Fortunately, ESA documentation provided an example of how to do this here: Managing a Current User.

This example loads and manages the current user using an Ember service. It also provides two different ways for loading the current user’s data:

  1. via a dedicated API endpoint; and
  2. using the user’s ID and using findRecord.

I opted to use the first approach: a dedicated API endpoint. However, it still doesn’t seem to work.

For reference, here’s what I did.

Created a /users/me endpoint in my Rails app

# routes.rb
...
resources :users do
  get "me", on: :collection
...
# controllers/users_controller.rb
class User < ApplicationRecord
  ...
  def me
    render json: current_user #current_user is provided by Knock
  end
  ...
end

Created Ember service as per ESA guide

// app/services/current-user.js
import Ember from 'ember';

const { inject: { service }, RSVP } = Ember;

export default Ember.Service.extend({
  session: service('session'),
  store: service(),

  load() {
    if (this.get('session.isAuthenticated')) {
      return this.get('store').queryRecord('user', { me: true }).then((user) => {
        this.set('user', user);
      });
    } else {
      return RSVP.resolve();
    }
  }
});

Created user adapter as ESA guide

// app/adapters/user.js
import ApplicationAdapter from './application';

export default ApplicationAdapter.extend({
  urlForQueryRecord(query) {
    if (query.me) {
      delete query.me;
      return `${this._super(...arguments)}/me`;
    }

    return this._super(...arguments);
  }
});

Modified routes/appplication.js to load the the current user as per the ESA guide

// app/routes/application.js
import Ember from 'ember';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';

const { service } = Ember.inject;

export default Ember.Route.extend(ApplicationRouteMixin, {
  currentUser: service(),

  beforeModel() {
    return this._loadCurrentUser();
  },

  sessionAuthenticated() {
    this._super(...arguments);
    this._loadCurrentUser();
  },

  _loadCurrentUser() {
    return this.get('currentUser').load().catch(() => this.get('session').invalidate());
  }
});

However, this doesn’t seem to work. The code within _loadCurrentUser of routes/application.js constantly fails and then invalidates the session after each authentication.

If I remove calls to _loadCurrentUser() with routes/application.js, then the session remains authenticated. However, as a result, no user is loaded into the Ember Data Store.

The guide makes it seem all pretty straight forward, so I’m not sure what I’m doing wrong.


#4

Ah gotcha. Didn’t know they added that to the ESA docs, that’s great. At a glance all your Ember code looks fine. I can’t speak to the Rails stuff as it’s been a while but that also seems pretty straightforward.

So you said this:

The code within _loadCurrentUser of routes/application.js constantly fails and then invalidates the session after each authentication.

Any idea why it fails? If it is invalidating the session it sound like your API might be returning an error response code like 401 or something.


#5

The API seems to be working fine. I can see it returning the proper JSON object with the User data in it.

I’m still pretty new to RSVP/Promises, but it seems like there’s a weird race condition going on. While I can see the API returning the appropriate JSON, it seems to be, somehow, too late and the catch block which invalidates the session inside _loadCurrentUser() runs.


#6

Ok maybe try replacing the _loadCurrentUser method with this:

  _loadCurrentUser() {
    return this.get('currentUser').load().catch((error) => {
      // put a breakpoint and maybe a console.log here and see what "error" is and where it's coming from
      return this.get('session').invalidate();
    });
  }

And put a breakpoint in there to try and figure out where the error is coming from. AFAIK catch should only be triggered if there is a runtime error or if the request fails so my guess is either there’s an issue happening in the “load” method or there’s some other problem caused by the function call stack which results from making the request.


#7

This is the error I’m getting:

TypeError: (0 , _private.getOwner) is not a function
    at Class.serializerFor (ext.js:81)
    at Class.superWrapper [as serializerFor] (utils.js:350)
    at serializerForAdapter (-private.js:7097)
    at -private.js:7453
    at tryCatcher (rsvp.js:334)
    at invokeCallback (rsvp.js:507)
    at publish (rsvp.js:493)
    at rsvp.js:17
    at invoke (backburner.js:331)
    at Queue.flush (backburner.js:223)

#8

Hmmmm ok well that’s something but that is kinda weird. It looks like it’s failing when trying to serialize the server response at the step where it looks up which serializer to use…

You aren’t customizing the store are you? And do you have a serializer for “user” also? And what does your application serializer look like? If you don’t have a user serializer maybe try defining one that re-exports the application serializer…


#9

I’m not modiying the store within this set of code from what I can tell. I’ve inherited this app, so I haven’t had a chance to look through it thoroughly. From a surface level search, the app has:

  • adapters/user.js
  • models/user.js

There is no serializer/user.js. I’ll try creating one now.


#10

I followed this guide to create the serializer: Customizing Serializers.

This is what my user serializer looks like:

import DS from 'ember-data';

export default DS.JSONAPISerializer.extend({
});

Not much to it. It is the same code as what would have been in the serializers/application.js if the app had one – which it doesn’t.


#11

Ok… and that didn’t change anything? Is your application adapter extending JSONAPIAdapter? If not what does that look like?


#12

The user adapter is extending ApplicationAdapter. I copied the implementation from the ESA guide. Here’s what it looks like:

import ApplicationAdapter from './application';

export default ApplicationAdapter.extend({
  urlForQueryRecord(query) {
    if (query.me) {
      delete query.me;
      return `${this._super(...arguments)}/me`;
    }

    return this._super(...arguments);
  }
});

#13

Yeah but I mean if it’s extending your application adapter what does that look like? The first line:

import ApplicationAdapter from './application';

Is trying to import and extend the adapter in app/adapters/application.js. So if the application adapter is a custom one it may be choking on that somewhere, if not that’s a dead end. If you don’t have an application adapter defined I’d expect that import to fail, i think during build time, so that would be strange…


#14

Here’s the ApplicationAdapter. It’s using a mixin from Ember Simple Auth Token.

import ActiveModelAdapter from 'active-model-adapter';
import TokenAuthorizerMixin from 'ember-simple-auth-token/mixins/token-authorizer';
import config from '../config/environment';

export default ActiveModelAdapter.extend(TokenAuthorizerMixin, {
  host: `${config.host}`,
});

#15

Ok… that’s one of the things I was guessing. I’d think you’d need to use the active model serializer also (unless your API is emitting JSONAPI format?). So try making an application serializer that looks like this (from active model adapter docs):

// app/serializers/application.js
import { ActiveModelSerializer } from 'active-model-adapter';

export default ActiveModelSerializer.extend();

and then either delete the user serializer or change it to:

// app/serializers/user.js
import ApplicationSerializer from './application';

export default ApplicationSerializer.extend();

I’m not sure that will fix anything but it will at least mean you have the correct adapter/serializer pair worked out. The active model adapter has the following:

defaultSerializer: '-active-model'

So I’d think it would correctly infer the serializer type but it’s worth ruling that out…


#16

First of all, thanks so much for continuing to help! I really appreciate it!

I’ve made the changes you’ve suggested. Unfortunately, the error is still occurring.

From what I can tell, the API is using the active-model-adapter syntax. This is the JSON response for requesting the current user from /users/me

{"user":{"id":1,"email":"john@test.com","first_name":"J","last_name":"G","title":null,"created_at":"2019-02-20T05:35:55.376+13:00","updated_at":"2019-02-20T05:35:55.376+13:00","initials":"JG","organisational_unit_id":1}}

From what I understand, active-model-adapter simple changes the naming conventions to use underscores instead of camelCase. So that looks right.

What is interesting, however, is that my Rails API isn’t using the ember-rails or active-model-adapter-source gem. It is, instead, using active-model-serializers which seems to be no longer active but also seems to be doing the right thing.


#17

Hi,

I can’t speak much to the specific issue you are having, but I figured I’d chime in with my side project which used the same libraries you are trying to use.

Frontend Code

Backend Code

This blog series was a great help when I got this working initially.

Feel free to dig around either repo and I can try to answer any questions you may have.

Hope that helps.

Matt