Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Ember authentication without ember-data or ember-auth.

I've been getting my head wrapped around Ember all week, and the biggest problem I kept coming back to is that I couldn't figure out how to accept errors from the server and then route to login when appropriate in a general way. Ember really wants you to only change route from within a Route, and it does not make it easy to get the current Route from elsewhere. This appears to be because if something happens to screw with routing at the wrong time, it's a Badness, so we have to work from within those restrictions.

Long story short, I tried out ember-data and ember-auth and found both a bit too alpha/beta for my tastes at the moment. I'm sure they'll be awesome in the long run, but for now there are enough frustrating quirks and annoyances with what I was trying to do that I decided to roll my own. An excellent writeup by Robin Ward on using ember without ember-data that helped me make that decision.

With this app structure in place, any Route that uses App.Ajax and utilizes the GET or POST functions will respond correctly to an auth challenge from the server, by redirecting the user to the 'login' route. Always remember to protect your data: if a service call requires authenticated access, return an authentication challenge from the server. Never trust that the app is sending you kosher data. It is really easy to screw with the internal state of an Ember app (or any JS app, really) through the browser console.

The templates are structured to look nice by using Twitter's Bootstrap CSS. I am using Ember 1.0.0-rc4 (way too many guides leave this off, and Ember has changed a ton in the last year... reading a guide for an old version is an excellent way to waste many hours, as my last week will show). Last updated 2013-05-31.

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
});
// 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));
});
}
});
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);
}
}
});
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 Dashboard.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>*/);
}
});
}
}
});
<!-- 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>
<!-- '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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment