Cookie-based Authentication in AngularJS

 

Single Page Apps are ruling the world and AngularJS is leading the charge. But many of the lessons we learned in the Web 2.0 era no longer apply, and few are as drastically different as authentication.

 

Depending on where you build and release your AngularJS app, there are different ways to handle authentication and I hope this post clears some of that up.

 

CORS

 

CORS is an oft-misunderstood feature of new browsers that is configured by a remote server. CORS stands for Cross-Origin-Resource-Sharing, and was designed to make it possible to access services outside of the current origin (or domain) of the current page.

 

Like many browser features, CORS works because we all agree that it works. So all major browsers like Chrome, Firefox, and IE support and enforce it. By using these browsers, you benefit from the security of CORS.

 

That means certain browsers do not enforce it, so it is not relevant there. One large example is a native Web View for things like Cordova and Phonegap (and hence, Ionic apps). However, these tools often have configuration options for whitelisting domains so you can add some security that way.

 

Configuring CORS on the Server

 

The way CORS works is the server decides which domains it will accept as clients. This means an open API like Twitter might allow any clients, or a closed API might decide to only allow access from the domain of the running client app.

 

I won't get into the details of configuring CORS on the server side, but it's really just setting some headers. Here's how you might do it in nginx.

 

There is one header in particular you must have if you want to do session based authentication in a single page app: Access-Control-Allow-Credentials: true which we show next.

 

AngularJS Auth

 

If you use the standard $http service to access remote APIs, it will Just Work as long as the server is configured to allow requests from your domain and you don't need to store cookies.

 

But for many applications, we also need to set and store cookie information for things like logins. By default this is not allowed in most browsers and you'll be smashing your head wondering why the cookie information isn't being saved!

 

Enter: withCredentials. withCredentials is a flag set on a low-level XMLHttpRequest (AJAX) object, but in Angular we can configure our $http requests to set this flag for everything by doing:

 

angular.module('myApp')

.config(['$httpProvider', function($httpProvider) {
  $httpProvider.defaults.withCredentials = true;
}])

 

CSRF

 

Many servers use CSRF as a security feature and you can certainly keep this feature in a hybrid app if you wish. CSRF is a way to ensure the client making a request is the same one that the server expects to make the request. This keeps someone from sniffing your cookie session data and making requests pretending to be you (and changing your password, for example).

 

To make for CSRF, we can tell $http to set the correct header for CSRF (might depend on your server framework, this one is for Django) using the specific cookie name:

 

angular.module('myApp', ['ngCookies'])

.run(['$http', '$cookies', function($http, $cookies) {
  $http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
}]);

 

While I've found this to work pretty well, if the CSRF token changes mid-session (for example if a new user signs up) the token won't update. To fix this, we can use an HTTP Interceptor to always set the correct CSRF header value before each request:

 

angular.module('myApp')

.provider('myCSRF',[function(){
  var headerName = 'X-CSRFToken';
  var cookieName = 'csrftoken';
  var allowedMethods = ['GET'];

  this.setHeaderName = function(n) {
    headerName = n;
  }
  this.setCookieName = function(n) {
    cookieName = n;
  }
  this.setAllowedMethods = function(n) {
    allowedMethods = n;
  }
  this.$get = ['$cookies', function($cookies){
    return {
      'request': function(config) {
        if(allowedMethods.indexOf(config.method) === -1) {
          // do something on success
          config.headers[headerName] = $cookies[cookieName];
        }
        return config;
      }
    }
  }];
}]).config(function($httpProvider) {
  $httpProvider.interceptors.push('myCSRF');
});

 

This will set the CSRF request header to the current value of the CSRF cookie for any request type not in allowedMethods.

 

Credentials and CORS

 

One thing to note when using withCredentials: true in your app and configuring the server for CORS is that you may not have your Access-Control-Allow-Origin header set to '*'. It must be configured to a few select origins. If you absolutely must have this set to *, then I suggest doing something beyond cookie based authentication, such as token-based authentication.

 

Token Auth

 

The methods described above work for cookie-based authentication that is common in most server-side setups. However, some APIs expect HTTP Basic Authentication or use a token-based system.

 

While the correct use of CORS will avoid cross-domain pitfalls of cookie-based authentication, those methods may be a better fit for your use case. In that case, take a look at this great post on token authentication with AngularJS.

 

We may revisit this topic in the future to add our thoughts on Token-based authentication.

(Help us change mobile development forever. We are hiring!)

 

默认情况下widthCredentials为false,我们需要设置widthCredentials为true:

 

Access-Control-Allow-Credentials: true

如果服务端不设置响应头,响应会被忽略不可用;同时,服务端需指定一个域名(Access-Control-Allow-Origin:www.zawaliang.com),而不能使用泛型(Access-Control-Allow-Origin: *)

有一点需要注意,设置了widthCredentials为true的请求中会包含远程域的所有cookie,但这些cookie仍然遵循同源策略,所以你是访问不了这些cookie的。

 支持CORS的服务器必须在响应中加入几个访问控制相关的头。

     Access-Control-Allow-Origin

这个头的值可以是与请求头的值相呼应的值,也可以是*,从而允许接收从任何来源发来的请求。

     Access-Control-Allow-Credentials(可选)

默认情况下,CORS请求不会发送cookie。如果服务器返回了这个头,那么就可以通过将
withCredentials设置为true来将cookie同请求一同发送出去。
如果将$http发送的请求中的withCredentials设置为true,但服务器没有返回Access-
Control-Allow-Credentials,请求就会失败,反之亦然。
后端服务器必须能处理OPTIONS方法的HTTP请求。

 

Cookies vs Tokens. Getting auth right with Angular.JS

 

Introduction

There are basically two different ways of implementing server side authentication for apps with a frontend and an API:

  • The most adopted one, is Cookie-Based Authentication (you can find an example here) that uses server side cookies to authenticate the user on every request.

  • A newer approach, Token-Based Authentication, relies on a signed token that is sent to the server on each request.

Token based vs. Cookie based

The following diagram explains how both of these methods work.

What are the benefits of using a token-based approach?

  • Cross-domain / CORS: cookies + CORS don't play well across different domains. A token-based approach allows you to make AJAX calls to any server, on any domain because you use an HTTP header to transmit the user information.
  • Stateless (a.k.a. Server side scalability): there is no need to keep a session store, the token is a self-contanined entity that conveys all the user information. The rest of the state lives in cookies or local storage on the client side.
  • CDN: you can serve all the assets of your app from a CDN (e.g. javascript, HTML, images, etc.), and your server side is just the API.
  • Decoupling: you are not tied to a particular authentication scheme. The token might be generated anywhere, hence your API can be called from anywhere with a single way of authenticating those calls.
  • Mobile ready: when you start working on a native platform (iOS, Android, Windows 8, etc.) cookies are not ideal when consuming a secure API (you have to deal with cookie containers). Adopting a token-based approach simplifies this a lot.
  • CSRF: since you are not relying on cookies, you don't need to protect against cross site requests (e.g. it would not be possible to <iframe> your site, generate a POST request and re-use the existing authentication cookie because there will be none).
  • Performance: we are not presenting any hard perf benchmarks here, but a network roundtrip (e.g. finding a session on database) is likely to take more time than calculating an HMACSHA256 to validate a token and parsing its contents.
  • Login page is not an special case: If you are using Protractor to write your functional tests, you don't need to handle any special case for login.
  • Standard-based: your API could accepts a standard JSON Web Token (JWT). This is a standard and there are multiple backend libraries (.NET, Ruby, Java, Python, PHP) and companies backing their infrastructure (e.g. Firebase, Google, Microsoft). As an example, Firebase allows their customers to use any authentication mechanism, as long as you generate a JWT with certain pre-defined properties, and signed with the shared secret to call their API.

What's JSON Web Token? JSON Web Token (JWT, pronounced jot) is a relatively new token format used in space-constrained environments such as HTTP Authorization headers. JWT is architected as a method for transferring security claims based between parties.

Implementation

Asuming you have a node.js app, below you can find the components of this architecture.

Server Side

Let's start by installing express-jwt and jsonwebtoken:

$ npm install express-jwt jsonwebtoken

Configure the express middleware to protect every call to /api.

var expressJwt = require('express-jwt');
var jwt = require('jsonwebtoken');

// We are going to protect /api routes with JWT
app.use('/api', expressJwt({secret: secret}));

app.use(express.json());
app.use(express.urlencoded());

The angular app will perform a POST through AJAX with the user's credentials:

app.post('/authenticate', function (req, res) {
  //TODO validate req.body.username and req.body.password
  //if is invalid, return 401
  if (!(req.body.username === 'john.doe' && req.body.password === 'foobar')) {
    res.send(401, 'Wrong user or password');
    return;
  }

  var profile = {
    first_name: 'John',
    last_name: 'Doe',
    email: '[email protected]',
    id: 123
  };

  // We are sending the profile inside the token
  var token = jwt.sign(profile, secret, { expiresInMinutes: 60*5 });

  res.json({ token: token });
});

GET'ing a resource named /api/restricted is straight forward. Notice that the credentials check is performed by the expressJwt middleware.

app.get('/api/restricted', function (req, res) {
  console.log('user ' + req.user.email + ' is calling /api/restricted');
  res.json({
    name: 'foo'
  });
});

Angular Side

The first step on the client side using AngularJS is to retrieve the JWT Token. In order to do that we will need user credentials. We will start by creating a view with a form where the user can input its username and password.

<div ng-controller="UserCtrl">
  <span></span>
  <form ng-submit="submit()">
    <input ng-model="user.username" type="text" name="user" placeholder="Username" />
    <input ng-model="user.password" type="password" name="pass" placeholder="Password" />
    <input type="submit" value="Login" />
  </form>
</div>

And a controller where to handle the submit action:

myApp.controller('UserCtrl', function ($scope, $http, $window) {
  $scope.user = {username: 'john.doe', password: 'foobar'};
  $scope.message = '';
  $scope.submit = function () {
    $http
      .post('/authenticate', $scope.user)
      .success(function (data, status, headers, config) {
        $window.sessionStorage.token = data.token;
        $scope.message = 'Welcome';
      })
      .error(function (data, status, headers, config) {
        // Erase the token if the user fails to log in
        delete $window.sessionStorage.token;

        // Handle login errors here
        $scope.message = 'Error: Invalid user or password';
      });
  };
});

Now we have the JWT saved on sessionStorage. If the token is set, we are going to set the Authorization header for every outgoing request done using $http. As value part of that header we are going to use Bearer <token>.

sessionStorage: Although is not supported in all browsers (you can use a polyfill) is a good idea to use it instead of cookies ($cookies, $cookieStore) and localStorage: The data persisted there lives until the browser tab is closed.

myApp.factory('authInterceptor', function ($rootScope, $q, $window) {
  return {
    request: function (config) {
      config.headers = config.headers || {};
      if ($window.sessionStorage.token) {
        config.headers.Authorization = 'Bearer ' + $window.sessionStorage.token;
      }
      return config;
    },
    response: function (response) {
      if (response.status === 401) {
        // handle the case where the user is not authenticated
      }
      return response || $q.when(response);
    }
  };
});

myApp.config(function ($httpProvider) {
  $httpProvider.interceptors.push('authInterceptor');
});

After that, we can send a request to a restricted resource:

$http({url: '/api/restricted', method: 'GET'})
.success(function (data, status, headers, config) {
  console.log(data.name); // Should log 'foo'
});

The server logged to the console:

user [email protected] is calling /api/restricted

The source code is here together with an AngularJS seed app.

What's next?

In upcoming posts we will revisit:

  • How to handle social authentication?
  • How to handle session expiration?

UPDATE: we published two new blog posts

Bottom Line

When building Single Page Applications, consider using a token-based authentication design over cookie-based authentication. Leave a comment or discuss on HN.

Aside: how it works with Auth0?

Auth0 issue JSON Web Tokens on every login. That means that you can have a solid identity infrastructure, including Single Sign On, User Management, support for Social, Enterprise and your own database of users with just a few lines of code. We implemented a tight integration with Angular: https://github.com/auth0/auth0-angular

你可能感兴趣的:(Authentication)