Spring-boot JWT logout

Martin Morek picture Martin Morek · Dec 26, 2015 · Viewed 15.7k times · Source

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.

Answer

Kevin Peters picture Kevin Peters · Mar 20, 2017

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.