Authentication in Ember.js ~ July 12, 2013

This post is about authentication, but it also covers a more general topic: setting up a framework for ajax calls that accepts errors and reacts to them appropriately. When implementing my own Ember app, I decided against ember-data and ember-auth because they’re both new enough that the rough edges were causing me more headache than good while learning my way around Ember to begin with. I also really struggled with getting basic auth such as this working while learning Ember, so hopefully this will be useful to a few other Ember newbies out there.

This guide was written for Ember 1.0.0-rc4, and originally published in May as a gist.

The Difficulty

Ember makes it hard to statically get a reference to the currently active Route, and with good reason. If the active route is changed in an unexpected way, or at an unexpected time, the result is undefined. Therefore, all result handlers from ajax calls must have a reference to the Route in hand so that they can react to the results appropriately.

Robin Ward wrote a good article on using Ember without ember-data, which convinced me to pursue this approach. There are some conventions your service must follow to use ember-data easily which I’m not a huge fan of, and the whole thing is still pretty new. I’m sure it’ll be awesome in time, but for now I’d rather just handle the data layer myself.

A Solution

A good way to accomplish this is with an Ember Mixin. With this, we can define a set of functions for calling REST services that do some appropriate preliminary processing on the result (such as redirecting to a login route when receiving an authentication challenge).

First, set up our App. Local storage is used to store the authentication token for two reasons: 1) cookies can be unreliable (they are difficult to set correctly if your app is running within an iframe, for instance) and 2) in some server frameworks, pulling cookies out of a request is slightly less convenient than GET or POST parameters, and we don’t actually need the browser to attach our auth token to each request for us; we can handle it.

app.js
1
2
3
4
5
6
7
window.App = Ember.Application.create({
    // support for remembering auth via localStorage. Only works on modern browsers
    authToken: localStorage['authToken'],

    // global alert error for user feedback <String>
    error: null
});

Remember that LocalStorage is a fairly new feature. If you are developing an app that must support older browsers (unlikely, since you’re developing in Ember to begin with) then you should use Cookies as your authentication transport instead, as annoying a they are.

Next, define the Mixin.

app.js
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Mixin to any Route that you want to be able to send authenticated ajax calls from. Calls that fail for auth
// reasons will result in a redirect to the 'login' route automatically
App.Ajax = Ember.Mixin.create({
    ajaxSuccessHandler: function (json) {
        // in my app, error code 201 is reserved for authentication errors that require a login
        if (json.error != null && json.error.code == 201) {
            App.error.set(json.error.message);
            var self = this;
            // delay to let current processing finish.
            setTimeout(function () { self.transitionTo('login'); });
            // let handlers further down the Promise chain know that we've already handled this one.
            return null;
        }
        return json;
    },

    // perform ajax GET call to retrieve json
    GET: function (url, data) {
        var settings = {data: data || {}};
        settings.url = url;
        settings.dataType = "json";
        settings.type = "GET";
        var authToken = App.get('authToken');
        if (authToken != null) settings.data.authToken = authToken;
        return this.ajax(settings);
    },

    // perform ajax POST call to retrieve json
    POST: function (url, data) {
        var settings = {data: data || {}};
        settings.url = url;
        settings.dataType = "json";
        settings.type = "POST";
        var authToken = App.get('authToken');
        if (authToken != null) settings.data.authToken = authToken;
        // post our data as a JSON object in the request body
        settings.data = JSON.stringify(settings.data);
        return this.ajax(settings);
    },

    ajax: function (settings) {
        var self = this;
        return $.ajax(settings).then(function () {
            // preserve 'this' for the success handler
            return self.ajaxSuccessHandler.apply(self, $.makeArray(arguments));
        });
    }
});

Templates

The templates are structured to look reasonable with Twitter’s Bootstrap CSS.

Here we have the top level template. It includes a conditionally displayed logout button in the top navigation bar, and a container for app contents.

