Let's implement the Basic Auth protocol in Contacts App
. As you have learned in the previous sections, you will need to add the Authorization
header for every request that you make to the server in order to be authenticated. From the server side, you will need to read and parse this header.
A useful npm
package to decode the Authorization
header has been developed. With the basic-auth
module, you can read the request headers and return an object with two fields: name
and pass
, these fields can be used to authenticate the user. For simplicity, we will use a hardcoded user and password, not a real database:
// server/basicAuthMiddleware.js var basicAuth = require('basic-auth'); var authorizationRequired = function (req, res, next) { var credentials = basicAuth(req) || {}; if (credentials.name === 'john' && credentials.pass === 'doe') { return next(); } else { return res.sendStatus(401); } }; module.exports = authorizationRequired;
The middleware checks whether the user is john
and the password is doe
. If not, an HTTP 401
error will be sent to the client. You can use the middleware for each resources that you want to protect:
var controller = require('./controller'); var authorizationRequired = require('./basicAuthMiddleware'); module.exports = routes = function(server) { server.post('/api/contacts', authorizationRequired, controller.createContact); server.get('/api/contacts', authorizationRequired, controller.showContacts); server.get('/api/contacts/:contactId', authorizationRequired, controller.findContactById); server.put('/api/contacts/:contactId', authorizationRequired, controller.updateContact); server.delete('/api/contacts/:contactId', authorizationRequired, controller.deleteContact); server.post('/api/contacts/:contactId/avatar', authorizationRequired, controller.uploadAvatar); };
The WWW-Authenticate
header that we include in the HTTP 401 response will make sure that the browser prompts a dialog box asking you for a user and password. You can use the john
user and the doe
password in the dialog, then the browser will build and send the Authentication header for you:
To have more control over how to ask for authentication, you can create a form
view and add some routes for authentication purposes:
<div class="col-xs-12 col-sm-offset-4 col-sm-4"> <div class="panel"> <div class="panel-body"> <h4> Login required </h4> <p> Use 'john' as user and 'doe' as password. </p> <form> <div class="form-group"> <label for="username">User</label> <input type="user" class="form-control" id="username" placeholder="Username"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" id="password" placeholder="Password"> </div> <p id="message" class="pull-left"></p> <button type="submit" class="btn btn-primary pull-right">Login</button> </form> </div> </div> </div>
The LoginView
method should handle the authentication process when the user clicks the Login button:
// apps/login/views/loginView.js 'use strict'; var Common = require('../../../common'); var template = require('../templates/login.tpl'); class LoginView extends Common.ModelView { constructor(options) { super(options); this.template = template; } get className() { return 'row'; } get events() { return { 'click button': 'makeLogin' }; } makeLogin(event) { event.preventDefault(); var username = this.$el.find('#username').val(); var password = this.$el.find('#password').val(); console.log('Will login the user', username, 'with password', password); } } module.exports = LoginView;
A new route should be added to show the #/login
form:
// apps/login/router.js 'use strict'; var Backbone = require('backbone'); var LoginView = require('./views/loginView'); class LoginRouter extends Backbone.Router { constructor(options) { super(options); this.routes = { 'login': 'showLogin' }; this._bindRoutes(); } showLogin() { var App = require('../../app'); var login = new LoginView(); App.mainRegion.show(login); } } module.exports = new LoginRouter();
You will need to include this new router when the application bootstraps, as follows:
// app.js // ... // Initialize all available routes require('./apps/contacts/router'); require('./apps/login/router'); // ...
When an unauthenticated user accesses the #/contacts
route, Backbone Application should redirect them to the login form:
Backbone.$.ajaxSetup({ statusCode: { 401: () =>{ window.location.replace('/#login'); } } });
When the server responds with an HTTP 401, it means that the user is not authenticated and you then can show the login window. Remember to remove the WWW-Authenticate
response header in order to prevent the browser from showing its login dialog:
function unauthorized(res) { // res.set('WWW-Authenticate', 'Basic realm=Authorization Required'); return res.sendStatus(401); };
Now that we have a login form in place, we can put the authentication code in it. That's going to be done in the following three steps:
The authentication string is easy to build, you can use the btoa()
function to convert strings to base64
, as follows:
class LoginView extends Common.ModelView { // ... makeLogin(event) { event.preventDefault(); var username = this.$el.find('#username').val(); var password = this.$el.find('#password').val(); var authString = this.buildAuthString( username, password ); console.log('Will use', authString); } buildAuthString(username, password) { return btoa(username + ':' + password); } }
Then, you can use authString
to test whether can get the contacts resource successfully. If the server answers successfully, then the user is using the right credentials:
class LoginView extends Common.ModelView { // ... makeLogin(event) { event.preventDefault(); var username = this.$el.find('#username').val(); var password = this.$el.find('#password').val(); var authString = this.buildAuthString( username, password ); Backbone.$.ajax({ url: '/api/contacts', headers: { Authorization: 'Basic ' + authString }, success: () => { var App = require('../../../app'); App.router.navigate('contacts', true); }, error: jqxhr => { if (jqxhr.status === 401) { this.showError('User/Password are not valid'); } else { this.showError('Oops... Unknown error happens'); } } }); } buildAuthString(username, password) { return btoa(username + ':' + password); } showError(message) { this.$('#message').html(message); } }
If the Authentication string is valid, then the user is redirected to the contact list; however, the redirection will not work as expected as the Authorization
header in the contact list is not sent. Remember that you should send the Authorization header for every request.
You will need to save the Authentication
string in sessionStorage
to be used in future requests. The sessionStorage
is similar to localStorage
; however, in sessionStorage
, the data will be removed when the browser is closed:
class LoginView extends Common.ModelView { // ... makeLogin(event) { // ... Backbone.$.ajax({ url: '/api/contacts', headers: { Authorization: 'Basic ' + authString }, success: () => { var App = require('../../../app'); App.saveAuth('Basic', authSting); App.router.navigate('contacts', true); }, error: jqxhr => { if (jqxhr.status === 401) { this.showError('User/Password are not valid'); } else { this.showError('Oops... Unknown error happens'); } } }); } // ... }
The App
object will be responsible for storing the token:
// app.js var App = { // ... // Save an authentication token saveAuth(type, token) { var authConfig = type + ':' + token; sessionStorage.setItem('auth', authConfig); this.setAuth(type, token); }, // ... }
After the token is saved in sessionStorage
, you should include the Authorization
header for every future request:
// app.js var App = { // ... // Set an authorization token setAuth(type, token) { var authString = type + ' ' + token; this.setupAjax(authString); }, // Set Authorization header for authentication setupAjax(authString) { var headers = {}; if (authString) { headers = { Authorization: authString }; } Backbone.$.ajaxSetup({ statusCode: { 401: () => { App.router.navigate('login', true); } }, headers: headers }); } // ... }
When the application is bootstrapped, it should look whether there is an active session open; if so, it should use the session, as shown in the following:
// app.js var App = { start() { // The common place where sub-applications will be showed App.mainRegion = new Region({el: '#main'}); this.initializePlugins(); // Load authentication data this.initializeAuth(); // Create a global router to enable sub-applications // to redirect to // other URLs App.router = new DefaultRouter(); Backbone.history.start(); }, // ... // Load authorization data from sessionStorage initializeAuth() { var authConfig = sessionStorage.getItem('auth'); if (!authConfig) { return window.location.replace('/#login'); } var splittedAuth = authConfig.split(':'); var type = splittedAuth[0]; var token = splittedAuth[1]; this.setAuth(type, token); }, // ... }
The user should be able to log out. Let's add a route for the user to log out in the App router:
// app.js // General routes non sub-application dependant class DefaultRouter extends Backbone.Router { constructor(options) { super(options); this.routes = { '': 'defaultRoute', 'logout': 'logout' }; this._bindRoutes(); } // Redirect to contacts app by default defaultRoute() { this.navigate('contacts', true); } // Drop session data logout() { App.dropAuth(); this.navigate('login', true); } }
The session is removed when the auth
string is removed from sessionStorage
and the Authentication header is not sent anymore:
var App = { // ... // Remove authorization token dropAuth() { sessionStorage.removeItem('auth'); this.setupAjax(null); }, // … }
That's how you can implement authorization with the HTTP Basic Auth protocol. An authorization string is generated and attached for every request made to the server, that's done with the help of the ajaxSetup()
method of jQuery. In the following section, we will see how to implement the OAuth2 protocol.
18.119.103.204