JWT Authentication with Rails + Ember Part II: Custom Ember Simple Auth

In Part I of this series, we set up a Rails API with the Knock gem, and implemented a JWT-based authentication system. In this post, we'll take a look at customizing the Ember Simple Auth add-on to complete our JWT auth flow.

You can see a live demo here and check out the repo for Part I here and Part II here


Let's begin by breaking down the authentication cycle that will occur, starting with our user signing in via our Ember app:

  • The user fills our the log in form with their email and password.
  • Ember will send this request to our authentication API endpoint, which, courtesy of Knock, is http://localhost:3000/knock/auth_token.
  • Rails (with the help of Knock) will authorize the current user (if their email and password are correct), and send the JWT back to Ember.
  • Ember will store the JWT in local storage.
  • Ember will extract the JWT from local storage and use it to construct the Authorization header of any subsequent requests to the Rails API. Rails will look for the header on any authenticated requests and only send back the requested data if a valid JWT is present.

Okay, let's build it!

Customizing Ember Simple Auth

The Ember Simple Auth add-on provides four basic building blocks that we'll use to enact our authentication cycle.

They are:

  • session
  • session store
  • authenticators
  • authorizers.

The session provides methods for authenticating or invaliding the session, and methods for setting/getting session data.

The session store is where we will persist session data––some unique identifier of a current user (in this case, our JWT).

The authenticator authenticates the session, and the authorizer uses the data retrieved by the authenticator to authorize outgoing requests.

We'll be building a custom authenticator that uses methods provided by the session service to request the JWT from Rails. We'll store that JWT in the session store. Lastly, our custom authorizer will retrieve the JWT from the session store and use it to construct the Authorization header of outgoing requests.

Let's get started!

Setting Up ESA

This tutorial assumes we have a basic Ember app set up, with a User model in which the user has at least an email and password.

We'll begin by installing the Ember Simple Auth add-on. In your terminal, in the directory of your Ember app, run the following:

ember install ember-simple-auth

Add the login route to your router.js

this.route('login');

Then, go ahead and generate a login controller and template:

ember g controller login; ember g template login

Log In Template

Let's build out our login form on the template:

// app/templates/login.hbs

<form {{action 'authenticate' on='submit'}}>
  <label for="identification">Email</label>
  {{input id='identification' value=identification}}
  <label for="password">Password</l abel>
  {{input id='password' value=password}}
  <button type="submit">Login</button>
 </form>

Note that we've attached an action, called authenticate, which will fire on the submission of our form.

Now we're ready to build out that action, which we'll define in our login controller.

LogIn Controller

// app/controllers/login.js
import Ember from 'ember';

export default Ember.Controller.extend({
  session: Ember.inject.service(),

  actions: {
    authenticate: function() {
      var credentials = this.getProperties
        ('identification', 'password'),
        authenticator = 'authenticator:jwt';
      this.get('session').authenticate(authenticator, 
        credentials).catch((reason)=>{
        this.set('errorMessage', reason.error || reason);
      });
    }
  }
});

