Implemented OAuth2 revoke token.

Change-Id: I896aeb759741395c1516772fea15886bb2470cdf
diff --git a/full/Changes b/full/Changes
index 081208d..bba0520 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,7 +1,9 @@
 version 0.61.0
-02/08/2018
+03/08/2018
 	- Added VC referencing tests (margaretha)
 	- Implemented loading and caching named VCs (margaretha)
+	- Implemented OAuth2 revoke token (margaretha)
+	- Updated OAuth2 refresh token implementation (margaretha)
 
 version 0.60.5
 02/08/2018
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java b/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
index 8f5e702..1b42d3e 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
@@ -34,7 +34,8 @@
 
         AccessToken accessToken = accessDao.retrieveAccessToken(authToken);
         if (accessToken.isRevoked()) {
-            throw new KustvaktException(StatusCodes.EXPIRED);
+            throw new KustvaktException(StatusCodes.INVALID_ACCESS_TOKEN,
+                    "Access token has been revoked");
         }
 
         ZonedDateTime expiry = accessToken.getCreatedDate()
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/AnnotationDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/AnnotationDao.java
index afc360f..22f2f37 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/AnnotationDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/AnnotationDao.java
@@ -36,6 +36,7 @@
      * 
      * @return a list of foundry-layer pairs.
      */
+    @SuppressWarnings("unchecked")
     public List<AnnotationPair> getAllFoundryLayerPairs () {
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
         CriteriaQuery<AnnotationPair> query =
@@ -61,6 +62,7 @@
      *            a layer code
      * @return a list of foundry-layer pairs.
      */
+    @SuppressWarnings("unchecked")
     public List<AnnotationPair> getAnnotationDescriptions (String foundry,
             String layer) {
 
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java
index 8bd533d..80a52a2 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java
@@ -54,6 +54,7 @@
         }
     }
 
+    @SuppressWarnings("unchecked")
     public List<Privilege> retrievePrivilegeByRoleId (int roleId) {
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
         CriteriaQuery<Privilege> query =
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/ResourceDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/ResourceDao.java
index 16913bd..0de5c9c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/ResourceDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/ResourceDao.java
@@ -30,6 +30,7 @@
      *  
      * @return a list of resources
      */
+    @SuppressWarnings("unchecked")
     public List<Resource> getAllResources () {
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
         CriteriaQuery<Resource> query =
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
index b02a283..8898685 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
@@ -78,6 +78,7 @@
         return (Role) q.getSingleResult();
     }
 
+    @SuppressWarnings("unchecked")
     public List<Role> retrieveRoleByGroupMemberId (int userId) {
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
         CriteriaQuery<Role> query = criteriaBuilder.createQuery(Role.class);
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
index 74d5b1a..2e59379 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
@@ -163,6 +163,7 @@
      * @return a list of UserGroup
      * @throws KustvaktException
      */
+    @SuppressWarnings("unchecked")
     public List<UserGroup> retrieveGroupByUserId (String userId)
             throws KustvaktException {
         ParameterChecker.checkStringValue(userId, "userId");
@@ -268,6 +269,7 @@
      * @return a list of {@link UserGroup}s
      * @throws KustvaktException
      */
+    @SuppressWarnings("unchecked")
     public List<UserGroup> retrieveGroupByStatus (String userId,
             UserGroupStatus status) throws KustvaktException {
 
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
index 47fef03..6d1975e 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
@@ -101,6 +101,7 @@
 
     }
 
+    @SuppressWarnings("unchecked")
     public List<UserGroupMember> retrieveMemberByRole (int groupId, int roleId)
             throws KustvaktException {
         ParameterChecker.checkIntegerValue(roleId, "roleId");
@@ -139,6 +140,7 @@
         return retrieveMemberByGroupId(groupId, false);
     }
 
+    @SuppressWarnings("unchecked")
     public List<UserGroupMember> retrieveMemberByGroupId (int groupId,
             boolean isAdmin) throws KustvaktException {
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusAccessDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusAccessDao.java
index 00eb460..3df91d3 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusAccessDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusAccessDao.java
@@ -58,6 +58,7 @@
     }
 
     // for vca admins
+    @SuppressWarnings("unchecked")
     public List<VirtualCorpusAccess> retrieveActiveAccessByVC (int vcId)
             throws KustvaktException {
         ParameterChecker.checkIntegerValue(vcId, "vcId");
@@ -82,6 +83,7 @@
     }
 
     // for system admins
+    @SuppressWarnings("unchecked")
     public List<VirtualCorpusAccess> retrieveAllAccessByVC (int vcId)
             throws KustvaktException {
         ParameterChecker.checkIntegerValue(vcId, "vcId");
@@ -101,6 +103,7 @@
         return q.getResultList();
     }
 
+    @SuppressWarnings("unchecked")
     public List<VirtualCorpusAccess> retrieveAllAccessByGroup (int groupId)
             throws KustvaktException {
         ParameterChecker.checkIntegerValue(groupId, "groupId");
@@ -120,6 +123,7 @@
         return q.getResultList();
     }
 
+    @SuppressWarnings("unchecked")
     public List<VirtualCorpusAccess> retrieveActiveAccessByGroup (int groupId)
             throws KustvaktException {
         ParameterChecker.checkIntegerValue(groupId, "groupId");
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusDao.java
index 4b26b2f..9e9983c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusDao.java
@@ -116,6 +116,7 @@
      * @return a list of {@link VirtualCorpus}
      * @throws KustvaktException
      */
+    @SuppressWarnings("unchecked")
     public List<VirtualCorpus> retrieveVCByType (VirtualCorpusType type,
             String createdBy) throws KustvaktException {
 
@@ -171,6 +172,7 @@
         return vc;
     }
 
+    @SuppressWarnings("unchecked")
     public List<VirtualCorpus> retrieveOwnerVC (String userId)
             throws KustvaktException {
         ParameterChecker.checkStringValue(userId, "userId");
@@ -188,6 +190,7 @@
         return q.getResultList();
     }
 
+    @SuppressWarnings("unchecked")
     public List<VirtualCorpus> retrieveOwnerVCByType (String userId,
             VirtualCorpusType type) throws KustvaktException {
         ParameterChecker.checkStringValue(userId, "userId");
@@ -209,6 +212,7 @@
         return q.getResultList();
     }
 
+    @SuppressWarnings("unchecked")
     public List<VirtualCorpus> retrieveGroupVCByUser (String userId)
             throws KustvaktException {
         ParameterChecker.checkStringValue(userId, "userId");
@@ -272,6 +276,7 @@
         query.distinct(true);
         Query q = entityManager.createQuery(query);
 
+        @SuppressWarnings("unchecked")
         List<VirtualCorpus> vcList = q.getResultList();
         List<VirtualCorpus> groupVC = retrieveGroupVCByUser(userId);
         Set<VirtualCorpus> vcSet = new HashSet<VirtualCorpus>();
@@ -285,6 +290,7 @@
     }
 
     // for admins
+    @SuppressWarnings("unchecked")
     public List<VirtualCorpus> retrieveVCByGroup (int groupId)
             throws KustvaktException {
         ParameterChecker.checkIntegerValue(groupId, "groupId");
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java
index a402c33..a94b506 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java
@@ -22,6 +22,7 @@
     @PersistenceContext
     private EntityManager entityManager;
 
+    @SuppressWarnings("unchecked")
     public List<AccessScope> retrieveAccessScopes () {
         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
         CriteriaQuery<AccessScope> query =
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 9e96654..67503e1 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
@@ -1,6 +1,7 @@
 package de.ids_mannheim.korap.oauth2.dao;
 
 import java.time.ZonedDateTime;
+import java.util.List;
 import java.util.Set;
 
 import javax.persistence.EntityManager;
@@ -9,6 +10,7 @@
 import javax.persistence.Query;
 import javax.persistence.criteria.CriteriaBuilder;
 import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Predicate;
 import javax.persistence.criteria.Root;
 
 import org.springframework.stereotype.Repository;
@@ -55,9 +57,9 @@
             Set<AccessScope> scopes, String userId, String clientId,
             ZonedDateTime authenticationTime) throws KustvaktException {
         ParameterChecker.checkStringValue(token, "access token");
-        ParameterChecker.checkStringValue(refreshToken, "refresh token");
+        ParameterChecker.checkObjectValue(refreshToken, "refresh token");
         ParameterChecker.checkObjectValue(scopes, "scopes");
-//        ParameterChecker.checkStringValue(userId, "username");
+        // ParameterChecker.checkStringValue(userId, "username");
         ParameterChecker.checkStringValue(clientId, "client_id");
         ParameterChecker.checkObjectValue(authenticationTime,
                 "authentication time");
@@ -75,7 +77,7 @@
 
     public AccessToken retrieveAccessToken (String accessToken)
             throws KustvaktException {
-
+        ParameterChecker.checkStringValue(accessToken, "access_token");
         AccessToken token = (AccessToken) this.getCacheValue(accessToken);
         if (token != null) {
             return token;
@@ -99,31 +101,29 @@
         }
     }
 
-    public AccessToken retrieveAccessTokenByRefreshToken (String refreshToken)
-            throws KustvaktException {
+    @SuppressWarnings("unchecked")
+    public List<AccessToken> retrieveAccessTokenByRefreshToken (
+            String refreshToken) throws KustvaktException {
 
+        ParameterChecker.checkStringValue(refreshToken, "refresh_token");
         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
         CriteriaQuery<AccessToken> query =
                 builder.createQuery(AccessToken.class);
         Root<AccessToken> root = query.from(AccessToken.class);
-        query.select(root);
-        query.where(builder.equal(root.get(AccessToken_.refreshToken),
-                refreshToken));
-        Query q = entityManager.createQuery(query);
-        try {
-            AccessToken token = (AccessToken) q.getSingleResult();
-            return token;
-        }
-        catch (NoResultException e) {
-            throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
-                    "Refresh token is not found", OAuth2Error.INVALID_GRANT);
-        }
+        Predicate condition = builder.equal(root.get(AccessToken_.refreshToken),
+                refreshToken);
 
+        query.select(root);
+        query.where(condition);
+        query.orderBy(builder.desc(root.get(AccessToken_.createdDate)));
+
+        Query q = entityManager.createQuery(query);
+        return q.getResultList();
     }
 
     public AccessToken updateAccessToken (AccessToken accessToken)
             throws KustvaktException {
-        ParameterChecker.checkObjectValue(accessToken, "access token");
+        ParameterChecker.checkObjectValue(accessToken, "access_token");
         AccessToken cachedToken =
                 (AccessToken) this.getCacheValue(accessToken.getId());
         if (cachedToken != null) {
@@ -133,4 +133,36 @@
         accessToken = entityManager.merge(accessToken);
         return accessToken;
     }
+
+    public AccessToken retrieveAccessTokenByAnynomousToken (String token)
+            throws KustvaktException {
+        ParameterChecker.checkObjectValue(token, "token");
+        AccessToken accessToken = (AccessToken) this.getCacheValue(token);
+        if (accessToken != null) {
+            return accessToken;
+        }
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AccessToken> query =
+                builder.createQuery(AccessToken.class);
+
+        Root<AccessToken> root = query.from(AccessToken.class);
+        Predicate condition = builder.or(
+                builder.equal(root.get(AccessToken_.token), token),
+                builder.equal(root.get(AccessToken_.refreshToken), token));
+
+
+        query.select(root);
+        query.where(condition);
+        Query q = entityManager.createQuery(query);
+
+        try {
+            accessToken = (AccessToken) q.getSingleResult();
+            return accessToken;
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.INVALID_ACCESS_TOKEN,
+                    "Access token is not found", OAuth2Error.INVALID_TOKEN);
+        }
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/OAuth2RevokeTokenRequest.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/OAuth2RevokeTokenRequest.java
new file mode 100644
index 0000000..78bf412
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/OAuth2RevokeTokenRequest.java
@@ -0,0 +1,83 @@
+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.as.request.OAuthRequest;
+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;
+
+/**
+ * A custom request based on {@link OAuthRequest}.
+ * 
+ * This class does not extend {@link OAuthRequest} because it contains some
+ * parameters i.e. redirect_uri and scopes that are not parts of
+ * revoke token request.
+ * 
+ * @author margaretha
+ *
+ */
+public class OAuth2RevokeTokenRequest {
+
+    protected HttpServletRequest request;
+    protected OAuthValidator<HttpServletRequest> validator;
+    protected Map<String, Class<? extends OAuthValidator<HttpServletRequest>>> validators =
+            new HashMap<String, Class<? extends OAuthValidator<HttpServletRequest>>>();
+
+    public OAuth2RevokeTokenRequest () {}
+
+    public OAuth2RevokeTokenRequest (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);
+//        validator.validateClientAuthenticationCredentials(request);
+    }
+
+    protected OAuthValidator<HttpServletRequest> initValidator ()
+            throws OAuthProblemException, OAuthSystemException {
+        return OAuthUtils.instantiateClass(RevokeTokenValidator.class);
+    }
+
+    public String getParam (String name) {
+        return request.getParameter(name);
+    }
+
+    public String getToken () {
+        return getParam("token");
+    }
+    
+    public String getTokenType () {
+        return getParam(OAuth.OAUTH_TOKEN_TYPE);
+    }
+
+    public String getClientId () {
+        String[] creds = OAuthUtils.decodeClientAuthenticationHeader(
+                request.getHeader(OAuth.HeaderType.AUTHORIZATION));
+        if (creds != null) {
+            return creds[0];
+        }
+        return getParam(OAuth.OAUTH_CLIENT_ID);
+    }
+
+    public String getClientSecret () {
+        String[] creds = OAuthUtils.decodeClientAuthenticationHeader(
+                request.getHeader(OAuth.HeaderType.AUTHORIZATION));
+        if (creds != null) {
+            return creds[1];
+        }
+        return getParam(OAuth.OAUTH_CLIENT_SECRET);
+    }
+}
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
new file mode 100644
index 0000000..0a9d9c7
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/RevokeTokenValidator.java
@@ -0,0 +1,45 @@
+package de.ids_mannheim.korap.oauth2.oltu;
+
+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.validators.AbstractValidator;
+
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
+
+/**
+ * A custom revoke token validator based on RFC 7009.
+ * 
+ * Additional changes to the RFC:
+ * <ul>
+ * <li>client_id is made required for public client
+ * authentication</li>
+ * </ul>
+ * 
+ * @author margaretha
+ *
+ */
+public class RevokeTokenValidator
+        extends AbstractValidator<HttpServletRequest> {
+
+    public RevokeTokenValidator () {
+        requiredParams.add("token");
+        requiredParams.add(OAuth.OAUTH_CLIENT_ID);
+    }
+
+    @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 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 c9b574d..c53fbb6 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
@@ -2,6 +2,7 @@
 
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
+import java.util.List;
 import java.util.Set;
 
 import javax.ws.rs.core.Response.Status;
@@ -25,14 +26,15 @@
 import de.ids_mannheim.korap.oauth2.entity.AccessToken;
 import de.ids_mannheim.korap.oauth2.entity.Authorization;
 import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.oauth2.oltu.OAuth2RevokeTokenRequest;
 import de.ids_mannheim.korap.oauth2.service.OAuth2TokenService;
 
 @Service
 public class OltuTokenService extends OAuth2TokenService {
-    
+
     @Autowired
-    private RandomCodeGenerator randomGenerator; 
-    
+    private RandomCodeGenerator randomGenerator;
+
     @Autowired
     private AccessTokenDao tokenDao;
 
@@ -70,6 +72,23 @@
 
     }
 
+    /**
+     * Revokes all access token associated with the given refresh
+     * token, and creates a new access token with the given refresh
+     * token and scopes. Thus, at one point of time, there is only one
+     * active access token associated with a refresh token.
+     * 
+     * Client authentication is done using the given client
+     * credentials.
+     * 
+     * @param refreshToken
+     * @param scopes
+     * @param clientId
+     * @param clientSecret
+     * @return if successful, a new access token
+     * @throws KustvaktException
+     * @throws OAuthSystemException
+     */
     private OAuthResponse requestAccessTokenWithRefreshToken (
             String refreshToken, Set<String> scopes, String clientId,
             String clientSecret)
@@ -82,39 +101,47 @@
         }
 
         clientService.authenticateClient(clientId, clientSecret);
-        AccessToken accessToken =
+        List<AccessToken> accessTokenList =
                 tokenDao.retrieveAccessTokenByRefreshToken(refreshToken);
+        if (accessTokenList.isEmpty()) {
+            throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
+                    "Refresh token is not found", OAuth2Error.INVALID_GRANT);
+        }
 
-        if (!clientId.equals(accessToken.getClientId())) {
+        AccessToken latestAccessToken = accessTokenList.get(0);
+        AccessToken origin = accessTokenList.get(accessTokenList.size() - 1);
+
+        if (!clientId.equals(latestAccessToken.getClientId())) {
             throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
                     "Client " + clientId + "is not authorized",
                     OAuth2Error.INVALID_CLIENT);
         }
 
-        if (accessToken.isRefreshTokenRevoked()) {
+        if (latestAccessToken.isRefreshTokenRevoked()) {
             throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
                     "Refresh token has been revoked.",
                     OAuth2Error.INVALID_GRANT);
         }
         else if (ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))
-                .isAfter(accessToken.getCreatedDate()
+                .isAfter(origin.getCreatedDate()
                         .plusSeconds(config.getRefreshTokenExpiry()))) {
             throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
                     "Refresh token is expired.", OAuth2Error.INVALID_GRANT);
         }
 
-        Set<AccessScope> requestedScopes = accessToken.getScopes();
+        Set<AccessScope> requestedScopes = latestAccessToken.getScopes();
         if (scopes != null && !scopes.isEmpty()) {
             requestedScopes = scopeService.verifyRefreshScope(scopes,
-                    accessToken.getScopes());
+                    latestAccessToken.getScopes());
         }
 
-        accessToken.setRefreshTokenRevoked(true);
-        accessToken = tokenDao.updateAccessToken(accessToken);
+        if (!latestAccessToken.isRevoked()) {
+            tokenDao.updateAccessToken(latestAccessToken);
+        }
 
         return createsAccessTokenResponse(scopes, requestedScopes, clientId,
-                accessToken.getUserId(),
-                accessToken.getUserAuthenticationTime());
+                latestAccessToken.getUserId(),
+                latestAccessToken.getUserAuthenticationTime(), refreshToken);
     }
 
     /**
@@ -280,8 +307,16 @@
             ZonedDateTime authenticationTime)
             throws OAuthSystemException, KustvaktException {
 
-        String accessToken = randomGenerator.createRandomCode();
         String refreshToken = randomGenerator.createRandomCode();
+        return createsAccessTokenResponse(scopes, accessScopes, clientId,
+                userId, authenticationTime, refreshToken);
+    }
+
+    private OAuthResponse createsAccessTokenResponse (Set<String> scopes,
+            Set<AccessScope> accessScopes, String clientId, String userId,
+            ZonedDateTime authenticationTime, String refreshToken)
+            throws OAuthSystemException, KustvaktException {
+        String accessToken = randomGenerator.createRandomCode();
 
         tokenDao.storeAccessToken(accessToken, refreshToken, accessScopes,
                 userId, clientId, authenticationTime);
@@ -293,4 +328,55 @@
                 .setRefreshToken(refreshToken)
                 .setScope(String.join(" ", scopes)).buildJSONMessage();
     }
+
+    public void revokeToken (OAuth2RevokeTokenRequest revokeTokenRequest)
+            throws KustvaktException {
+        String clientId = revokeTokenRequest.getClientId();
+        String clientSecret = revokeTokenRequest.getClientSecret();
+        String token = revokeTokenRequest.getToken();
+        String tokenType = revokeTokenRequest.getTokenType();
+
+        clientService.authenticateClient(clientId, clientSecret);
+        tokenDao.removeCacheEntry(token);
+        if (tokenType != null && tokenType.equals("refresh_token")) {
+            if (!revokeRefreshToken(token)) {
+                revokeAccessToken(token);
+            }
+            return;
+        }
+
+        if (!revokeAccessToken(token)) {
+            revokeRefreshToken(token);
+        }
+    }
+
+    private boolean revokeAccessToken (String token) throws KustvaktException {
+        try {
+            AccessToken accessToken = tokenDao.retrieveAccessToken(token);
+            accessToken.setRevoked(true);
+            tokenDao.updateAccessToken(accessToken);
+            return true;
+        }
+        catch (KustvaktException e) {
+            if (!e.getStatusCode().equals(StatusCodes.INVALID_ACCESS_TOKEN)) {
+                return false;
+            }
+            throw e;
+        }
+    }
+
+    private boolean revokeRefreshToken (String token) throws KustvaktException {
+        List<AccessToken> accessTokenList =
+                tokenDao.retrieveAccessTokenByRefreshToken(token);
+        if (accessTokenList.isEmpty()) {
+            return false;
+        }
+
+        for (AccessToken accessToken : accessTokenList) {
+            accessToken.setRevoked(true);
+            accessToken.setRefreshTokenRevoked(true);
+            tokenDao.updateAccessToken(accessToken);
+        }
+        return true;
+    }
 }
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 4ab46b4..e78aa2a 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
@@ -24,10 +24,12 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 
+import com.sun.jersey.api.client.ClientResponse.Status;
 import com.sun.jersey.spi.container.ResourceFilters;
 
 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.service.OltuAuthorizationService;
 import de.ids_mannheim.korap.oauth2.oltu.service.OltuTokenService;
 import de.ids_mannheim.korap.security.context.TokenContext;
@@ -106,15 +108,26 @@
     /**
      * Grants a client an access token, namely a string used in
      * authenticated requests representing user authorization for
-     * the client to access user resources. Client credentials for
-     * authentication can be provided either as an authorization
-     * header with Basic authentication scheme or as form parameters
-     * in the request body.
+     * the client to access user resources. An additional refresh
+     * token strictly associated to the access token is also granted.
      * 
      * <br /><br />
      * 
-     * OAuth2 describes various ways of requesting an access token.
-     * Kustvakt supports:
+     * Clients may refreshing access token using this endpoint. This
+     * request will revoke all access token associated with the given
+     * refresh token, and grants a new access token. The refresh token
+     * is not changed and can be used until it expires.
+     * 
+     * <br /><br />
+     * 
+     * Client credentials for authentication can be provided either as
+     * an authorization header with Basic authentication scheme or as
+     * form parameters in the request body.
+     * 
+     * <br /><br />
+     * 
+     * OAuth2 specification describes various ways of requesting an
+     * access token. Kustvakt supports:
      * <ul>
      * <li> Authorization code grant: obtains authorization from a
      * third party application. Required parameters: grant_type,
@@ -135,7 +148,6 @@
      * </li>
      * </ul>
      * 
-     * <br /><br />
      * RFC 6749: The value of the scope parameter is expressed as a
      * list of space-delimited, case-sensitive strings defined by the
      * authorization server.
@@ -186,14 +198,40 @@
         }
     }
 
-    // @POST
-    // @Path("revoke")
-    // @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
-    // @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
-    // public Response revokeAccessToken (@Context HttpServletRequest
-    // request,
-    // @FormParam("grant_type") String grantType,
-    // MultivaluedMap<String, String> form) {
-    // return null;
-    // }
+    /**
+     * Revoking an access token also revokes its refresh token, vice
+     * versa.
+     * 
+     * RFC 7009
+     * Client authentication for confidential client
+     * 
+     * @param request
+     * @param form
+     * @return 200 if token invalidation is successful or the given
+     *         token is invalid
+     */
+    @POST
+    @Path("revoke")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public Response revokeAccessToken (@Context HttpServletRequest request,
+            MultivaluedMap<String, String> form) {
+
+        try {
+            OAuth2RevokeTokenRequest revokeTokenRequest =
+                    new OAuth2RevokeTokenRequest(
+                            new FormRequestWrapper(request, form));
+            tokenService.revokeToken(revokeTokenRequest);
+            return Response.ok().build();
+        }
+        catch (OAuthProblemException e) {
+            throw responseHandler.throwit(e);
+        }
+        catch (OAuthSystemException 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/OAuth2AccessTokenTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
index d7bcde2..8282baa 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
@@ -77,9 +77,16 @@
     }
 
     @Test
-    public void testListVCScopeNotAuthorized ()
-            throws KustvaktException, IOException {
+    public void testTokenAccessScope () throws KustvaktException, IOException {
         String accessToken = requestToken();
+        testListVCScopeNotAuthorized(accessToken);
+        testListVCAccessBearerNotAuthorize(accessToken);
+        testSearchWithOAuth2Token(accessToken);
+
+    }
+
+    private void testListVCScopeNotAuthorized (String accessToken)
+            throws KustvaktException {
         ClientResponse response = resource().path("vc").path("list")
                 .header(Attributes.AUTHORIZATION, "Bearer " + accessToken)
                 .get(ClientResponse.class);
@@ -92,9 +99,6 @@
                 node.at("/errors/0/0").asInt());
         assertEquals("Scope vc_info is not authorized",
                 node.at("/errors/0/1").asText());
-
-        testListVCAccessBearerNotAuthorize(accessToken);
-        testSearchWithOAuth2Token(accessToken);
     }
 
     private void testListVCAccessBearerNotAuthorize (String accessToken)
@@ -151,4 +155,41 @@
         assertEquals("Access token is not found",
                 node.at("/errors/0/1").asText());
     }
