This recipe presents a solution for storing credentials in RESTful applications.
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.
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; } }});
$http
to pass and handle transparently the credentials or identification headers required for AJAX requests.sessionStorage
attribute from the different controllers, in order to prevent being tightly coupled with this storage solution.account_management.js
file regroups different controllers (LoginByUsernameAndPasswordController
, createNewAccountController
, and OAuth2Controller
) that store credentials and provider IDs in sessionStorage
through httpAuth
.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"); } } });
passwordEncoder
bean in security-config.xml
(in the cloudstreetmarket-core
module):<bean id="passwordEncoder" class="org.sfw.security.crypto.bcrypt.BCryptPasswordEnco der"/>
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>
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; } ... }
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.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).
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.
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
.
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:
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:
We should implement a fallback option with cookies for noncompliant web browsers, or at least a warning message when the browsers seem outdated.
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.
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:
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
As we have set Headers, check out the following page for more information about headers management with AngularJS: