Storing credentials in a REST environment

This recipe presents a solution for storing credentials in RESTful applications.

Getting ready

The solution is a compromise between temporary client-side storage and permanent server-side storage.

On the client side, we are using HTML5 session storage to store temporarily the usernames and passwords encoded in base 64. On the server side, only hashes are stored for passwords. Those hashes are created with passwordEncoder. This passwordEncoder is registered in Spring Security, autowired, and used in the UserDetailsService implementation.

How to do it...

Client side (AngularJS)

  1. We have made use of the HTML5 sessionStorage attribute. The main change has been the creation of a httpAuth factory. Presented in the http_authorized.js file, this factory is a wrapper around $http to take care transparently of client-side storage and authentication headers. The code for this factory is as follows:
    cloudStreetMarketApp.factory("httpAuth", function ($http) {
      return {
        clearSession: function () {
          var authBasicItem = sessionStorage.getItem('basicHeaderCSM');
          var oAuthSpiItem = sessionStorage.getItem('oAuthSpiCSM');
        if(authBasicItem || oAuthSpiItem){
          sessionStorage.removeItem('basicHeaderCSM');
          sessionStorage.removeItem('oAuthSpiCSM');
          sessionStorage.removeItem('authenticatedCSM');
          $http.defaults.headers.common.Authorization = undefined;
          $http.defaults.headers.common.Spi = undefined;
          $http.defaults.headers.common.OAuthProvider = undefined;
      }
      },
      refresh: function(){
        var authBasicItem = sessionStorage.getItem('basicHeaderCSM');
        var oAuthSpiItem = sessionStorage.getItem('oAuthSpiCSM');
        if(authBasicItem){
          $http.defaults.headers.common.Authorization = 
          $.parseJSON(authBasicItem).Authorization;
          }
          if(oAuthSpiItem){
            $http.defaults.headers.common.Spi = oAuthSpiItem;
            $http.defaults.headers.common.OAuthProvider = "yahoo";
        }
        },
        setCredentials: function (login, password) {
        //Encodes in base 64
        var encodedData = window.btoa(login+":"+password);
        var basicAuthToken = 'Basic '+encodedData;
        var header = {Authorization: basicAuthToken};
        sessionStorage.setItem('basicHeaderCSM', JSON.stringify(header));
        $http.defaults.headers.common.Authorization = basicAuthToken;
      },
      setSession: function(attributeName, attributeValue) {
        sessionStorage.setItem(attributeName, attributeValue);
      },
      getSession: function (attributeName) {
        return sessionStorage.getItem(attributeName);
      },
      post: function (url, body) {
        this.refresh();
      return $http.post(url, body);
      },
      post: function (url, body, headers, data) {
        this.refresh();
        return $http.post(url, body, headers, data);
      },
      get: function (url) {
        this.refresh();
        return $http.get(url);
      },
      isUserAuthenticated: function () {
        var authBasicItem = sessionStorage.getItem('authenticatedCSM');
      if(authBasicItem){
        return true;
        }
      return false;
      }
    }});
  2. This factory is invoked everywhere (or almost) in the former place of $http to pass and handle transparently the credentials or identification headers required for AJAX requests.
  3. We have avoided dealing directly with the sessionStorage attribute from the different controllers, in order to prevent being tightly coupled with this storage solution.
  4. The account_management.js file regroups different controllers (LoginByUsernameAndPasswordController, createNewAccountController, and OAuth2Controller) that store credentials and provider IDs in sessionStorage through httpAuth.
  5. A couple of factories have also been modified to pull and push data through the httpAuth factory. For example, the indiceTableFactory (from home_financial_table.js) requests the indices of a market with credentials handled transparently:
    cloudStreetMarketApp.factory("indicesTableFactory", function (httpAuth) {
        return {
            get: function (market) {
            return httpAuth.get("/api/indices/" + market + ".json?ps=6");
            }
        }
    });

Server side

  1. We have declared a passwordEncoder bean in security-config.xml (in the cloudstreetmarket-core module):
    <bean id="passwordEncoder" 
      class="org.sfw.security.crypto.bcrypt.BCryptPasswordEnco  der"/>
  2. In security-config.xml, a reference to the password-encoder is made, as follows, in our authenticationProvider to.
    <security:authentication-manager alias"="authenticationManager">
      <security:authentication-provider user-service-ref='communityServiceImpl'>
        <security:password-encoder ref="passwordEncoder"/>
      </security:authentication-provider>
    </security:authentication-manager>
  3. The passwordEncoder bean is autowired in CommunityServiceImpl (our UserDetailsService implementation). Passwords are hashed here with passwordEncoder when accounts are registered. The stored hash is then compared to the user-submitted password when the user attempts to log in. The CommunityServiceImpl code is as follows:
    @Service(value="communityServiceImpl")
    @Transactional(propagation = Propagation.REQUIRED)
    public class CommunityServiceImpl implements CommunityService {
      @Autowired
      private ActionRepository actionRepository;	
      ...
      @Autowired
      private PasswordEncoder passwordEncoder;
      ...
      @Override
      public User createUser(User user, Role role) {
        if(findByUserName(user.getUsername()) != null){
          throw new ConstraintViolationException("The provided user name already exists!", null, null);
         }
        user.addAuthority(new Authority(user, role));
        user.addAction(new AccountActivity(user, UserActivityType.REGISTER, new Date()));
        user.setPassword(passwordEncoder. encode(user.getPassword()));
        return userRepository.save(user);
      }
      @Override
      public User identifyUser(User user) {
        Preconditions.checkArgument(user.getPassword() != null, "The provided password cannot be null!");
       Preconditions.checkArgument( StringUtils.isNotBlank(user.getPassword()), "The provided password cannot be empty!");
        User retreivedUser = userRepository.findByUsername(user.getUsername());
        if(!passwordEncoder.matches(user.getPassword(), retreivedUser.getPassword())){
          throw new BadCredentialsException"("No match has been found with the provided credentials!");
        }
      return retreivedUser;
      }
      ...
    }
  4. Our ConnectionFactory implementation SocialUserConnectionRepositoryImpl is instantiated in SocialUserServiceImpl with an instance of the Spring TextEncryptor. This gives the possibility to encrypt the stored connection-data for OAuth2 (most importantly, the access-tokens and refresh-tokens). At the moment, this data is not encrypted in our code.

How it works...

In this chapter, wetried to maintain the statelessness of our RESTful API for the benefits it provides (scalability, easy deployment, fault tolerance, and so on).

Authenticating for Microservices

Staying stateless matches a key concept of Microservices: the self-sufficiency of our modules. We won't be using sticky sessions for scalability. When a state is maintained, it is only by the client, keeping for a limited time the user's identifier and/or his credentials.

Another key concept of Microservices is the concept of limited and identified responsibilities (horizontal scalability). Our design supports this principle even if the size of the application doesn't require domain segmentation. We can fully imagine splitting our API by domains (community, indices and stocks, monitoring, and so on). Spring Security, which is located in the core-module, would be embedded in every API war without any problem.

Let's focus on how a state is maintained on the client side. We offer to our users two ways of signing-in: using a BASIC scheme or using OAuth2.

  • A user can register his account for a BASIC authentication and then later decide to sign-in using OAuth2 (to do so, he has to bind his social account to his existing account).
  • Alternatively, a user can register his account with OAuth2 and later sign in with a BASIC form. His OAuth2 credentials will naturally be bound to his authentication.

Using the BASIC authentication

When a user registers an account, he defines a username and a password. These credentials are stored using the httpAuth factory and the setCredentials method.

In the account_management.js file and especially in the createNewAccountController (invoked through the create_account_modal.html modal), the setCredentials call can be found in the success handler of the createAccount method:

httpAuth.setCredentials($scope.form.username, $scope.form.password);

Right now, this method uses HTML5 sessionStorage as storage device:

setCredentials: function (login, password) {
  var encodedData = window.btoa(login"+":"+password);
  var basicAuthToken = 'Basic '+encodedData;
  var header = {Authorization: basicAuthToken};
  sessionStorage.setItem('basicHeaderCSM', JSON.stringify(header));
  $http.defaults.headers.common.Authorization = basicAuthToken;
}

The window.btoa(...) function encodes in base 64 the provided String. The $httpProvider.defaults.headers configuration object is also added a new header which will potentially be used by the next AJAX request.

When a user signs in using the BASIC form (see also the account_management.js and especially the LoginByUsernameAndPasswordController that is invoked from the auth_modal.html modal), the username and password are stored using the same method:

httpAuth.setCredentials($scope.form.username, $scope.form.password);

Now with the httpAuth abstraction layer the angular $http service, we make sure that the Authorization header is set in each call to the API that is made using $http.

Using the BASIC authentication

Using OAuth2

Initiated from auth_modal.html, signing in using OAuth2 creates a POST HTTP request to the API handler /api/signin/yahoo (this handler is located in the abstracted ProviderSignInController).

The sign in request is redirected to the Yahoo! authentication screens. The whole page goes to Yahoo! until completion. When the API ultimately redirects the request to the home page of the portal, a spi request parameter is added: http://cloudstreetmarket.com/portal/index?spi=F2YY6VNSXIU7CTAUB2A6U6KD7E

This spi parameter is the Yahoo! user ID (GUID). It is caught by the DefaultController (cloudstreetmarket-webapp) and injected into the model:

@RequestMapping(value="/*", method={RequestMethod.GET,RequestMethod.HEAD})
public String fallback(Model model, @RequestParam(value="spi", required=false) String spi) {
  if(StringUtils.isNotBlank(spi)){
    model.addAttribute("spi", spi);
  }
  return "index";
}

The index.jsp file renders the value directly in the top menu's DOM:

<div id="spi" class="hide">${spi}</div>

When the menuController (bound to the top menu) initializes itself, this value is read and stored in sessionStorage:

$scope.init = function () {
  if($('#spi').text()){
    httpAuth.setSession('oAuthSpiCSM', $('#spi').text());
  }
}

In our httpAuth factory (http_authorized.js), the refresh() method that is invoked before every single call to the API checks if this value is present and add two extra headers: Spi with the GUID value and the OAuthProvider (yahoo in our case). The code is as follows:

refresh: function(){
  var authBasicItem = sessionStorage.getItem('basicHeaderCSM');
  var oAuthSpiItem = sessionStorage.getItem('oAuthSpiCSM');
  if(authBasicItem){
    $http.defaults.headers.common.Authorization = $.parseJSON(authBasicItem).Authorization;
  }
  if(oAuthSpiItem){
    $http.defaults.headers.common.Spi = oAuthSpiItem;
    $http.defaults.headers.common.OAuthProvider = "yahoo";
  }
}

The screenshot here shows those two headers for one of our an AJAX requests:

Using OAuth2

HTML5 SessionStorage

We used the SessionStorage as storage solution on the client side for user credentials and social identifiers (GUIDs).

In HTML5, web pages have the capability to store data locally in the browser using the Web Storage technology. Data in stored Web Storage can be accessed from the page scripts' and values can be relatively large (up to 5MB) with no impact on client-side performance.

Web Storage is per origin (the combination of protocol, hostname, and port number). All pages from one origin can store and access the same data. There are two types of objects that can be used for storing data locally:

  • window.localStorage: This stores data with no expiration date.
  • window.sessionStorage: This stores data for one session (data is lost when the tab is closed).

These two objects can be accessed directly from the window object and they both come with the self-explanatory methods:

setItem(key,value);
getItem(key);
removeItem(key);
clear();

As indicated by http://www.w3schools.com/, localStorage is almost supported by all browsers nowadays (between 94% and 98% depending upon your market). The following table shows the first versions that fully support it:

HTML5 SessionStorage

We should implement a fallback option with cookies for noncompliant web browsers, or at least a warning message when the browsers seem outdated.

SSL/TLS

An encrypted communication protocol must be setup when using a BASIC authentication. We have seen that the credentials username:password and the Yahoo! GUID are sent as request headers. Even though those credentials are encoded in base 64, this doesn't represent a sufficient protection.

BCryptPasswordEncoder

On the server side, we don't store the User passwords in plain text. We only store an encoded description of them (a hash). Therefore, a hashing function is supposedly not reversible.

 

"A hash function is any function that can be used to map digital data of arbitrary size to digital data of fixed size".

 
 --Wikipedia

Let's have a look at the following mapping:

BCryptPasswordEncoder

This diagram shows a hash function that maps names to integers from 0 to 15.

We used a PasswordEncoder implementation invoked manually while persisting and updating Users. Also PasswordEncoder is an Interface of Spring Security core:

public interface PasswordEncoder {
  String encode(CharSequence rawPassword);
  boolean matches(CharSequence rawPassword, String encodedPassword);
}

Spring Security provides three implementations: StandardPasswordEncoder, NoOpPasswordEncoder, and BCryptPasswordEncoder.

We used BCryptPasswordEncoder as it is recommended on new projects. Instead of implementing a MD5 or SHA hashing algorithm, BCryptPasswordEncoder uses a stronger hashing algorithm with randomly generated salt.

This allows the storage of different HASH values for the same password. Here's an example of different BCrypt hashes for the 123456 value:

$2a$10$Qz5slUkuV7RXfaH/otDY9udROisOwf6XXAOLt4PHWnYgOhG59teC6
$2a$10$GYCkBzp2NlpGS/qjp5f6NOWHeF56ENAlHNuSssSJpE1MMYJevHBWO
$2a$10$5uKS72xK2ArGDgb2CwjYnOzQcOmB7CPxK6fz2MGcDBM9vJ4rUql36

There is more…

Setting HTTP headers with AngularJS

As we have set Headers, check out the following page for more information about headers management with AngularJS:

https://docs.angularjs.org/api/ng/service/$http

Browser support for localStorage

Get an insight about the overall support per bbrowser version:

http://caniuse.com/#search=localstorage

About SSL and TLS

We have installed a SSL certificate on our production server. To buy and get issued a SSL certificate, we have had to provide our web server type (Apache 2) and a Certificate Signing Request (CSR) generated from the keytool program (embedded in the JDK).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset