If you're working on a Spring Security (and especially an OAuth) implementation, definitely have a look at the Learn Spring Security course:
>> LEARN SPRING SECURITYMocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
1. Overview
In this quick tutorial, weβll illustrate how we can revoke tokens granted by an OAuth Authorization Server implemented with Spring Security.
When a user logs out, their token is not immediately removed from the token store; instead, it remains valid until it expires on its own.
And so, revocation of a token will mean removing that token from the token store. Weβll cover the standard token implementation in the framework, not JWT tokens.
Note: this article is using the Spring OAuth legacy project.
2. The TokenStore
First, letβs set up the token store; weβll use a JdbcTokenStore, along with the accompanying data source:
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource());
}
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
dataSource.setUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.user"));
dataSource.setPassword(env.getProperty("jdbc.pass"));
return dataSource;
}
3. The DefaultTokenServices Bean
The class that handles all tokens is the DefaultTokenServices β and has to be defined as a bean in our configuration:
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
4. Displaying the List of Tokens
For admin purposes, letβs also set up a way to view the currently valid tokens.
Weβll access the TokenStore in a controller, and retrieve the currently stored tokens for a specified client id:
@Resource(name="tokenStore")
TokenStore tokenStore;
@RequestMapping(method = RequestMethod.GET, value = "/tokens")
@ResponseBody
public List<String> getTokens() {
List<String> tokenValues = new ArrayList<String>();
Collection<OAuth2AccessToken> tokens = tokenStore.findTokensByClientId("sampleClientId");
if (tokens!=null){
for (OAuth2AccessToken token:tokens){
tokenValues.add(token.getValue());
}
}
return tokenValues;
}
5. Revoking an Access Token
In order to invalidate a token, weβll make use of the revokeToken() API from the ConsumerTokenServices interface:
@Resource(name="tokenServices")
ConsumerTokenServices tokenServices;
@RequestMapping(method = RequestMethod.POST, value = "/tokens/revoke/{tokenId:.*}")
@ResponseBody
public String revokeToken(@PathVariable String tokenId) {
tokenServices.revokeToken(tokenId);
return tokenId;
}
Of course this is a very sensitive operation so we should either only use it internally, or we should take great care to expose it with the proper security in place.
6. The Front-End
For the front-end of our example, weβll display the list of valid tokens, the token currently used by the logged in user making the revocation request, and a field where the user can enter the token they wish to revoke:
$scope.revokeToken =
$resource("http://localhost:8082/spring-security-oauth-resource/tokens/revoke/:tokenId",
{tokenId:'@tokenId'});
$scope.tokens = $resource("http://localhost:8082/spring-security-oauth-resource/tokens");
$scope.getTokens = function(){
$scope.tokenList = $scope.tokens.query();
}
$scope.revokeAccessToken = function(){
if ($scope.tokenToRevoke && $scope.tokenToRevoke.length !=0){
$scope.revokeToken.save({tokenId:$scope.tokenToRevoke});
$rootScope.message="Token:"+$scope.tokenToRevoke+" was revoked!";
$scope.tokenToRevoke="";
}
}
If a user attempts to use a revoked token again, they will receive an βinvalid tokenβ error with status code 401.
7. Revoking the Refresh Token
The refresh token can be used to obtain a new access token. Whenever an access token is revoked, the refresh token that was received with it is invalidated.
If we want to invalidate the refresh token itself also, we can use the method removeRefreshToken() of class JdbcTokenStore, which will remove the refresh token from the store:
@RequestMapping(method = RequestMethod.POST, value = "/tokens/revokeRefreshToken/{tokenId:.*}")
@ResponseBody
public String revokeRefreshToken(@PathVariable String tokenId) {
if (tokenStore instanceof JdbcTokenStore){
((JdbcTokenStore) tokenStore).removeRefreshToken(tokenId);
}
return tokenId;
}
In order to test that the refresh token is no longer valid after being revoked, weβll write the following test, in which we obtain an access token, refresh it, then remove the refresh token, and attempt to refresh it again.
We will see that after revocation, we will receive the response error: βinvalid refresh tokenβ:
public class TokenRevocationLiveTest {
private String refreshToken;
private String obtainAccessToken(String clientId, String username, String password) {
Map<String, String> params = new HashMap<String, String>();
params.put("grant_type", "password");
params.put("client_id", clientId);
params.put("username", username);
params.put("password", password);
Response response = RestAssured.given().auth().
preemptive().basic(clientId,"secret").and().with().params(params).
when().post("http://localhost:8081/spring-security-oauth-server/oauth/token");
refreshToken = response.jsonPath().getString("refresh_token");
return response.jsonPath().getString("access_token");
}
private String obtainRefreshToken(String clientId) {
Map<String, String> params = new HashMap<String, String>();
params.put("grant_type", "refresh_token");
params.put("client_id", clientId);
params.put("refresh_token", refreshToken);
Response response = RestAssured.given().auth()
.preemptive().basic(clientId,"secret").and().with().params(params)
.when().post("http://localhost:8081/spring-security-oauth-server/oauth/token");
return response.jsonPath().getString("access_token");
}
private void authorizeClient(String clientId) {
Map<String, String> params = new HashMap<String, String>();
params.put("response_type", "code");
params.put("client_id", clientId);
params.put("scope", "read,write");
Response response = RestAssured.given().auth().preemptive()
.basic(clientId,"secret").and().with().params(params).
when().post("http://localhost:8081/spring-security-oauth-server/oauth/authorize");
}
@Test
public void givenUser_whenRevokeRefreshToken_thenRefreshTokenInvalidError() {
String accessToken1 = obtainAccessToken("fooClientIdPassword", "john", "123");
String accessToken2 = obtainAccessToken("fooClientIdPassword", "tom", "111");
authorizeClient("fooClientIdPassword");
String accessToken3 = obtainRefreshToken("fooClientIdPassword");
authorizeClient("fooClientIdPassword");
Response refreshTokenResponse = RestAssured.given().
header("Authorization", "Bearer " + accessToken3)
.get("http://localhost:8082/spring-security-oauth-resource/tokens");
assertEquals(200, refreshTokenResponse.getStatusCode());
Response revokeRefreshTokenResponse = RestAssured.given()
.header("Authorization", "Bearer " + accessToken1)
.post("http://localhost:8082/spring-security-oauth-resource/tokens/revokeRefreshToken/"+refreshToken);
assertEquals(200, revokeRefreshTokenResponse.getStatusCode());
String accessToken4 = obtainRefreshToken("fooClientIdPassword");
authorizeClient("fooClientIdPassword");
Response refreshTokenResponse2 = RestAssured.given()
.header("Authorization", "Bearer " + accessToken4)
.get("http://localhost:8082/spring-security-oauth-resource/tokens");
assertEquals(401, refreshTokenResponse2.getStatusCode());
}
}
8. Conclusion
In this tutorial, we have demonstrated how to revoke an OAuth access token and an Oauth refresh token.
