Implemented revoking all tokens of a user client via a super client.

Change-Id: I08dbd09b68a0ae17f810ec0bdc68951318aea9fe
diff --git a/full/Changes b/full/Changes
index 45cab74..65b5d4c 100644
--- a/full/Changes
+++ b/full/Changes
@@ -13,6 +13,9 @@
 6/12/2018
    - Added debug flags to mitigate log4j debugging performance (margaretha)
    - Fixed KoralNode at() method (margaretha)
+11/12/2018
+   - Implemented revoking all tokens of a user client via a super client
+   (margaretha)   
    
 # version 0.61.3
 17/10/2018
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
index b0785cd..b5f4173 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
@@ -9,8 +9,10 @@
 import javax.persistence.NoResultException;
 import javax.persistence.PersistenceContext;
 import javax.persistence.Query;
+import javax.persistence.TypedQuery;
 import javax.persistence.criteria.CriteriaBuilder;
 import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Join;
 import javax.persistence.criteria.Root;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -26,7 +28,10 @@
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
 import de.ids_mannheim.korap.oauth2.entity.AccessToken;
 import de.ids_mannheim.korap.oauth2.entity.AccessToken_;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client_;
 import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
+import de.ids_mannheim.korap.oauth2.entity.RefreshToken_;
 import de.ids_mannheim.korap.utils.ParameterChecker;
 
 @Repository
@@ -55,7 +60,7 @@
 
         ZonedDateTime now =
                 ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
-        
+
         AccessToken accessToken = new AccessToken();
         accessToken.setCreatedDate(now);
         accessToken
@@ -108,7 +113,6 @@
         }
     }
 