Here we're doing a few things.

  • Injecting the session service. This service is available to us thanks to the ESA add-on. Remember when we said earlier that the session service will expose some methods that will allow us to authenticate or invalidate the session? Well, this is where we will be using those methods.
  • Defining our authenticate action, which we just attached to the submission of our login form.
  • That action is responsible for:
    • Grabbing the user's identification (in our case, email) and password from the form.
    • Invoking the authenticate function (which comes to us from ESA) on the session service, and passing it two arguments: the JWT authenticator (we haven't built it yet!), and the credentials. This will be the code that fires the authentication request to our Rails API endpoint, http://localhost:3000/knock/auth_token. We'll have to build out our custom JWT authenticator for that to work though.

Log Out Link + Action

We'll define our "log out" action in the Application Route:

import Ember from 'ember';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';

const { service } = Ember.inject;

export default Ember.Route.extend(ApplicationRouteMixin, {
  actions: {
    invalidateSession: function() {
        this.get('session').invalidate();
    }
  }

Here, we're invoking the invalidate() function on our session service. We'll also define this function in our custom authenticator.

The "log out" link that triggers this action should look something like this:

// app/templates/application.hbs

<li>< a href="#" {{action 'invalidateSession'}}>Logout</ a></li>

Okay, now we're ready to build our custom authenticator and authorizer.

Building the Custom Authenticator

We'll call our custom authenticator jwt. From the command line:

ember g authenticator jwt

This will generate the following file: app/authenticators/jwt.js.

Here, we'll extend the base authenticator that ESA provides us with and configure it to hit our Rails API endpoint.

Let's take a look at the completed authenticator first, then we'll break it down one step at a time.

// app/authenticators/jwt.js
import Ember from 'ember';
import Base from 'ember-simple-auth/authenticators/base';
import config from '../config/environment';

const { RSVP: { Promise }, $: { ajax }, run } = Ember;

export default Base.extend({
  tokenEndpoint: `${config.host}/knock/auth_token`,

  restore(data) {
    return new Promise((resolve, reject) => {
      if (!Ember.isEmpty(data.token)) {
        resolve(data);
      } else {
        reject();
      }
    });
  },

  authenticate(creds) {
    const { identification, password } = creds;
    const data = JSON.stringify({
      auth: {
        email: identification,
        password
      }
    });

    const requestOptions = {
      url: this.tokenEndpoint,
      type: 'POST',
      data,
      contentType: 'application/json',
      dataType: 'json'
    };

    return new Promise((resolve, reject) => {
      ajax(requestOptions).then((response) => {
        const { jwt } = response;
        // Wrapping aync operation in Ember.run
        run(() => {
          resolve({
            token: jwt
          });
        });
      }, (error) => {
        // Wrapping aync operation in Ember.run
        run(() => {
          reject(error);
        });
      });
    });
  },

  invalidate(data) {
    return Promise.resolve(data);
  }
});

Notice that we're importing the base authenticator from ESA, as well as our own config/environment.js. We're requiring this last one, simply because we set ENV.host in our development environment there to be equal to http://localhost:3000.

// config/environment.js

...
if (environment === 'development') {
    ENV.host = 'http://localhost:3000';
}
...
authenticate()

The authenticate() function gets invoked when we call the session service's authenticate method in our login controller, passing in an argument of our jwt authenticator, and the user's credentials plucked from the form:

// app/controllers/login.js
...
this.get('session').authenticate(authenticator, credentials)
..

So, our jwt authenticator's authenticate() method will get invoked with an argument of the credentials that we take from the form the user filled out. Something like this:

authenticate({email: "sophie@email.com", password: "secretpassword"})

The authenticate function then does the following:

  • Construct a JSON object out of the credentials
 const { identification, password } = creds;
    const data = JSON.stringify({
      auth: {
        email: identification,
        password
      }
    });
  • Construct the request options with a url of our tokenEndpoint, which was earlier set to http://localhost:3000/knock/auth_token, the JSON-ified credentials, and a few other necessary bits of info:
const requestOptions = {
      url: this.tokenEndpoint,
      type: 'POST',
      data,
      contentType: 'application/json',
      dataType: 'json'
    };

Create a new Promise that makes the AJAX request to our API endpoint:

return new Promise((resolve, reject) => {
  ajax(requestOptions).then((response) => {
    const { jwt } = response;
    run(() => {
      resolve({
        token: jwt
      });
    });
   }, (error) => {
        run(() => {
          reject(error);
        });
      });
   });
}

Aside: Promises

A quick refresher/intro to JavaScript Promises, for those of us who are unfamiliar. MDN says of Promises:

A Promise represents an operation that hasn't completed yet, but is expected in the future.

We can use a Promise to wrap asynchronous code (like an AJAX call). A Promise is created with the new keyword, and given an argument of an anonymous function that in turn takes two arguments, resolve and reject.

If the asynchronous code in our Promise returns a successful result, we will call the resolve function. Otherwise, we will call the reject keyword.

Okay, back to your regularly scheduled programming...

(sort of)...

Aside: The Ember Run Loop

The run loop is essentially Ember's way of scheduling and queueing batches of work. It's not important to understand the mechanics of that for the purposes of this tutorial. But it is super interesting and I suggest you follow the link to learn more.

Okay, now back to your regularly scheduled programming...

A closer Look at our resolve function

Note the the resolve function does one thing: sets the token attribute equal to jwt, i.e. the response from the API.

Recall from the previous post that the payload our API sends back to Ember, when sent an authorization request, looks like this:

{jwt: "asdf8q94raidflj3892.a2389y428iwhfa.af8923ur29}

Our resolve function takes this JWT, and, by setting the token attribute, stores it in localStorage.

restore()

The restore() function is invoked by the session when properties in the session store change (i.e. a new tab has been opened). Essentially, we're checking to see if the session store is in fact storing a token. If so, we'll resolve our Promise, otherwise, we'll reject.

invalidate()

Lastly, we've defined our invalidate() function, which is trigged when we call invalidate on the session service we injected in to our Application Route

// app/routes/applicaton.js
...
this.get('session').invalidate();
...

Our invalidate function resolves a promise, which will result in the session becoming unauthenticated.

invalidate(data) {
  return Promise.resolve(data);
}

Okay, now we're ready to define our custom authorizer.

Building the Custom Authorizer

If our user is valid, and Rails + Knock can authenticate them on the backend, the JWT gets sent back to Ember, and stored in localStorage as {jwt: aslfiejaf832.sadf3294uasdf3.faw39823asdf}

We have to build an authorizer that will add the Authorization header to any requests to our API, setting the header equal to the JWT. That way, Rails will receive the JWT with any requests from Ember and be able to identify the current user.

Let's do it!

Generate the authorizer with:

ember g authorizer custom

This will generate the following file: app/authorizers/custom.js.

We'll define our authorizer here.

// app/authorizers/custom.js
import Base from 'ember-simple-auth/authorizers/base';
import Ember from 'ember';

export default Base.extend({
  session: Ember.inject.service(),
  authorize(data, block) {
    if (Ember.testing) {
      block('Authorization', 'Bearer beyonce');
    }
    const { token } = data
    if (this.get('session.isAuthenticated') && token) {
      block('Authorization', `Bearer ${token}`);
    }
  }
});

Our custom authorizer:

  • Imports the base authorizer from ESA
  • Defines the authorize() function, which:
    *Ttakes in an argument of the JWT, taken from localStorage, and sets the outgoing request's Authorization header to: Bearer <token>.

The authorizer's authorize method will be invoked by the session service automatically when Ember makes any calls to our Rails API. However, in order for that to happen, we have to tell our Ember app to user our custom authorizer.

We'll can accomplish this with the following:

  • Specify the authorizer in your application adapter:
// app/adapters/application.js

import ActiveModelAdapter from 'active-model-adapter';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';
import config from '../config/environment';


export default ActiveModelAdapter.extend(DataAdapterMixin, {
  host: `${config.host}`,
  authorizer: 'authorizer:custom'
});
  • Specify the authorizer in the ESA configuration in your environment.js:
// config/environment.js

ENV['ember-simple-auth'] = {
    authorizer: 'authorizer:custom',
    routeAfterAuthentication: '/'
};

Conclusion

To summarize, in Part I of this series, we set up JWT authorization in a Rails API using the Knock gem. Knock is responsible for receiving authorization requests. If a valid user is trying to log in, Knock will authenticate the user and send a JWT back to Ember. Knock also provides a current_user method that can extract the JWT from the header of incoming requests and return the current_user using the information in the decoded token.

In Part II of this series, we implemented a customization of the Ember Simple Auth add-on, so that Ember would send authorization requests to the correct endpoint of our API. In our case, that endpoint is http://localhost:3000/knock/auth_token.

Our custom authenticator was built to send the user's info (email and password) to that endpoint, and store the response (i.e the JWT), in localStorage.

We also built a custom authorizer that will intercept all outgoing API requests, retrieve the JWT from local storage, and set the Authentication header of that request.

These posts didn't cover:

  • User registration
  • An Ember "current user" feature

Those features are built out in these repos though, if you'd care to take a look.

Happy coding!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus