Spring Security OAuth2 - Révocation de jeton simple

Spring Security OAuth2 - Révocation de jeton simple

1. Vue d'ensemble

Dans ce rapide tutoriel, nous allons illustrer comment nous pouvons révoquer les jetons accordés par unOAuth Authorization Server implémenté avecSpring Security.

Lorsqu'un utilisateur se déconnecte, son jeton n'est pas immédiatement supprimé du magasin de jetons, mais reste valide jusqu'à son expiration.

Et ainsi, la révocation d'un jeton signifiera la suppression de ce jeton du magasin de jetons.

Notez également que cet article ne couvre que l'implémentation standard de jetons dans le cadre, pas les jetons JWT.

2. LesTokenStore

Commençons par configurer le magasin de jetons; nous utiliserons unJdbcTokenStore, ainsi que la source de données associée:

@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. Le haricotDefaultTokenServices

La classe qui gère tous les jetons est leDefaultTokenServices - et doit être définie comme un bean dans notre configuration:

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

4. Affichage de la liste des jetons

À des fins d'administration, nous allons également configurer un moyen d'afficher les jetons actuellement valides.

Nous accéderons auxTokenStore dans un contrôleur et récupérerons les jetons actuellement stockés pour un ID client spécifié:

@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. Révoquer un jeton d'accès

Pour invalider un jeton, nous utiliserons l'APIrevokeToken() depuis l'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;
}

Bien sûr, il s’agit d’une opération très délicate. Nous devons donc l’utiliser en interne ou veiller à l’exposer avec la sécurité appropriée.

6. Le Front-End

Pour le front-end de notre exemple, nous afficherons la liste des jetons valides, le jeton actuellement utilisé par l'utilisateur connecté effectuant la demande de révocation, et un champ dans lequel l'utilisateur peut entrer le jeton qu'il souhaite révoquer:

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

Si un utilisateur tente à nouveau d'utiliser un jeton révoqué, il recevra une erreur «Jeton invalide» avec le code d'état 401.

7. Révocation du jeton d'actualisation

Le jeton d'actualisation peut être utilisé pour obtenir un nouveau jeton d'accès. Lorsqu'un jeton d'accès est révoqué, le jeton d'actualisation reçu avec ce dernier est invalidé.

Si nous voulons également invalider le jeton d'actualisation lui-même, nous pouvons utiliser la méthoderemoveRefreshToken() de la classeJdbcTokenStore, qui supprimera le jeton d'actualisation du magasin:

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

Afin de tester que le jeton d'actualisation n'est plus valide après avoir été révoqué, nous allons écrire le test suivant, dans lequel nous obtenons un jeton d'accès, l'actualisons, puis supprimons le jeton d'actualisation et tentons de l'actualiser à nouveau.

Nous verrons qu'après la révocation, nous recevrons l'erreur de réponse: «jeton d'actualisation invalide»:

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. Conclusion

Dans ce didacticiel, nous avons montré comment révoquer un jeton d'accès OAuth et un jeton d'actualisation Oauth.

L'implémentation de ce tutoriel peut être trouvée dansthe GitHub project - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.