Fast and stateless API authentication with Spring Security
Would Spring Security work and perform equally well for securing stateless APIs used for non-browser clients, such as mobile apps or other kinds of applications? The short answer is yes.
The purpose of this article is to demonstrate how to configure Spring Security for securing stateless APIs; what to consider when designing security solution and how to improve performance of user authentication. Another purpose of this article is to provide you (source code of fully configured application)1 which you can start from your IDE (embedded Jetty) and play with it by trying different configuration options.
Sample webapp for this article
The application written to accompany this article is fully configured with Java Config (*with exception to cache and logback), uses embedded Jetty, Spring Framework 4, Spring Security 3.2.5, Servlet 3.x, H2 embedded database with sample data and EHCache. You can clone it from my (git repository)1 and launch it straight away by launching Jetty.java from your IDE (under src/test/java/…/jetty) or by running mvn jetty:run Take a look at scripts under /scripts directory.
The basics
It’s impractical for API clients to use standard, form based, authentication due to necessity of providing JSESSIONID cookie with following requests. Spring Security allows to use HTTP Basic Authentication which may sound old-school but actually works quite well. Similarly to form-based authentication, all traffic should go over https. HTTP Basic Authentication user/password details need are best to be set on the request header. Header attribute is ‘Authorization’ and value is ‘Basic ‘ + Base64 encoded username:password. In the examples I’m using that’s user:password which after encoding is dXNlcjpwYXNzd29yZA== For testing purposes, you can set this header with curl by adding -H “Authorization: Basic dXNlcjpwYXNzd29yZA==” or with excellent Chrome plugin postman.
HTTP Basic Authentication is pretty straightforward to configure, take a look at following code:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().hasAuthority("USER")
.and()
.httpBasic();
}
This configuration has one drawback: every time you call your secured endpoint a new session is created. I’m calling sample endpoint and that’s what I’m getting back:
marcin@RDS:~$ curl -sL --connect-timeout 1 -i http://localhost:9090/secure/aa -H "Authorization: Basic dXNlcjpwYXNzd29yZA=="
HTTP/1.1 200 OK
Date: Sat, 27 Dec 2014 21:53:35 GMT
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
X-Frame-Options: DENY
Set-Cookie: JSESSIONID=1llnaga69lfg01x0s5za9lg2ma;Path=/
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Server: Jetty(9.2.6.v20141205)
{"message":"Ha! aa"}
marcin@RDS:~$ curl -sL --connect-timeout 1 -i http://localhost:9090/secure/aa -H "Authorization: Basic dXNlcjpwYXNzd29yZA=="
HTTP/1.1 200 OK
Date: Sat, 27 Dec 2014 21:53:54 GMT
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
X-Frame-Options: DENY
Set-Cookie: JSESSIONID=1edajcn1pz8v64iz3ltvhvyy7;Path=/
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Server: Jetty(9.2.6.v20141205)
{"message":"Ha! aa"}
As you can see I called a service twice with Basic Auth headers and in each response I got a new JSESSIONID cookie – each time a new session was created. This is bit pointless as we are not using and don’t need sessions in this scenarios. This could be easily turned off by modifying our configuration as following:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.anyRequest().hasAuthority("USER")
.and()
.httpBasic();
}
As you can see, after this change there is no cookie in the header of a response:
marcin@RDS:~$ curl -sL --connect-timeout 1 -i http://localhost:9090/secure/aa -H "Authorization: Basic dXNlcjpwYXNzd29yZA=="
HTTP/1.1 200 OK
Date: Sat, 27 Dec 2014 22:05:04 GMT
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Server: Jetty(9.2.6.v20141205)
{"message":"Ha! aa"}
Improving performance
We are nearly there – we can authenticate users and no sessions are created. Most set-ups will use some sort of database to store usernames and passwords. That could lead to unnecessary load if your users are doing frequent calls to the backend services. Let’s take a look at sample logs for two consecutive calls to the secured endpoint:
22:17:59.081 RDS DEBUG JdbcTemplate - Executing prepared SQL query
22:17:59.081 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,password,enabled from users where username = ?]
22:17:59.081 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
22:17:59.081 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
22:17:59.082 RDS DEBUG JdbcTemplate - Executing prepared SQL query
22:17:59.082 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,authority from authorities where username = ?]
22:17:59.082 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
22:17:59.082 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
22:17:59.754 RDS DEBUG JdbcTemplate - Executing prepared SQL query
22:17:59.754 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,password,enabled from users where username = ?]
22:17:59.754 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
22:17:59.754 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
22:17:59.754 RDS DEBUG JdbcTemplate - Executing prepared SQL query
22:17:59.754 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,authority from authorities where username = ?]
22:17:59.754 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
22:17:59.754 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
As you can see in provided logs, for each endpoint call we queried database for user details and user’s authorities. To overcome this issue we can use one of multiple implementations of UserCache – in provided application I used EHCache so that you can play with TTL of cached objects.
For me, the easiest way to enable UserCache was to provide a properly configured AuthenticationProvider and pass it to AuthenticationManagerBuilder – which you can see in SecurityConfig.java in the sample application.
Take a look at following console logs, this time UserCache is used and I called the endpoint called 5 times:
22:28:18.784 RDS DEBUG EhCacheBasedUserCache - Cache hit: false; username: user
22:28:18.786 RDS DEBUG JdbcTemplate - Executing prepared SQL query
22:28:18.786 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,password,enabled from users where username = ?]
22:28:18.794 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
22:28:18.811 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
22:28:18.812 RDS DEBUG JdbcTemplate - Executing prepared SQL query
22:28:18.812 RDS DEBUG JdbcTemplate - Executing prepared SQL statement [select username,authority from authorities where username = ?]
22:28:18.812 RDS DEBUG DataSourceUtils - Fetching JDBC Connection from DataSource
22:28:18.813 RDS DEBUG DataSourceUtils - Returning JDBC Connection to DataSource
22:28:18.815 RDS DEBUG EhCacheBasedUserCache - Cache put: user
22:28:20.488 RDS DEBUG EhCacheBasedUserCache - Cache hit: true; username: user
22:28:22.915 RDS DEBUG EhCacheBasedUserCache - Cache hit: true; username: user
22:28:24.905 RDS DEBUG EhCacheBasedUserCache - Cache hit: true; username: user
22:28:26.734 RDS DEBUG EhCacheBasedUserCache - Cache hit: true; username: user
Initially AuthenticationProvider checks for UserDetails object in cache – as it’s not there we got ‘cache miss’. Then AuthenticationProvider queries the DB for UserDetails and authorities, puts UserDetails into the cache and for all consecutive calls retrieving UserDetails from the cache – until the object is evicted from cache.
One thing worth mentioning is that you do not necessarily need to remove UserDetails object from the cache if you update password of a user – if you try to authenticate a user with new password and it won’t match value in the cache, then DB will be queried for UserDetails.
SPRING
java security spring