-    @SuppressWarnings("unchecked")
     public List<AccessToken> retrieveAccessTokenByClientId (String clientId) {
         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
         CriteriaQuery<AccessToken> query =
@@ -116,7 +120,19 @@
         Root<AccessToken> root = query.from(AccessToken.class);
         query.select(root);
         query.where(builder.equal(root.get(AccessToken_.clientId), clientId));
-        Query q = entityManager.createQuery(query);
+        TypedQuery<AccessToken> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public List<RefreshToken> retrieveRefreshTokenByClientId (String clientId) {
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<RefreshToken> query =
+                builder.createQuery(RefreshToken.class);
+        Root<RefreshToken> root = query.from(RefreshToken.class);
+        Join<RefreshToken, OAuth2Client> client = root.join(RefreshToken_.client);
+        query.select(root);
+        query.where(builder.equal(client.get(OAuth2Client_.id), clientId));
+        TypedQuery<RefreshToken> q = entityManager.createQuery(query);
         return q.getResultList();
     }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/OAuth2RevokeTokenSuperRequest.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/OAuth2RevokeTokenSuperRequest.java
new file mode 100644
index 0000000..183d2a3
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/OAuth2RevokeTokenSuperRequest.java
@@ -0,0 +1,61 @@
+package de.ids_mannheim.korap.oauth2.oltu;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.oltu.oauth2.common.OAuth;
+import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
+import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
+import org.apache.oltu.oauth2.common.utils.OAuthUtils;
+import org.apache.oltu.oauth2.common.validators.OAuthValidator;
+
+public class OAuth2RevokeTokenSuperRequest {
+    protected HttpServletRequest request;
+    protected OAuthValidator<HttpServletRequest> validator;
+    protected Map<String, Class<? extends OAuthValidator<HttpServletRequest>>> validators =
+            new HashMap<String, Class<? extends OAuthValidator<HttpServletRequest>>>();
+
+    public OAuth2RevokeTokenSuperRequest () {
+        // TODO Auto-generated constructor stub
+    }
+
+    public OAuth2RevokeTokenSuperRequest (HttpServletRequest request)
+            throws OAuthSystemException, OAuthProblemException {
+        this.request = request;
+        validate();
+    }
+
+    protected void validate ()
+            throws OAuthSystemException, OAuthProblemException {
+        validator = initValidator();
+        validator.validateMethod(request);
+        validator.validateContentType(request);
+        validator.validateRequiredParameters(request);
+        // for super client authentication
+        validator.validateClientAuthenticationCredentials(request);
+    }
+
+    protected OAuthValidator<HttpServletRequest> initValidator ()
+            throws OAuthProblemException, OAuthSystemException {
+        return OAuthUtils.instantiateClass(RevokeTokenSuperValidator.class);
+    }
+
+    public String getParam (String name) {
+        return request.getParameter(name);
+    }
+
+    public String getClientId () {
+        return request.getParameter(OAuth.OAUTH_CLIENT_ID);
+    }
+    
+    public String getSuperClientId () {
+        return request.getParameter(RevokeTokenSuperValidator.SUPER_CLIENT_ID);
+    }
+    
+    public String getSuperClientSecret () {
+        return request.getParameter(RevokeTokenSuperValidator.
+                SUPER_CLIENT_SECRET);
+    }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/RevokeTokenSuperValidator.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/RevokeTokenSuperValidator.java
new file mode 100644
index 0000000..650e8a5
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/RevokeTokenSuperValidator.java
@@ -0,0 +1,59 @@
+package de.ids_mannheim.korap.oauth2.oltu;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.oltu.oauth2.common.OAuth;
+import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
+import org.apache.oltu.oauth2.common.utils.OAuthUtils;
+import org.apache.oltu.oauth2.common.validators.AbstractValidator;
+
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
+
+public class RevokeTokenSuperValidator
+        extends AbstractValidator<HttpServletRequest> {
+
+    public static final String SUPER_CLIENT_ID = "super_client_id";
+    public static final String SUPER_CLIENT_SECRET = "super_client_secret";
+
+    public RevokeTokenSuperValidator () {
+        requiredParams.add(OAuth.OAUTH_CLIENT_ID);
+        requiredParams.add(SUPER_CLIENT_ID);
+        requiredParams.add(SUPER_CLIENT_SECRET);
+
+        enforceClientAuthentication = true;
+    }
+
+    @Override
+    public void validateMethod (HttpServletRequest request)
+            throws OAuthProblemException {
+        String method = request.getMethod();
+        if (!OAuth.HttpMethod.POST.equals(method)) {
+            throw OAuthProblemException.error(OAuth2Error.INVALID_REQUEST)
+                    .description("Method not correct.");
+        }
+    }
+
+    @Override
+    public void validateClientAuthenticationCredentials (
+            HttpServletRequest request) throws OAuthProblemException {
+        if (enforceClientAuthentication) {
+            Set<String> missingParameters = new HashSet<String>();
+
+            if (OAuthUtils
+                    .isEmpty(request.getParameter(SUPER_CLIENT_ID))) {
+                missingParameters.add(SUPER_CLIENT_ID);
+            }
+            if (OAuthUtils
+                    .isEmpty(request.getParameter(SUPER_CLIENT_SECRET))) {
+                missingParameters.add(SUPER_CLIENT_SECRET);
+            }
+
+            if (!missingParameters.isEmpty()) {
+                throw OAuthUtils.handleMissingParameters(missingParameters);
+            }
+        }
+    }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/RevokeTokenValidator.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/RevokeTokenValidator.java
index 0a9d9c7..60ff77f 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/RevokeTokenValidator.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/RevokeTokenValidator.java
@@ -38,8 +38,4 @@
         }
     }
 
-    @Override
-    public void validateContentType (HttpServletRequest request)
-            throws OAuthProblemException {}
-
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
index 7b4ac81..a88355e 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
@@ -3,6 +3,7 @@
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 import javax.persistence.NoResultException;
@@ -30,6 +31,7 @@
 import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
 import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
 import de.ids_mannheim.korap.oauth2.oltu.OAuth2RevokeTokenRequest;
+import de.ids_mannheim.korap.oauth2.oltu.OAuth2RevokeTokenSuperRequest;
 import de.ids_mannheim.korap.oauth2.service.OAuth2TokenService;
 
 @Service
@@ -127,13 +129,13 @@
         }
         else if (refreshToken.isRevoked()) {
             throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
-                    "Refresh token has been revoked.",
+                    "Refresh token has been revoked",
                     OAuth2Error.INVALID_GRANT);
         }
         else if (ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))
                 .isAfter(refreshToken.getExpiryDate())) {
             throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
-                    "Refresh token is expired.", OAuth2Error.INVALID_GRANT);
+                    "Refresh token is expired", OAuth2Error.INVALID_GRANT);
         }
 
         Set<AccessScope> requestedScopes = refreshToken.getScopes();
@@ -394,6 +396,12 @@
             return false;
         }
 
+        revokeRefreshToken(refreshToken);
+        return true;
+    }
+
+    private void revokeRefreshToken (RefreshToken refreshToken)
+            throws KustvaktException {
         refreshToken.setRevoked(true);
         refreshDao.updateRefreshToken(refreshToken);
 
@@ -402,7 +410,27 @@
             accessToken.setRevoked(true);
             tokenDao.updateAccessToken(accessToken);
         }
+    }
 
-        return true;
+    public void revokeTokenViaSuperClient (
+            OAuth2RevokeTokenSuperRequest revokeTokenRequest)
+            throws KustvaktException {
+        String superClientId = revokeTokenRequest.getSuperClientId();
+        String superClientSecret = revokeTokenRequest.getSuperClientSecret();
+
+        OAuth2Client superClient = clientService
+                .authenticateClient(superClientId, superClientSecret);
+        if (!superClient.isSuper()) {
+            throw new KustvaktException(
+                    StatusCodes.CLIENT_AUTHENTICATION_FAILED);
+        }
+
+        String clientId = revokeTokenRequest.getClientId();
+        List<RefreshToken> refreshTokens =
+                tokenDao.retrieveRefreshTokenByClientId(clientId);
+
+        for (RefreshToken r : refreshTokens) {
+            revokeRefreshToken(r);
+        }
     }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
index d86a2b1..020eb97 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
@@ -30,6 +30,7 @@
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.oauth2.oltu.OAuth2AuthorizationRequest;
 import de.ids_mannheim.korap.oauth2.oltu.OAuth2RevokeTokenRequest;
+import de.ids_mannheim.korap.oauth2.oltu.OAuth2RevokeTokenSuperRequest;
 import de.ids_mannheim.korap.oauth2.oltu.service.OltuAuthorizationService;
 import de.ids_mannheim.korap.oauth2.oltu.service.OltuTokenService;
 import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
@@ -42,7 +43,7 @@
 
 @Controller
 @Path("{version}/oauth2")
-@ResourceFilters({APIVersionFilter.class})
+@ResourceFilters({ APIVersionFilter.class })
 public class OAuth2Controller {
 
     @Autowired
@@ -60,7 +61,7 @@
      * Kustvakt supports authorization only with Kalamar as the
      * authorization web-frontend or user interface. Thus
      * authorization code request requires user authentication
-     * using authentication header.
+     * using authorization header.
      * 
      * <br /><br />
      * RFC 6749:
@@ -243,4 +244,40 @@
             throw responseHandler.throwit(e);
         }
     }
+
+    /**
+     * Revokes all tokens of a client from a super client. This
+     * service is not part of the OAUTH2 specification. It requires
+     * user authentication via authorization header, and super client
+     * via URL-encoded form parameters.
+     * 
+     * @param request
+     * @param form
+     * @return
+     */
+    @POST
+    @Path("revoke/super")
+    @ResourceFilters({ AuthenticationFilter.class, BlockingFilter.class })
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response revokeTokenViaSuperClient (
+            @Context HttpServletRequest request,
+            MultivaluedMap<String, String> form) {
+
+        try {
+            OAuth2RevokeTokenSuperRequest revokeTokenRequest =
+                    new OAuth2RevokeTokenSuperRequest(
+                            new FormRequestWrapper(request, form));
+            tokenService.revokeTokenViaSuperClient(revokeTokenRequest);
+            return Response.ok().build();
+        }
+        catch (OAuthSystemException e) {
+            throw responseHandler.throwit(e);
+        }
+        catch (OAuthProblemException e) {
+            throw responseHandler.throwit(e);
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
index a373b72..bb57104 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
@@ -482,6 +482,9 @@
                 code);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
+        JsonNode node = JsonUtils.readTree(response.getEntity(String.class));
+        String refreshToken = node.at("/refresh_token").asText();
+
         // client 2
         code = requestAuthorizationCode(confidentialClientId, clientSecret,
                 null, userAuthHeader);
@@ -490,10 +493,20 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         requestUserClientList();
-        testListClientWithMultipleRefreshTokens();
+        testListClientWithMultipleRefreshTokens(userAuthHeader);
+
+        testRequestTokenWithRevokedRefreshToken(publicClientId, clientSecret,
+                refreshToken);
+
+        node = JsonUtils.readTree(response.getEntity(String.class));
+        String accessToken = node.at("/access_token").asText();
+        refreshToken = node.at("/refresh_token").asText();
+
+        testRevokeTokenViaSuperClient(confidentialClientId, userAuthHeader,
+                accessToken, refreshToken);
     }
 
