Authentication - Full Stack Web Development with Backbone.js (2014)

Full Stack Web Development with Backbone.js (2014)

Chapter 9. Authentication

Many applications require spaces for public and private information. This often means two things: while interfaces should look different depending on who users are, server-side data must be protected from outsiders.

For example, users of the Munich Cinema application could store which movies they liked and maintain a history of favorite movies. They might also comment on other users’ choices or maintain a personal calendar for movies to watch.

For all these actions, the application needs to know who we are (authentication), and what we are allowed to do (authorization). Authentication and authorization over HTTP are closely related.

In this chapter, our goal is to understand aspects of security in browsers and the backend requirements.

We will discuss the following:

§ Security of Backbone applications

§ Principles of client-server authentication

§ Managing sessions

§ Modal dialogs for signup and login

Security in Browsers

Bringing security to web browsers is a difficult task. Ideally, we want to authenticate every HTTP request. But practically, entering passwords multiple times can often become frustrating for users. Unfortunately, browsers do not provide native support for secure sessions right now, and most authentication strategies are vulnerable to attacks.

To solve the authentication dilemma over HTTP, there are basically two approaches:

Cookies

This is the most popular, but also one of the less secure approaches to securing web applications in browsers. The main vulnerabilities of cookies are that they can be hijacked (e.g., with network sniffers) or stolen. Also, the content in cookies could be guessed by observing many of them. By putting random content in a cookie, this can be minimized, but it still can be a problem.

Signing requests

When an HTTP request from the browser is made, a parameter in a URL, or in the HTTP header of the request, is sent along. This strategy goes under the name “signing” requests. Different signing strategies for requests exist, but the general advantage is the compatibility with RESTful principles. In contrast to cookies, we don’t introduce a tight coupling between client and server. This is the most secure approach so far in web browsers, but also takes the most effort to implement.

Popular approaches for signing requests are basic auth and third-party authentication. With basic auth, a username and password are sent over the network with every request. Without using an SSL connection, basic auth is rather insecure, as attackers could see user credentials in plain text.

You should be aware of two common vulnerabilities of JavaScript applications: cross-site scripting (XSS) and cross-site request forgery (CSRF). While XSS vulnerabilities can especially occur if you allow users to submit content (such as in posts or comments in forums), CSRF is more subtle. By exploiting known links or settings in cookies, an attacker can potentially make HTTP requests on behalf of a user without her permission. Both approaches and security approaches with Backbone are described in Stephen A. Thomas’s blog post “Securing Javascript Web Apps”.

Security is improved when your application uses OAuth tokens in HTTP headers. Users are redirected to an external OAuth provider, such as Twitter or Facebook, to obtain an access token for your application. Your web application can then use this token for signing requests.

However, authentication with cookies is the easiest type to understand. Once we understand the idea, we can replace cookie authentication with signing requests for OAuth. The following section discusses this approach in more depth.

Cookies

To give users a private space in the application, we need a sense of user identity on the client and on the server. We need to introduce the idea of state being “in use,” or user sessions.

This conflicts a bit with HTTP, because HTTP is stateless, while user sessions introduce some “memory” to avoid multiple password inputs for each request.

HTTP cookies are a pragmatic solution to deal with this conflict. As can be seen in Figure 9-1, the client invokes a request to obtain the state of a session. To grant permissions to vote on a movie or display votes of the past, a session must be valid.

WARNING

Using cookies couples a browser to a server and is not very RESTful. According to RESTful principles, browsers don’t have any state, and HTTP requests should be signed to conserve the addressibility principle. For this, access tokens in an HTTP header or as a URL parameter are often used.

Cookies are stored in the browser until they expire. With every request from the client to the API, we check the cookies on the server side and compare whether a session is still valid. If not, a user needs to request a new cookie.

On first page load, we are interested in whether a user has a working session—we want to render the screen differently if he has (he can see his private information and can do more); if a user has not yet had a valid session, we need some way to provide one

Figure 9-1. On first page load, we are interested in whether a user has a working session—we want to render the screen differently if he has (he can see his private information and can do more); if a user has not yet had a valid session, we need some way to provide one

Our basic authentication is the following: before users can request a session, they must register with the site. Once they have signed up, we need to have ways for users to manage a session—specifically, a check of whether a session is (still) valid, calls to obtain a new session, and finally, a call to delete a session or logout. For this, we will set up some new API endpoints.

These API calls will provide the background in this chapter, and we’ll first discuss some possible implementations.

NOTE

A word must be said on client-side versus server-side rendering of HTML in this context. With server-side rendering, it is possible to embed the state of a session in the first page load. For example, if a valid cookie has been sent in the first request, the server could respond with HTML where data on a user is embedded. The discussion for authentication with support of server-side rendering would be different.

Signup

To create new users on the server, we need to provide the following API endpoint:

POST /api/auth/create_user

This request allows to register a new user in the system. A user should provide

us with basic credentials. If a users signs up, we get a 200, if a username is

taken a 422 is returned, and for all other problems a 500

For user signup, you can add a new route to api.js and use a promise to wrap the database access ds.createUser(…) as follows:

server.post('/api/auth/create_user', function(req, res, next) {

ds.createUser(req.body)

.then(function(user) {

res.send({id: user.id, username: user.username});

})

.error(function(err) {

res.send(422, { error: err.message });

})

.catch(function(err) {

res.send(500, { error: err });

});

});

Before looking at the details of ds.createUser(…), have a look at the different paths that the promises can take:

§ For the success path, the user is successfully created and we send back a user ID together with the user data. You could add information on authentication, such as a cookie or HTTP header, if you want to provide a direct login after signup.

§ If a username is taken, the promise is rejected, and the Backbone application must process a 422 Unprocessable Entity (WebDAV; RFC 4918).

§ When an unexpected error occurs, you send back a 500 (Internal Server Error) response, including an error message.

Now, let’s look how the createUser method wraps the database access. To keep it simple, you can write the following implementation of createUser in the data store DS.js:

var Users = [];

function _createUser(raw) {

var userId = Users.length + 1;

var newUser = {

id: userId,

username: raw.username,

password: raw.password,

email: raw.email

};

// would require DB access

Users.push(newUser);

return _returnUser(newUser);

}

With the private function _createUser(), data would be stored in a data store. In our case, we just push the new user to the Users array. Next, we only want to return the user ID and username of a user:

function _returnUser(newUser) {

return _.pick(newUser, 'username', 'id')

}

And last, before we actually create a user from an HTTP request, you should check for duplicates as follows:

function _findByUsername(username) {

var user = _.findWhere(Users, {username: username});

// _simulate_ a DB operation with time dependency

return Promise.delay(30).thenReturn(user);

}

function _checkDuplicates(raw) {

var username = raw.username;

// would require DB access

return _findByUsername(username).then(function(existingUser) {

if (existingUser) {

return Promise.RejectionError('Username taken.');

}

return raw;

});

}

Now, the createUser function is as simple as:

DS.prototype.createUser = function(req) {

var raw = JSON.parse(req.body);

return _checkDuplicates(raw)

.then(_createUser);

}

You can test the function with the following curl call:

$ curl -X POST 0.0.0.0:5001/api/auth/create_user \

-H 'content-type: application/json' \

-d '{"username": "beppo", "password": "pass", "email": "b@test.com"}'

And as response, you should see:

{"id":1}

If you run the same curl call again, you will see:

{"error":"Username taken."}

This error will be important later, when we add a validation to a Backbone user model. But we first continue with adding session management to the API.

Managing Sessions

Conceptually, when a user logs in, a cookie is set. Every request that follows carries this session information. For this, we need an API endpoint that sets up a cookie:

POST /api(auth/session

Setup a cookie if a user logs in with valid credentials

Sessions and cookies are easily confused. By definition, sessions persist until the user shuts down their browser. Cookies have an expire attribute and persist for a longer duration.

An important difference between session and cookie is that sessions can generally be trusted. Sessions are under control at the server, while information from a cookie can be manipulated or stolen.

NOTE

Cookie theft can be made more difficult by setting the http-only flag. For more details about this approach, consult Jeff Atwood’s “Protecting Your Cookies: HttpOnly”.

To provide sessions in the Backbone application, we need two more API calls. One for checking if a session exists, and one endpoint for session logout:

POST /api/auth/session

Return basic user information, such as user ID and username, if a user

successfully enters her credentials. Return a 422 if either the username

or password is missing. Return a 403 if the password is wrong. Additionally,

an 'auth' attribute is returned to identify if a HTTP session was (not)

successfully initiated

GET /api/auth/session

Checks whether we have a valid session

DELETE /api/auth/session

Logout

Next, let’s prepare the use of sessions for the Backbone app at the server side by extending the API. After a successful login, we set a cookie in the HTTP header with Set-Cookie.

For starting a user session, you want to validate the presence of user credentials only once. Therefore, you could start the route /api/auth/session in api.js as follows:

server.post('/api/auth/session', function(req, res, next) {

if (!req.body.username || !req.body.password) {

res.send(422, {status: 'err',

error: 'Username and password are two required fields.'

});

next();

}

ds.authUser(req)

.then(function(activeUser) {

res.header('Set-Cookie', 'session=' + activeUser.token

+ '; expires=Thu, 1 Aug 2030 20:00:00 UTC; path=/; HttpOnly');

res.send({ auth: "OK", id: activeUser.id,

username: activeUser.username,

email: activeUser.email });

})

.error(function(err) {

res.header('Set-Cookie', 'session=; HttpOnly')

res.send(403, { auth: "NOK", error: err.message });

})

.catch(function(err) {

console.log("/auth/session: %", err);

res.send(401, { auth: "NOK" });

})

});

As you can see, in case of the success path of the promise, an HTTP cookie is set. When the promise from ds.authUser is rejected, the HTTP cookie is cleared, and a 403 (Forbidden) response is generated.

What does the ds.authUser do? The authentication operations in ds.authUser will generally involve some database lookup and some code to compare hashes of passwords. We skip the details here, but you can see some more details on the book’s GitHub page. For the Backbone app, it is important that a unique token can be attached to users from the data store DS.js:

// data store operations to authenticate a user

function _matchPasswords(req) {

return _findByUsername(req.body.username).then(function(activeUser) {

if (activeUser && req.body.password === activeUser.password) {

return activeUser;

} else {

return Promise.RejectionError('username not found');

}

});

}

function _generateToken(activeUser) {

var token = sha1(_.now().toString()); // generate a unique token

activeUser.token = token;

return activeUser;

}

DS.prototype.authUser = function(req) {

return _matchPasswords(req).then(_generateToken);

}

Also with curl, it is possible to activate a session as follows:

$ curl -v -X POST 0.0.0.0:5001/api/auth/session \

-H 'content-type: application/json' \

-c token.txt \

-d '{"username": "beppo", "password": "pass"}'

And you should see a response similar to the following:

> POST /api/auth/session HTTP/1.1

> User-Agent: libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5

> Host: 0.0.0.0:5001

> Accept: application/json

> content-type: application/json

> Content-Length: 41

>

< HTTP/1.1 200 OK

< Set-Cookie: session=e48b670ee65a7fae0f61772172bebd2956b7ef5c;

< expires=Thu, 1 Aug 2030 20:00:00 UTC; path=/; HttpOnly

< Content-Type: application/json

< Content-Length: 60

< Access-Control-Allow-Origin: *

< Access-Control-Allow-Headers: Accept, Accept-Version, Content-Length,

< Content-MD5, Content-Type, Date, Api-Version, Response-Time

< Access-Control-Allow-Methods: POST

< Access-Control-Expose-Headers: Api-Version, Request-Id, Response-Time

< Connection: Keep-Alive

< Content-MD5: jdbl/l65DKaRSReguYM9IA==

< Date: Wed, 23 Apr 2014 19:17:13 GMT

< Server: api

< Request-Id: e0089060-cb1b-11e3-9ae3-6f4dd4cfa1a1

< Response-Time: 2

<

* Connection #0 to host 0.0.0.0 left intact

* Closing connection #0

{"auth":"OK","id":1,"username":"beppo","email":"b@test.com"}

Additionally, the curl command saved the cookie in a token.txt file that contains the following content:

# Netscape HTTP Cookie File

# http://curl.haxx.se/rfc/cookie_spec.html

# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_0.0.0.0 FALSE / FALSE 1911844800 session e48b670ee65a7fae0

Once a session is active, we need to validate a token against valid tokens from users. To do this, you add a function checkAuth in the data store DS.js as follows:

function _findUserByToken(req) {

var cookies = getCookies(req);

var user = _.findWhere(Users, { token: cookies.session });

// _simulate_ a DB operation with time dependency

return Promise.delay(30).thenReturn(user);

}

DS.prototype.checkAuth: function(req) {

return _findUserByToken(req).then(function(activeUser) {

if (!activeUser) {

return Promise.reject("No Session")

}

return _returnUser(activeUser);

});

}

There is a helper needed to parse cookies. This parser can be added in a lib folder under cookiesParser.js:

var getCookies = function(request) {

var cookies = {};

request.headers && request.headers.cookie &&

request.headers.cookie.split(';').forEach(function(cookie) {

var parts = cookie.match(/(.*?)=(.*)$/)

cookies[ parts[1].trim() ] = (parts[2] || '').trim();

});

return cookies;

};

module.exports = getCookies;

Now, let’s add an endpoint for GET /api/auth:

server.get('/api/auth/session', function(req, res, next) {

ds.checkAuth(req)

.then(function(user) {

res.send({ auth: "OK", id: user.id, username: user.username });

})

.error(function(err) {

res.header('Set-Cookie', 'session=; HttpOnly')

res.send(403, { auth: "NOK", error: err.message });

})

.catch(function(err) {

// error

res.header('Set-Cookie', 'session=; HttpOnly')

res.send(403, { auth: "NOK" });

});

});

And again you can test the idea with curl:

$ curl -v 0.0.0.0:5001/api/auth/session -b token.txt

> GET /api/auth/session HTTP/1.1

> User-Agent: libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5

> Host: 0.0.0.0:5001

> Cookie: session=182b39c7347a587d584d92545a1f1dc341754871

> Accept: application/json

>

< HTTP/1.1 200 OK

< Content-Type: application/json

< Content-Length: 39

< Access-Control-Allow-Origin: *

< Access-Control-Allow-Headers: Accept, Accept-Version, Content-Length

< Access-Control-Allow-Methods: POST, GET

< Access-Control-Expose-Headers: Api-Version, Request-Id, Response-Time

< Connection: Keep-Alive

< Content-MD5: hIFmw2EZ305jVJGz0Gupog==

< Date: Wed, 23 Apr 2014 19:36:03 GMT

< Server: api

< Request-Id: 81b05040-cb1e-11e3-bb04-517baaa76d9b

< Response-Time: 2

<

* Connection #0 to host 0.0.0.0 left intact

* Closing connection #0

{"auth":"OK","id":1,"username":"beppo"}

Last, to reset a session, the DELETE /api/auth looks as follows:

server.del('/api/auth/session', function(req, res, next) {

ds.clearSession(req)

.then(function() {

res.header('Set-Cookie', 'session=; HttpOnly')

res.send(200, {auth: 'NOK'});

});

});

The idea of clear session looks as follows:

DS.prototype.clearSession = function(req) {

return _findUserByToken(req).then(function(activeUser) {

if (activeUser) {

activeUser.auth = null;

}

return activeUser;

});

}

And if you verify the idea with curl, you can observer that all cookies will be gone:

$ curl -v -X DELETE 0.0.0.0:5001/api/auth/session -b token.txt

With these basic API endpoints for login, we continue to the corresponding Backbone actions on the client side.

Sessions with Backbone

On the client side, the API from the previous section allows us to build actions for sign up and sign in. Also, we can render views differently depending on the login state. For example, we might want to show different buttons in the header, and actions to “like” and “rate” movies, depending on whether a user is logged in.

So, let’s prepare our UI to show forms for signup and login, as well as a dynamic header with some profile information.

A Navbar View

In most web applications, session management is taken care of on the top part of the screen with a navigation view or “navbar.” In this view, you can render profile information and logout or login actions depending on the state of the user sessions.

As the navbar view manages views for signup and login, it is a kind of layout view on its own. Its rendering behavior is also different depending on whether users are logged in.

We can apply ideas to manages views as discussed in previous chapters and bind the view to a Session model and corresponding events for login and logout. Let’s walk through the code of app/views/navbar.js:

var Backbone = require('backbone');

var _ = require('underscore');

var $ = Backbone.$;

var Handlebars = require('handlebars');

var Templates = require('templates/compiledTemplates')(Handlebars);

var LoginView = require('views/login');

var JoinView = require('views/join');

var Session = require('models/session');

var NavbarView = Backbone.View.extend({

template: Templates['navbar'],

To set up the view, you require Backbone and dependencies for templating, as well as child views and a new Session model. This model will be used to talk to the API from earlier, and represents the state of a user session.

You only will need one Session instance per app, and the instance is setup in the constructor of the Navbar in app/views/navbar.js:

initialize: function() {

// make sure to keep the layout reference from the callbacks

_.bindAll(this, 'render', 'login', 'join', 'logout');

// the navbar manages a session instance as will be discussed

this.session = Session.getInstance();

// instantiate the modals:

this.loginView = new LoginView();

this.joinView = new JoinView();

// subscribe to events for login:

this.listenTo(this.session, 'login:success', this.render);

this.listenTo(this.session, 'logout:success', this.render);

}

This code makes sure that the this reference is set up correctly when callbacks for events are processed. The bindAll syntax from Underscore helps doing this. Then, you set up the Session model, the modal views for login/signup, and listen to events from the user session.

The session model decides how a Navbar is rendered, and what DOM events are bound to the view. In app/views/navbar.js, the following render function is used:

render: function() {

var session = this.session.currentUser();

this.$el.html(this.template({session: session}));

if (session) {

this.$el.delegate('.logout', 'click', this.logout);

} else {

this.$el.delegate('.login', 'click', this.login);

this.$el.delegate('.join', 'click', this.join);

}

return this;

}

Because the events hash of a Navbar would be dynamic depending on the session state, you attach the event handlers manually with this.$el.delegate(…).

Last, you need some rendering logic when a user clicks on the DOM elements for login and signup. If a user wants to logout, no feedback is rendered. So, the view callbacks for DOM events in app/views/navbar.js could become:

// the modal for login is rendered here, as will be discussed:

login: function(ev) {

ev.preventDefault();

$('body').append(this.loginView.render().el);

},

// the modal for signup is rendered here, as will be discussed:

join: function(ev) {

ev.preventDefault();

$('body').append(this.joinView.render().el);

},

logout: function(ev) {

ev.preventDefault();

this.session.logout();

},

Next, you can set up the template of the navbar view. In app/templates/navbar.hbs, you could define:

{{#if session }}

<!-- active user session -->

<a href="#" class="logout">Logout</a>

{{else}}

<!-- no user session -->

<a href="#" class="login">Login</a> |

<a href="#" class="join">Join</a>

{{/if}}

Depending on the state in the session variable, you render the template differently: for active sessions, a user is able to logout. When no session is active, a user is able to log in or sign up.

The views for joining and login are modal boxes, so let’s look at a base modal view next.

A Modal View for Sign Up

There are many different ways to design interfaces for signup, but a popular one is to break the application view with a modal dialog.

Because signup and login modal views are very similar, you can first write a modal base view in app/views/modal.js. Then, you customize the modal view behavior for the login and signup actions in app/views/join.js and app/views/login.js.

The base modal view is defined in app/views/modal.js as follows:

var Backbone = require('backbone');

var _ = require('underscore');

var $ = Backbone.$;

var ModalView = Backbone.View.extend({

className: 'ui-modal',

render: function() {

this.$el.html(this.template());

this.$el.delegate('.close', 'click', this.closeModal);

this.$error = this.$el.find('.error');

return this;

},

closeModal: function(ev) {

if (ev) ev.preventDefault();

this.$el.unbind();

this.$el.empty();

this.$el.remove();

},

initialize: function() {

_.bindAll(this, 'render', 'closeModal');

return Backbone.View.prototype.initialize.call(this);

}

});

module.exports = ModalView;

Apart from adding a CSS class name ui-modal, there is logic to render and clean up the modal view. In particular, a modal view also manages an element for showing errors with this.$error.

We are now going to adapt the base modal view for the signup action. In app/views/join.js, you can inherit from the modal view as follows:

var Backbone = require('backbone');

var ModalView = require('views/modal');

var Handlebars = require('handlebars');

var Templates = require('templates/compiledTemplates')(Handlebars);

var $ = require('jquery-untouched');

var _ = require('underscore');

var User = require('models/user');

var JoinView = ModalView.extend({

template: Templates['join'],

events: {

'submit': 'registerUser'

},

render: function() {

ModalView.prototype.render.call(this);

this.delegateEvents();

return this;

},

registerUser: function(ev) {

ev.preventDefault();

this.user.clear();

var username = $('input[name=username]').val();

var password = $('input[name=password]').val();

var email = $('input[name=email]').val();

this.user.signup({username: username, password: password, email: email});

},

initialize: function() {

this.user = new User();

return ModalView.prototype.initialize.call(this);

}

});

module.exports = JoinView;

As you can see from this code, the JoinView inherits from ModalView and extends the logic of initialize and render to account for custom events of JoinView. The registerUser takes the values from the signup form and passes the values to a User model. How the Userinstance makes the API calls will be discussed in the next section.

To complete the UI for signup, you need to include a template and add some styling. In app/templates/join.hbs, you can add the following signup form:

<div class="overlay"></div>

<div class="content">

<span class="close">close</span>

<section class="join">

<h1>Register</h1>

<div class="error"></div>

<form>

<label for="username">Username</label>

<input type="text" name="username" />

<br>

<label for="email">Email Address</label>

<input type="text" name="email" />

<br>

<label for="password">Password</label>

<input type="password" name="password" />

<br>

<input type="submit"></input>

</section>

</div>

So far, the signup form template and view can pick up data from a user. Because the user data should be subsequently passed to the application backend, let’s look at the data handling of app/models/user.js next:

var Backbone = require('backbone');

var _ = require('underscore');

var UserModel = Backbone.Model.extend({

defaults: {

username: '',

password: '',

email: ''

},

urlRoot: '/api/auth/create_user',

validate: function(attrs) {

var errors = this.errors = {};

if (!attrs.username) errors.firstname = 'username is required';

if (!attrs.email) errors.email = 'email is required';

if (!_.isEmpty(errors)) return errors;

},

signup: function(attrs) {

var that = this;

this.save(attrs, {success: function(model, response) {

that.trigger('signup:success');

},

error: function(model, response) {

var error = JSON.parse(response.responseText).error;

that.validationError = {"username": error};

that.trigger('invalid', that);

}

});

},

save: function(attrs, options) {

options || (options = {});

options.contentType = 'application/json';

options.data = JSON.stringify(attrs);

return Backbone.Model.prototype.save.call(this, attrs, options);

}

});

module.exports = UserModel;

A number of ideas are important here:

§ To synch a UserModel with a server, we reference the create_user path from the API with the urlRoot property.

§ The UserModel includes some validation logic of a Backbone model. Validating the user data is important to make sure that a user has entered all required fields in the form.

§ The signup function wraps the save function of a Backbone model. This allows you to process errors that are coming from the server.

§ The behavior of the save function is extended to pass JSON instead of text values to the server.

To improve the signup form, you can add some behavior for rendering errors in app/views/join.js:

renderError: function(err, options) {

var errors = _.map(_.keys(err.validationError), function(key) {

return err.validationError[key];

})

this.$error.text(errors);

},

renderThanks: function() {

this.$el.find('.join').html('thanks for signup');

}

And bind these callbacks to the invalid events in the constructor of app/views/join.js:

this.listenTo(this.user, 'invalid', this.renderError);

this.listenTo(this.user, 'signup:success', this.renderThanks);

A working signup form should then render feedback on a successful signup or about possible problems, as shown in Figure 9-2.

The view for signup is a custom modal view and handles errors from the validation of the UserModel

Figure 9-2. The view for signup is a custom modal view and handles errors from the validation of the UserModel

The Login Dialog

Similar to the modal view for signup, you can continue with a view for the login. Let’s start with a template in app/templates/join.js this time:

<div class="overlay"></div>

<div class="content">

<span class="close">close</span>

<h2>Login</h2>

<div class="error"></div>

<form id="login">

<label for="username">

Username:

</label>

<input name="username" />

<br>

<label for="password">

Password:

</label>

<input type="password" name="password" />

<br>

<input type="submit"></input>

</form>

</div>

This can be wired up to a modal dialog similar to the registration form. In app/views/login.js, you can add:

var ModalView = require('views/modal');

var Handlebars = require('handlebars');

var Templates = require('templates/compiledTemplates')(Handlebars);

var $ = require('jquery-untouched');

var _ = require('underscore');

var Session = require('models/session');

var LoginView = ModalView.extend({

template: Templates['login'],

events: {

'submit': 'login'

},

render: function() {

ModalView.prototype.render.call(this);

this.delegateEvents();

this.$error = this.$el.find('.error');

return this;

},

login: function(ev) {

ev.preventDefault();

var username = $('input[name=username]').val();

var password = $('input[name=password]').val();

// ... login action

var that = this;

Session.getInstance().login(username, password);

},

initialize: function() {

this.session = Session.getInstance();

this.listenTo(this.session, 'login:success', this.closeModal);

return ModalView.prototype.initialize.call(this);

}

});

module.exports = LoginView;

In contrast to the view for signup, the LoginView directly manages the user session with a Session instance. How the state of a session is tracked will be discussed in the next section. Let’s continue first with working on the view for activating a session.

The Session Logic

Now that we have a user interface for creating sessions, let’s continue with the logic to activate a user model.

A New Session

For login, all we need to do is fetch a cookie from the server. You can call the authentication API directly from the Session model in app/models/session.js:

login: function(username, password) {

var that = this;

var credentials = JSON.stringify({username: username,

password: password });

$.ajax({type: 'POST', dataType: 'json',

contentType: "application/json",

url: "/api/auth/session",

data: credentials})

.done(function(data) {

that.user = new User(data);

that.trigger('login:success');

})

.fail(function(response) {

var error = JSON.parse(response.responseText).error;

console.log(error);

that.validationError = {"username": error};

that.trigger('invalid', that);

});

}

The paths through the promises can handle a successful login or the error from a failed login. In the case that our server responds with a “successful” authentication, we also set the user data from the backend.

State of a Session

Addtionally, there must be some logic to get the current state of a session in app/models/session.js. Often, this also means we will need to retrieve some information about the user.

A simple check whether we have a valid user session might be:

currentUser: function() {

// ... retrieve currentUser if authenticated

if (this.user && (this.user.get('auth') == 'OK')) {

return this.user;

} else {

return false;

}

}

First, we check if there is a user. Then, we check if the user hasn’t logged out by retrieving the auth attribute.

Still, we have a problem. When the user reloads the page or visits the page on the next day, we might want to check in the background for a valid cookie. As long as we work with a static HTML, we need to process an extra API call.

So far we haven’t used the Session model’s fetch function so we can do it now as follows in app/models/session.js:

var deferred = this.fetch();

var self = this;

deferred.done(function(data) {

self.user = new User(data);

});

deferred.fail(function() {

self.user = null;

});

Now, we have enough logic to start personal conversations with users. We still need some logic to log out.

Logout

Technically, the logout should do the inverse of the login. That means killing a cookie and removing the session from the server:

logout: function() {

// ... delete a session

var that = this;

$.ajax({type: 'DELETE', dataType: 'json',

contentType: 'application/json',

url: '/api/auth/session' })

.done(function(data) {

that.user.set('auth', 'NOK');

that.trigger('logout:success');

})

}

Conclusion

This chapter started out with some conceptual work needed to create security in web browsers. Cookies provide a pragmatic solution to make HTTP sessions safe and give a good foundation for exploring further authentication work. You saw a number of HTTP requests with curl to experiment with authentication from the command line.

We then built a user interface that allows users to sign up and log in. These views are based on a common modal view. The JoinView manages a UserModel, while the LoginView manages a Session model, where the state of a session can be resolved.

With these new routes for authentication, you could go ahead and add more functions to voting on movies or maybe commenting. Because the application is evolving more and more into a full, single-page web application, a look at frameworks on top of Backbone might become interesting to you. To prepare you for using a Backbone framework, we will discuss workflow automation in the next chapter.