+
+    @Test
+    public void testRevokeAccessTokenConfidentialClient ()
+            throws KustvaktException {
+        String accessToken = requestToken();
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("token", accessToken);
+        form.add("client_id", "fCBbQkAyYzI4NzUxMg");
+        form.add("client_secret", "secret");
+
+        ClientResponse response = resource().path("oauth2").path("revoke")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
+        testSearchWithRevokedToken(accessToken);
+    }
+
+    private void testSearchWithRevokedToken (String accessToken)
+            throws KustvaktException {
+        ClientResponse response = resource().path("search")
+                .queryParam("q", "Wasser").queryParam("ql", "poliqarp")
+                .header(Attributes.AUTHORIZATION, "Bearer " + accessToken)
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .get(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        assertEquals(ClientResponse.Status.UNAUTHORIZED.getStatusCode(),
+                response.getStatus());
+        
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.INVALID_ACCESS_TOKEN, node.at("/errors/0/0").asInt());
+        assertEquals("Access token has been revoked", node.at("/errors/0/1").asText());
+
+    }
 }
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 a5ba179..3dabb7b 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
@@ -35,7 +35,7 @@
  */
 public class OAuth2ControllerTest extends SpringJerseyTest {
 
-    private ClientResponse requestAuthorizationConfidentialClient (
+    private ClientResponse requestAuthorization (
             MultivaluedMap<String, String> form) throws KustvaktException {
 
         return resource().path("oauth2").path("authorize").header(
@@ -55,7 +55,25 @@
         form.add("client_id", "fCBbQkAyYzI4NzUxMg");
         form.add("state", "thisIsMyState");
 
-        ClientResponse response = requestAuthorizationConfidentialClient(form);
+        ClientResponse response = requestAuthorization(form);
+
+        assertEquals(Status.TEMPORARY_REDIRECT.getStatusCode(),
+                response.getStatus());
+        URI redirectUri = response.getLocation();
+        MultiValueMap<String, String> params = UriComponentsBuilder
+                .fromUri(redirectUri).build().getQueryParams();
+        assertNotNull(params.getFirst("code"));
+        assertEquals("thisIsMyState", params.getFirst("state"));
+    }
+
+    @Test
+    public void testAuthorizePublicClient () throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("response_type", "code");
+        form.add("client_id", "8bIDtZnH6NvRkW2Fq");
+        form.add("state", "thisIsMyState");
+
+        ClientResponse response = requestAuthorization(form);
 
         assertEquals(Status.TEMPORARY_REDIRECT.getStatusCode(),
                 response.getStatus());
@@ -75,7 +93,7 @@
         form.add("client_id", "fCBbQkAyYzI4NzUxMg");
         form.add("redirect_uri", redirectUri);
         form.add("state", "thisIsMyState");
-        ClientResponse response = requestAuthorizationConfidentialClient(form);
+        ClientResponse response = requestAuthorization(form);
 
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
 
@@ -94,7 +112,7 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("state", "thisIsMyState");
         // missing response_type
-        ClientResponse response = requestAuthorizationConfidentialClient(form);
+        ClientResponse response = requestAuthorization(form);
 
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
 
@@ -108,7 +126,7 @@
 
         // missing client_id
         form.add("response_type", "code");
-        response = requestAuthorizationConfidentialClient(form);
+        response = requestAuthorization(form);
         entity = response.getEntity(String.class);
         node = JsonUtils.readTree(entity);
         assertEquals("Missing parameters: client_id",
@@ -121,7 +139,7 @@
         form.add("response_type", "string");
         form.add("state", "thisIsMyState");
 
-        ClientResponse response = requestAuthorizationConfidentialClient(form);
+        ClientResponse response = requestAuthorization(form);
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
 
         String entity = response.getEntity(String.class);
@@ -141,7 +159,7 @@
         form.add("scope", "read_address");
         form.add("state", "thisIsMyState");
 
-        ClientResponse response = requestAuthorizationConfidentialClient(form);
+        ClientResponse response = requestAuthorization(form);
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
 
         URI location = response.getLocation();
@@ -171,8 +189,7 @@
         authForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
         authForm.add("scope", "search");
 
-        ClientResponse response =
-                requestAuthorizationConfidentialClient(authForm);
+        ClientResponse response = requestAuthorization(authForm);
         URI redirectUri = response.getLocation();
         MultivaluedMap<String, String> params =
                 UriComponent.decodeQuery(redirectUri, true);
@@ -243,8 +260,7 @@
         authForm.add("scope", "search");
         authForm.add("redirect_uri", uri);
 
-        ClientResponse response =
-                requestAuthorizationConfidentialClient(authForm);
+        ClientResponse response = requestAuthorization(authForm);
         URI redirectUri = response.getLocation();
         MultivaluedMap<String, String> params =
                 UriComponent.decodeQuery(redirectUri, true);
@@ -434,17 +450,22 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         JsonNode node = JsonUtils.readTree(entity);
-        assertNotNull(node.at("/access_token").asText());
+        String accessToken = node.at("/access_token").asText();
         assertEquals(TokenType.BEARER.toString(),
                 node.at("/token_type").asText());
         assertNotNull(node.at("/expires_in").asText());
 
+
+        testRevokeTokenPublicClient(accessToken, clientId, "access_token");
+
         String refreshToken = node.at("/refresh_token").asText();
         testRequestRefreshTokenInvalidScope(clientId, refreshToken);
         testRequestRefreshTokenPublicClient(clientId, refreshToken);
         testRequestRefreshTokenInvalidClient(refreshToken);
         testRequestRefreshTokenInvalidRefreshToken(clientId);
-        testRevokedRefreshToken(clientId, refreshToken);
+        
+        testRevokeTokenPublicClient(refreshToken, clientId, "refresh_token");
+        testRequestRefreshWithRevokedRefreshToken(clientId, refreshToken);
     }
 
     @Test
@@ -490,6 +511,29 @@
         assertNotNull(node.at("/expires_in").asText());
     }
 
+    /**
+     * Client credentials grant is only allowed for confidential
+     * clients.
+     */
+    @Test
+    public void testRequestTokenClientCredentialsGrantPublic ()
+            throws KustvaktException {
+
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", "client_credentials");
+        form.add("client_id", "8bIDtZnH6NvRkW2Fq");
+        form.add("client_secret", "");
+        ClientResponse response = requestToken(form);
+
+        String entity = response.getEntity(String.class);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuthError.TokenResponse.INVALID_REQUEST,
+                node.at("/error").asText());
+        assertEquals("Missing parameters: client_secret",
+                node.at("/error_description").asText());
+    }
+
     @Test
     public void testRequestTokenClientCredentialsGrantReducedScope ()
             throws KustvaktException {
@@ -627,8 +671,8 @@
         assertEquals(OAuth2Error.INVALID_GRANT, node.at("/error").asText());
     }
 
-    private void testRevokedRefreshToken (String clientId, String refreshToken)
-            throws KustvaktException {
+    private void testRequestRefreshWithRevokedRefreshToken (String clientId,
+            String refreshToken) throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", GrantType.REFRESH_TOKEN.toString());
         form.add("client_id", clientId);
@@ -645,4 +689,19 @@
         assertEquals(OAuth2Error.INVALID_GRANT, node.at("/error").asText());
     }
 
+    private void testRevokeTokenPublicClient (String token,
+            String clientId, String tokenType) {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("token_type", tokenType);
+        form.add("token", token);
+        form.add("client_id", clientId);
+
+        ClientResponse response = resource().path("oauth2").path("revoke")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+        
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    }
+
 }