app.hbs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- top level app -->
<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="navbar-inner">
        <a class="brand" href="#">App</a>
        <!-- if our App's authToken field is set, display a logout button. Action event is handled
             in ApplicationRoute -->
        {{#if App.authToken}}
            <button class="btn pull-right" {{action logout}}>Logout</button>
        {{/if}}
    </div>
</div>
<div class="container">
    <!-- if our App's error field is set, display the error with a close button. Button's action is
         handled in ApplicationRoute -->
    {{#if App.error}}
        <div class="alert alert-error">
            <button type="button" class="close" data-dismiss="alert"
                {{action "dismissError"}}>&times;</button>
            {{App.error}}}
        </div>
    {{/if}}
    {{outlet}}
</div>

The login template is pretty standard: username and password fields, plus a “remember me” checkbox, telling the application to store the authentication token in LocalStorage if checked.

login.hbs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 'login' route's template, goes in the top level outlet, is replaced by other route content
     after -->
<div class="session_form">
    <form>
        <fieldset>
            <legend>Sign In</legend>
            <label>
                Username<br>
                {{view Ember.TextField name="username" valueBinding="username"}}
            </label>
            <label>
                Password<br>
                {{view Ember.TextField type="password" name="password" valueBinding="password"}}
            </label>
            <label class="checkbox">Remember me {{view Ember.Checkbox type="checkbox"
                name="remember_me" checkedBinding="remember"}}</label>
            <br>
            <!-- login action handled in LoginRoute -->
            <button {{action "login"}} class="btn" type="submit">Sign In</button>
        </fieldset>
    </form>
</div>

Routes

App.Router is defined here with just the login route, but of course a full app would have more. The ApplicationRoute handles the logout event (from the logout button).

app.js
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
App.Router.map(function () {
    this.resource('login');
    // others...
});

App.ApplicationRoute = Ember.Route.extend(App.Ajax, {
    events: {
        logout: function () {
            this.GET('/auth/logout').then(function (json) {
                if (json != null && json.error != null) {
                    App.set('error', json.error.message);
                }
            });
            // even if we error out, we can still clear our own record
            App.set('authToken', null);
            delete localStorage['authToken'];
            this.transitionTo('login');
        },

        dismissError: function () {
            App.set('error', null);
        }
    }
});

The login logic uses a LoginCreds model that the login template fills in, the App.Ajax mixin, and the Ajax call to our login service.

app.js
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
App.LoginCreds = Ember.Object.extend({
    username: null,
    password: null,
    remember: false,

    json: function () {
        return {
            username: this.get('username'),
            password: this.get('password'),
            remember: this.get('remember')
        }
    }
});

App.LoginController = Ember.ObjectController.extend({});
App.LoginRoute = Ember.Route.extend(App.Ajax, {
    model: function () {
        // let our login template fill in the properties of a creds object
        return App.LoginCreds.create({});
    },

    events: {
        login: function () {
            var model = this.modelFor('login'); // <App.LoginCreds>
            var self = this;
            self.GET('/auth/login', model.json()).then(
                function (json) {
                    if (json == null) return; // shouldn't happen, but should still NPE protect
                    if (json.error != null) {
                        // useful for any ajax call: set the global error alert with our error message
                        App.set('error', json.error.message);
                    } else {
                        // setting this value will reveal our logout button
                        App.set('authToken', json.authToken);
                        if (model.get('remember')) {
                            localStorage['authToken'] = json.authToken;
                        } else {
                            // make sure a stale value isn't left behind
                            delete localStorage['authToken'];
                        }
                        // clear out any login error that was left over
                        App.set('error', null);
                        self.router.transitionTo(/*<wherever you want after login>*/);
                    }
                });
        }
    }
});

That’s it! Using the App.Ajax for normal service calls is easy:

example.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
App.FooRoute = Ember.Route.extend(App.Ajax, {
    model: function () {
        // create and return model
    },

    events: {
        // handle events from the model that result in Ajax calls
        updateBar: function () {
            this.GET('/bar/update', {/* parameters */}).then(
                function (json) {
                    if (json == null) {
                        // there was an auth challenge, and we'll get set to login automatically
                        return;
                    }

                    // process result, likely updating the model and/or moving to another route
                };
        }
    }
});

comments powered by Disqus