Cache OAuth 2 in Spring With Redis
In this article, we are going to go over how to create a caching mechanism for Spring and OAuth2 using Redis as our database for cache storage.
Join the DZone community and get the full member experience.
Join For FreeIn this post, we are going to demonstrate the caching mechanism for Spring + OAuth2 using Redis for cache storage. Before you start, you should familiarize yourself with OAuth2 fundamentals.
Application
In my previous post, Secure Spring REST With Spring Security and OAuth2, we developed simple Spring Boot application with OAuth 2 that we’re going to use as a starting point for this post (for the purposes of this post, I modified a little bit of the application by defining the relationship between entities like User, Role, and Permission, so we are not storing direct relationships between User and GrantedAuthority in the database anymore).
In our example application, we have the following structure for our business data:
We are able to access this structure via secured endpoints:
@RestController
@RequestMapping("/secured/company")
public class CompanyController {
@Autowired
private CompanyService companyService;
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.OK)
public @ResponseBody
List<Company> getAll() {
return companyService.getAll();
}
@RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.OK)
public @ResponseBody
Company get(@PathVariable Long id) {
return companyService.get(id);
}
@RequestMapping(value = "/filter", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.OK)
public @ResponseBody
Company get(@RequestParam String name) {
return companyService.get(name);
}
@RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity<?> create(@RequestBody Company company) {
companyService.create(company);
HttpHeaders headers = new HttpHeaders();
ControllerLinkBuilder linkBuilder = linkTo(methodOn(CompanyController.class).get(company.getId()));
headers.setLocation(linkBuilder.toUri());
return new ResponseEntity<>(headers, HttpStatus.CREATED);
}
@RequestMapping(method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.OK)
public void update(@RequestBody Company company) {
companyService.update(company);
}
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.OK)
public void delete(@PathVariable Long id) {
companyService.delete(id);
}
}
Collect Possible Performance Issues
First of all, we will have to check oauth2-related database operations for endpoints:
/oauth/token
(endpoint for getting access token)/secured/company/{companyId}
(example of a secured endpoint)
Get Access Token
If we run our endpoint to get an access token:
curl -X POST \
http://localhost:8080/oauth/token \
-H 'authorization: Basic c3ByaW5nLXNlY3VyaXR5LW9hdXRoMi1yZWFkLXdyaXRlLWNsaWVudDpzcHJpbmctc2VjdXJpdHktb2F1dGgyLXJlYWQtd3JpdGUtY2xpZW50LXBhc3N3b3JkMTIzNA==' \
-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
-F grant_type=password \
-F username=admin \
-F password=admin1234 \
-F client_id=spring-security-oauth2-read-write-client
The above code result int the following queries being executed:
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select token_id, token from oauth_access_token where authentication_id = ?
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select token_id, token from oauth_access_token where token_id = ?
insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)
insert into oauth_refresh_token (token_id, token, authentication) values (?, ?, ?)
As you can see, the same query was executed 7 times in one request:
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
JdbcClientDetailsService.loadClientByClientId(String clientId)
is a service that executes queries to the database. Since it’s an implementation that uses the database, we should prepare a more generic solution that would cache data even if the source of that data is not a SQL database. To proceed with that, we can cache results of the interface that needs to be implemented to load the client by its id:
Get Access Token Based on Refresh Token
To getaccess_token
based onrefresh_token
we use the following code:
curl -X POST \
http://localhost:8080/oauth/token \
-H 'authorization: Basic c3ByaW5nLXNlY3VyaXR5LW9hdXRoMi1yZWFkLXdyaXRlLWNsaWVudDpzcHJpbmctc2VjdXJpdHktb2F1dGgyLXJlYWQtd3JpdGUtY2xpZW50LXBhc3N3b3JkMTIzNA==' \
-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
-F grant_type=refresh_token \
-F refresh_token=REFRESH_TOKEN
The following queries are then executed:
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select token_id, token from oauth_refresh_token where token_id = ?
select token_id, authentication from oauth_refresh_token where token_id = ?
delete from oauth_access_token where refresh_token = ?
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
select token_id, token from oauth_access_token where token_id = ?
insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)
The below query was executed 5 times in one request:
select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
However, we have already covered the caching of this query.
Get a Secured Resource
To get an example of a secured resource, we have to send an authentication token:
curl -X GET \
http://localhost:8080/secured/company/1 \
-H 'authorization: Bearer ACCESS_TOKEN'
During token validation, the following queries are executed for each request:
select token_id, token from oauth_access_token where token_id = ?
select token_id, authentication from oauth_access_token where token_id = ?
Since token_id
is usually the same for a long time, we might cache these values. Below are implementations of these respective queries:
- JdbcTokenStore.readAccessToken(String tokenValue)
- JdbcTokenStore.readAuthentication(String token)
Like in the previous example, we should cache the invocation of methods on the interface level:
Refresh Client Data
We are not going to demonstrate all the cache operations that should be taken into account during the caching of OAuth2 data. We should remember to properly handle token invalidation or client deletion.
Cache Service Data
Since we want to define caching on OAuth2 library level services, we would like to inject caching without touching the OOTB code. We can use AOP to define pointcuts where a caching mechanism should be injected.
Enable Caching
To enable cache management capabilities in a Spring application, we need to add the @EnableCaching
annotation to the configuration class. In this post, we are going to use a Redis provider to store the cache. The below example shows cache enabling with Redis related beans in a separate configuration class. Overriding these beans is not needed if you want to stay with a default Spring Boot setup.
@Configuration
@EnableCaching
@PropertySource("classpath:/redis.properties")
public class CacheConfiguration {
@Value("${redis.host}")
private String redisHost;
@Value("${redis.port}")
private int redisPort;
@Bean
public JedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
redisConnectionFactory.setHostName(redisHost);
redisConnectionFactory.setPort(redisPort);
return redisConnectionFactory;
}
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<?, ?> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
return template;
}
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate());
cacheManager.setUsePrefix(true);
cacheManager.setDefaultExpiration(240);
return cacheManager;
}
}
Cache Operations
We are going to use declarative XML-based caching to do this, rather than modifying our OAuth2 classes. We will create a separate configuration that imports the XML file.
@Configuration
@ImportResource({"classpath:oauth2-cache.xml"})
public class OAuth2CacheConfig {
}
In the XML file, we have to define caching behaviors. In order to cache following methods:
- loadClientByClientId
- readAuthentication
- readAccessToken
The below definitions need to be created:
<!-- define caching behavior -->
<cache:advice id="clientById" cache-manager="redisCacheManager">
<cache:caching cache="OAuthClientDetailsServiceCache">
<cache:cacheable method="loadClientByClientId" key="#clientId"/>
</cache:caching>
</cache:advice>
<cache:advice id="authByTokenId" cache-manager="redisCacheManager">
<cache:caching cache="OAuthTokenStoreReadAuthenticationCache">
<cache:cacheable method="readAuthentication" key="#token.toString()" />
</cache:caching>
</cache:advice>
<cache:advice id="tokenById" cache-manager="redisCacheManager">
<cache:caching cache="OAuthTokenByIdCache">
<cache:cacheable method="readAccessToken" key="#tokenValue" unless="#result == null"/>
</cache:caching>
</cache:advice>
For the above definition, we have to apply a cacheable behavior to all implementations of the below interfaces:
<!-- apply the cacheable behavior to all interface methods -->
<aop:config>
<aop:advisor advice-ref="clientById" pointcut="execution(* org.springframework.security.oauth2.provider.ClientDetailsService.*(..))"/>
<aop:advisor advice-ref="authByTokenId" pointcut="execution(* org.springframework.security.oauth2.provider.token.TokenStore.*(..)) "/>
<aop:advisor advice-ref="tokenById" pointcut="execution(* org.springframework.security.oauth2.provider.token.TokenStore.*(..))"/>
</aop:config>
Summary
In this blog post, we showed OAuth2 caching with Spring and Redis.
The source code for the above listings can be found in the GitHub project company-structure-spring-security-oauth2-cache (in the ehcache branch, you will find the configuration for storing cache in EhCache).
Opinions expressed by DZone contributors are their own.
Comments