-    private void testListClientWithMultipleRefreshTokens ()
+    private void testListClientWithMultipleRefreshTokens (String userAuthHeader)
             throws KustvaktException {
         // client 1
         String code = requestAuthorizationCode(publicClientId, clientSecret,
@@ -504,6 +517,44 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         requestUserClientList();
+
+        JsonNode node = JsonUtils.readTree(response.getEntity(String.class));
+        String accessToken = node.at("/access_token").asText();
+        String refreshToken = node.at("/refresh_token").asText();
+
+        testRevokeTokenViaSuperClient(publicClientId, userAuthHeader,
+                accessToken, refreshToken);
     }
 
+    private void testRevokeTokenViaSuperClient (String clientId,
+            String userAuthHeader, String accessToken, String refreshToken)
+            throws KustvaktException {
+        // check token before revoking
+        ClientResponse response = searchWithAccessToken(accessToken);
+        JsonNode node = JsonUtils.readTree(response.getEntity(String.class));
+        assertTrue(node.at("/matches").size() > 0);
+
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("client_id", clientId);
+        form.add("super_client_id", superClientId);
+        form.add("super_client_secret", clientSecret);
+
+        response = resource().path(API_VERSION).path("oauth2").path("revoke")
+                .path("super").header(Attributes.AUTHORIZATION, userAuthHeader)
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        response = searchWithAccessToken(accessToken);
+        node = JsonUtils.readTree(response.getEntity(String.class));
+        assertEquals(StatusCodes.INVALID_ACCESS_TOKEN,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Access token has been revoked",
+                node.at("/errors/0/1").asText());
+
+        testRequestTokenWithRevokedRefreshToken(clientId, clientSecret,
+                refreshToken);
+    }
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
index e454762..863a6ec 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
@@ -184,7 +184,7 @@
         testRequestRefreshTokenInvalidRefreshToken(publicClientId);
         testRequestRefreshTokenPublicClient(publicClientId, refreshToken);
 
-        testRequestRefreshWithRevokedRefreshToken(publicClientId, null,
+        testRequestTokenWithRevokedRefreshToken(publicClientId, null,
                 refreshToken);
     }
 
@@ -222,7 +222,7 @@
         String refreshToken = node.at("/refresh_token").asText();
         testRevokeToken(refreshToken, confidentialClientId,clientSecret,
                 "refresh_token");
-        testRequestRefreshWithRevokedRefreshToken(confidentialClientId, clientSecret, refreshToken);
+        testRequestTokenWithRevokedRefreshToken(confidentialClientId, clientSecret, refreshToken);
     }
 
     private void testRequestTokenWithUsedAuthorization (String code)
@@ -630,27 +630,6 @@
         assertEquals(OAuth2Error.INVALID_GRANT, node.at("/error").asText());
     }
 
-    private void testRequestRefreshWithRevokedRefreshToken (String clientId,
-            String clientSecret, String refreshToken) throws KustvaktException {
-        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
-        form.add("grant_type", GrantType.REFRESH_TOKEN.toString());
-        form.add("client_id", clientId);
-        form.add("refresh_token", refreshToken);
-        if (clientSecret!=null){
-            form.add("client_secret", clientSecret);
-        }
-
-        ClientResponse response = resource().path(API_VERSION).path("oauth2").path("token")
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(HttpHeaders.CONTENT_TYPE,
-                        ContentType.APPLICATION_FORM_URLENCODED)
-                .entity(form).post(ClientResponse.class);
-
-        String entity = response.getEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(OAuth2Error.INVALID_GRANT, node.at("/error").asText());
-    }
-
     private void testRevokeToken (String token, String clientId,
             String clientSecret, String tokenType) {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
index 42318bc..cfb3f41 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
@@ -8,6 +8,7 @@
 import javax.ws.rs.core.Response.Status;
 
 import org.apache.http.entity.ContentType;
+import org.apache.oltu.oauth2.common.message.types.GrantType;
 import org.springframework.util.MultiValueMap;
 import org.springframework.web.util.UriComponentsBuilder;
 
@@ -22,6 +23,7 @@
 import de.ids_mannheim.korap.config.Attributes;
 import de.ids_mannheim.korap.config.SpringJerseyTest;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
 import de.ids_mannheim.korap.utils.JsonUtils;
 
 /**
@@ -38,7 +40,7 @@
     protected String superClientId = "fCBbQkAyYzI4NzUxMg";
     protected String clientSecret = "secret";
 
-    public ClientResponse requestAuthorizationCode (
+    protected ClientResponse requestAuthorizationCode (
             MultivaluedMap<String, String> form, String authHeader)
             throws KustvaktException {
 
@@ -50,7 +52,7 @@
                 .entity(form).post(ClientResponse.class);
     }
 
-    public String requestAuthorizationCode (String clientId,
+    protected String requestAuthorizationCode (String clientId,
             String clientSecret, String scope, String authHeader)
             throws KustvaktException {
 
@@ -72,7 +74,7 @@
         return params.getFirst("code");
     }
 
-    public ClientResponse requestToken (MultivaluedMap<String, String> form)
+    protected ClientResponse requestToken (MultivaluedMap<String, String> form)
             throws KustvaktException {
         return resource().path(API_VERSION).path("oauth2").path("token")
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -82,7 +84,7 @@
     }
 
     // client credentials as form params
-    public ClientResponse requestTokenWithAuthorizationCodeAndForm (
+    protected ClientResponse requestTokenWithAuthorizationCodeAndForm (
             String clientId, String clientSecret, String code)
             throws KustvaktException {
 
@@ -99,7 +101,7 @@
     }
 
     // client credentials in authorization header
-    public JsonNode requestTokenWithAuthorizationCodeAndHeader (String clientId,
+    protected JsonNode requestTokenWithAuthorizationCodeAndHeader (String clientId,
             String code, String authHeader) throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", "authorization_code");
@@ -116,13 +118,13 @@
         return JsonUtils.readTree(entity);
     }
 
-    public ClientResponse requestTokenWithDoryPassword (String clientId,
+    protected ClientResponse requestTokenWithDoryPassword (String clientId,
             String clientSecret) throws KustvaktException {
         return requestTokenWithPassword(clientId, clientSecret, "dory",
                 "password");
     }
 
-    public ClientResponse requestTokenWithPassword (String clientId,
+    protected ClientResponse requestTokenWithPassword (String clientId,
             String clientSecret, String username, String password)
             throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
@@ -134,8 +136,32 @@
 
         return requestToken(form);
     }
+    
+    protected void testRequestTokenWithRevokedRefreshToken (String clientId,
+            String clientSecret, String refreshToken) throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", GrantType.REFRESH_TOKEN.toString());
+        form.add("client_id", clientId);
+        form.add("refresh_token", refreshToken);
+        if (clientSecret != null) {
+            form.add("client_secret", clientSecret);
+        }
 
-    public void updateClientPrivilege (MultivaluedMap<String, String> form)
+        ClientResponse response =
+                resource().path(API_VERSION).path("oauth2").path("token")
+                        .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                        .header(HttpHeaders.CONTENT_TYPE,
+                                ContentType.APPLICATION_FORM_URLENCODED)
+                        .entity(form).post(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_GRANT, node.at("/error").asText());
+        assertEquals("Refresh token has been revoked",
+                node.at("/error_description").asText());
+    }
+
+    protected void updateClientPrivilege (MultivaluedMap<String, String> form)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
         ClientResponse response = resource().path(API_VERSION).path("oauth2")
@@ -149,7 +175,7 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
     }
 
-    public ClientResponse searchWithAccessToken (String accessToken) {
+    protected ClientResponse searchWithAccessToken (String accessToken) {
         return resource().path(API_VERSION).path("search")
                 .queryParam("q", "Wasser").queryParam("ql", "poliqarp")
                 .header(Attributes.AUTHORIZATION, "Bearer " + accessToken)