Spring Security OAuth2 - Revogação de token simples

Spring Security OAuth2 - Revogação de token simples

1. Visão geral

Neste tutorial rápido, ilustraremos como podemos revogar tokens concedidos por umOAuth Authorization Server implementado comSpring Security.

Quando um usuário efetua logout, seu token não é removido imediatamente do repositório de tokens, mas permanece válido até expirar por conta própria.

E assim, a revogação de um token significa remover esse token do repositório de tokens.

Observe também que este artigo abrange apenas a implementação de token padrão na estrutura, não os tokens JWT.

2. OTokenStore

Primeiro, vamos configurar o armazenamento de tokens; usaremos umJdbcTokenStore, junto com a fonte de dados que o acompanha:

@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. O feijãoDefaultTokenServices

A classe que lida com todos os tokens éDefaultTokenServices - e deve ser definida como um bean em nossa configuração:

@Bean
@Primary
public DefaultTokenServices tokenServices() {
    DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
    defaultTokenServices.setTokenStore(tokenStore());
    defaultTokenServices.setSupportRefreshToken(true);
    return defaultTokenServices;
}

4. Exibindo a lista de tokens

Para fins administrativos, vamos configurar também uma maneira de visualizar os tokens válidos no momento.

Iremos acessar oTokenStore em um controlador e recuperar os tokens armazenados atualmente para um ID de cliente especificado:

@Resource(name="tokenStore")
TokenStore tokenStore;

@RequestMapping(method = RequestMethod.GET, value = "/tokens")
@ResponseBody
public List getTokens() {
    List tokenValues = new ArrayList();
    Collection tokens = tokenStore.findTokensByClientId("sampleClientId");
    if (tokens!=null){
        for (OAuth2AccessToken token:tokens){
            tokenValues.add(token.getValue());
        }
    }
    return tokenValues;
}

5. Revogando um token de acesso

Para invalidar um token, usaremos a APIrevokeToken() da interfaceConsumerTokenServices:

@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;
}

É claro que essa é uma operação muito delicada, portanto, devemos usá-la apenas internamente ou ter muito cuidado em expô-la com a segurança adequada.

6. O Front-End

Para o front-end de nosso exemplo, exibiremos a lista de tokens válidos, o token usado atualmente pelo usuário conectado que faz a solicitação de revogação e um campo onde o usuário pode inserir o token que deseja revogar:

$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="";
    }
}

Se um usuário tentar usar um token revogado novamente, ele receberá um erro de ‘token inválido 'com o código de status 401.

7. Revogando o token de atualização

O token de atualização pode ser usado para obter um novo token de acesso. Sempre que um token de acesso é revogado, o token de atualização que foi recebido com ele é invalidado.

Se quisermos invalidar o próprio token de atualização também, podemos usar o métodoremoveRefreshToken() da classeJdbcTokenStore, que removerá o token de atualização da loja:

@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;
}

Para testar se o token de atualização não é mais válido após ser revogado, vamos escrever o seguinte teste, no qual obtemos um token de acesso, o atualizamos e, em seguida, removemos o token de atualização e tentamos atualizá-lo novamente.

Veremos que após a revogação, receberemos o erro de resposta: "token de atualização inválido":

public class TokenRevocationLiveTest {
    private String refreshToken;

    private String obtainAccessToken(String clientId, String username, String password) {
        Map params = new HashMap();
        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 params = new HashMap();
        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 params = new HashMap();
        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. Conclusão

Neste tutorial, demonstramos como revogar um token de acesso do OAuth e um token de atualização do Oauth.

A implementação deste tutorial pode ser encontrada emthe GitHub project - este é um projeto baseado em Maven, portanto, deve ser fácil de importar e executar como está.