I use this code https://github.com/gdongus/spring-boot-oauth-jwt-example and everything works perfect, but I don't know how to implement logout functionality. Can somebody give me advice? Thank you.
3 Answers
The client-side logout is simple, just discard the token you own. To provide a server-side logout functionality your application has to be aware of currently authenticated clients, in other words, existing tokens. The "build-in" problem with the token based authentication is that if a token is published it is valid until it expires and there is no "remote invalidation" solution. Your only chance is to avoid access for requests with a token you don't trust anymore.
So you have to remember every published token in a container called token store.
There are some implementations of the TokenStore interface to work in-memory or maybe with a database (JdbcTokenStore). For a simple example the InMemoryTokenStore is totally sufficient.
To use it, a token store has to be created and configured as follows.
Add this to your AuthorizationServerConfiguration:
@Bean
public InMemoryTokenStore tokenStore() {
return new InMemoryTokenStore();
}
And use it in the AuthorizationServerEndpointsConfigurer:
@Override
public void configure(AuthorizationServerEndpointsConfigurer configurer) throws Exception {
configurer.authenticationManager(authenticationManager);
configurer.userDetailsService(userDetailsService);
configurer.accessTokenConverter(accessTokenConverter());
configurer.tokenStore(tokenStore());
}
Add it also to your ResourceServerConfiguration:
@Autowired
private InMemoryTokenStore inMemoryTokenStore;
...
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("resource").tokenStore(inMemoryTokenStore);
}
That's nearly all. Now you can implement your logout functionality like you need it, maybe with a special endpoint where you only have to get the token(s) and remove it from the token store with:
inMemoryTokenStore.removeAccessToken(accessToken);
inMemoryTokenStore.removeRefreshToken(refreshToken);
Be aware to also remove the refresh token, otherwise (if only the access token is removed) the client is able to gain a new one with the refresh token.
Here is a test case according to your tests to verify if it's working:
@Test
public void getUserWithValidAuth() throws Exception {
final HttpHeaders headers = getHttpHeader(CLIENT_USER, CLIENT_SECRET);
final HttpEntity<String> request = new HttpEntity<>(headers);
final String tokenUrl = getOAuthTokenUrl(OAUTH_TOKEN_USERNAME, OAUTH_TOKEN_PASSWORD);
final ResponseEntity<Object> response = restTemplate.exchange(tokenUrl, HttpMethod.POST, request, Object.class);
assertTrue("Did not get auth tokens!", response.getStatusCode().is2xxSuccessful());
final Map result = (Map) response.getBody();
final String accessTokenAsString = (String) result.get(ACCESS_TOKEN);
final String refreshTokenAsString = (String) result.get(REFRESH_TOKEN);
final String resourceUrlWithToken = "http://localhost:" + port + "/users?access_token=" + accessTokenAsString;
final ResponseEntity<String> userResponse = restTemplate.exchange(resourceUrlWithToken, HttpMethod.GET, null,
String.class);
assertTrue("Could not request user data!", userResponse.getStatusCode().is2xxSuccessful());
final OAuth2AccessToken accessToken = inMemoryTokenStore.readAccessToken(accessTokenAsString);
final OAuth2RefreshToken refreshToken = inMemoryTokenStore.readRefreshToken(refreshTokenAsString);
inMemoryTokenStore.removeAccessToken(accessToken);
inMemoryTokenStore.removeRefreshToken(refreshToken);
try {
restTemplate.exchange(resourceUrlWithToken, HttpMethod.GET, null, String.class);
fail("Should not get here, expected 401 for request with access token!");
} catch (HttpClientErrorException e) {
// would not be needed with MockMvc
}
final String refreshTokenUrl = REFRESH_TOKEN_URL + refreshTokenAsString;
try {
restTemplate.exchange(refreshTokenUrl, HttpMethod.POST, request, Object.class);
fail("Should not get here, expected 401 for request with refresh token!");
} catch (HttpClientErrorException e) {
// would not be needed with MockMvc
}
}
And at least just a recommendation, using MockMvc is an awesome test framework which makes it easy to test rest calls and you can get rid of the obstacles and boiler-plate code while working with the RestTemplate. Maybe you want to give it a try.
2 Comments
Since as soon as the logout is complete, both access token & refresh token will be removed from the underlying storage on auth server, we only need to worry about the access token invalidation in resource servers only till it expires
In order to accomplish this, you need to publish an event from auth-server as soon as the logout is complete via Spring Stream/Integration & make all token audience instances to subscribe to the Logout event
You can add your own LogoutHandler that publishes this event from auth server. @StreamListner from Spring cloud stream can be used to listen for this events on each of the resource servers
This logout event must contain the removed access token & the amount of time left before it expires. All receivers of this event must store these access tokens in memory to a blacklist & make sure they reject access to a resource, if the received access token matches any of existing blacklisted tokens. After the access token expires, just remove it from memory. To automatically expire keys, you can use something like CacheBuilder from guava
So, overall, AFAIK, there is no ready to use solution for the expiration of access token due to the nature of JWT
Comments
You can create a USER_TOKEN table and keep all the tokens generated by the user, [helpful for multiple device login], And when logout will be called from front end , remove or destroy the token from front-end local storage and call a /logout api with the same token , then validate the token and enter server code and delete that particular token from USER_TOKEN table.
So next time if someone wants to access the API using same token , that token will not be validated , as it is not present in DB.
But in case someone is closing the browser tab , instead of logout, then the token will work until it get expired.